Category实现原理

336 阅读1分钟

我们都知道 Category 是在编译期决定结构,在运行时将内容附加到Class、MetaClass中,并且 Category 不能添加成员变量。 那么问题来了, 1.这个过程都经历了哪些步骤? 2.又是怎么将内容附加到Class、MetaClass中的呢? 3.成员变量这么好用的东西,为啥不让加呢?


实现原理

首先我们看看 Category 编译期都做啥了 先搞一个测试类和他的分类

// TYObject.h
@interface TYObject : NSObject
@end

// TYObject+Test.h
@interface TYObject (Test)
@property (strong, nonatomic) NSString *testStr;
@property (assign, nonatomic) NSInteger testInt;
- (void)test;
+ (void)test;
@end

使用 clang 编译器指令将其转为C++代码 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc TYObject+Test.m

TYObject+Test.cpp

struct _category_t {
	const char *name; // 类名
	struct _class_t *cls;
	const struct _method_list_t *instance_methods; // 实例方法列表
	const struct _method_list_t *class_methods; // 类方法列表
	const struct _protocol_list_t *protocols; // 协议列表
	const struct _prop_list_t *properties; // 属性列表
};

static struct _category_t _OBJC_$_CATEGORY_TYObject_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"TYObject",
	0, // &OBJC_CLASS_$_TYObject,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_TYObject_$_Test,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_TYObject_$_Test,
	0,
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_TYObject_$_Test,
};

// 实例方法列表
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_TYObject_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"test", "v16@0:8", (void *)_I_TYObject_Test_test}}
};

// 类方法列表
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_TYObject_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"test", "v16@0:8", (void *)_C_TYObject_Test_test}}
};

// 属性列表
static struct /*_prop_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count_of_properties;
	struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_TYObject_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_prop_t),
	2,
	{{"testStr","T@\"NSString\",&,N"},
	{"testInt","Tq,N"}}
};

由c++代码可知,编译期分类里的东西只放到 _category_t 的结构体里面,并没有合并到类里面去。

将 _category_t 里内容附加到 Class、MetaClass中的流程可以查看 Apple 关于objc的开源代码来了解。

首先查看 objc-os.mm 文件,这个是 runtime 的入口类。 (主要查看我添加中文注释的部分)

// 运行时的初始化
void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();
    
    // 主要在 map_images 方法里 image这里不是图片的意思,是镜像、模块的意思
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

void map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    mutex_locker_t lock(runtimeLock);
    
    // 继续进到这个里面查看
    return map_images_nolock(count, paths, mhdrs);
}

void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
    // ...
    
    if (hCount > 0) {
    // 相关逻辑在这里, 加载模块
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }
    
    // ...
}

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
    // ... 
    
    // Discover categories. (从这行注释开始就是有关gategory的逻辑)
    
    // ... 
    
    for (EACH_HEADER) {
        // 获取分类的数组
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);

            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Disavow any knowledge of this category.
                catlist[i] = nil;
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class", 
                                 cat->name, cat);
                }
                continue;
            }

            // Process this category. 
            // First, register the category with its target class. 
            // Then, rebuild the class's method lists (etc) if 
            // the class is realized. 
            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    // 核心方法,重新组织类对象方法结构
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls->nameForLogging(), cat->name, 
                                 classExists ? "on existing class" : "");
                }
            }

            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    // 核心方法,重新组织元类对象方法结构
                    remethodizeClass(cls->ISA());
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)", 
                                 cls->nameForLogging(), cat->name);
                }
            }
        }
    }

    ts.log("IMAGE TIMES: discover categories");
    
    // ... 
}

// 进到核心代码 remethodizeClass() 里查看
static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertLocked();

    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        
        // 主要逻辑在这里,附加Categories
        // cls:类对象
        // cats:分类对象数组
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

// 真正的添加步骤就在这里了
// cls = [TYOject class]
// cats = [category_t(TYOject+Test的), category_t(TYOject+其它的)]
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    
    // 方法数组(二维数组[[method_t, method_t],[method_t, method_t]])
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    // 属性数组(二维数组)
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    // 协议数组(二维数组)
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    
    // 这里很重要,这种遍历方式决定了分类中方法添加的顺序
    while (i--) {
        // 取出某个分类(先取出最后面的分类)
        auto& entry = cats->list[i];
        // 取出分类中方法列表(根据isMeta参数判断是实例方法还是类方法)
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            // 将分类中的对象方法数组放到mlists(上面创建的方法数组)中
            // mlists中方法的顺序:[后编译分类的方法,先编译分类的方法]
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        // 跟方法数组的逻辑一样
        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }
        // 跟方法数组的逻辑一样
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    // 取出类的 class_rw_t
    // 这里存的是类本身的方法、属性等数据
    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    
    // 将mlists(所有分类的对象方法)附加到类对象的方法列表中
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
    
    // 将proplists(所有分类的属性)附加到类对象的属性列表中
    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    // protolists(所有分类的协议)附加到类对象的协议列表中
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

// 这里使用methods.attachLists的实现举例
// 向类对象中添加方法函数的内部实现
void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            
            // 从这里开始是合并方法列表方式的逻辑
            // 重新分配数组内存(相当于扩容)
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            
            // array()->lists 是原来的方法列表
            // 将 array()->lists 向后挪动 addedCount 个位置
            memmove(array()->lists + addedCount,
                    array()->lists,
                    oldCount * sizeof(array()->lists[0]));
                    
            // addedLists 是所有分类的方法列表
            // 将 addedLists 内部 addedCount 个元素拷贝到 array()->lists 的位置
            memcpy(array()->lists,
                   addedLists,
                   addedCount * sizeof(array()->lists[0]));
                   
            // 由上述逻辑可知,类和分类中的方法同名,就会先调用分类中的。
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }

到这里,附加信息的逻辑就结束了。

总结:

当分类中方法数组附加到类对象中后,类对象中方法列表结构如下

后编译的分类方法列表 后编译 = [method_t, method_t]
先编译的分类方法列表 先编译 = [method_t, method_t]
类原有的方法列表 原有 = [method_t, method_t]
附加后的顺序 [后编译,先编译,原有]

编译顺序可在XCode项目中 TARGETS -> Build Phases -> Compile Sources 中拖动位置修改。(在上面的先编译,在下面的后编译)

成员变量

关于不能添加成员变量的问题:

根据clang编译器转出的c++代码可以看出 category_t结构体内部是没有成员变量的,所以我们如果强行手动在分类内部添加成员变量的话就会报错: Instance variables may not be placed in categories

那苹果为什么要这么设计呢? 我觉得要从几个角度去理解 1.OC对象的内存布局 2.加载时机

OC的实例对象底层实现都是结构体,结构体内部成员地址都是连续的,所以其结构在编译期就定好了,无法修改。 可以使用以下方法加以证明: 我们在测试用的自定义类中加入一个 int 数据类型的成员变量以供调试

@interface TYObject : NSObject
{
    @public int _age;
}
@end

在 main 函数中使用

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        TYObject *obj = [[TYObject alloc] init];
        obj->_age = 10;
        
    }
    return 0;
}

转成c++代码为

extern "C" unsigned long OBJC_IVAR_$_TYObject$_age;
struct TYObject_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	int _age;
};

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        TYObject *obj = ((TYObject *(*)(id, SEL))(void *)objc_msgSend)((id)((TYObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("TYObject"), sel_registerName("alloc")), sel_registerName("init"));
        (*(int *)((char *)obj + OBJC_IVAR_$_TYObject$_age)) = 10;

    }
    return 0;
}

注意(*(int *)((char *)obj + OBJC_IVAR_$_TYObject$_age)) = 10;这行赋值的代码,OBJC_IVAR_$_TYObject$_ageunsigned long类型,在我们64位系统中占8个字节。获取到obj的地址,加上8个字节刚好就是age成员变量所在的地址,然后赋值。

这也刚好就证明了,OC实例对象的成员变量内容是存在结构体内的,并且编译期就已经决定好结构体的内容都有什么。而category_t里的内容是在运行时附加到类里面的,如果在运行时时期去改变内存结构那将是件多可怕的事啊!

我们也可以从objc源码了解它的结构 ivars 是位于 class_ro_t 结构体内的 并且使用 const 来修饰

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars; // 成员变量列表

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
}

所以介于以上原因category是不可以添加成员变量的