category探究准备
先来创建我们测试需要的类:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @interface Animal : NSObject - (void)animal; @end
@interface Animal (Eat) <NSCopying, NSCoding> @property (nonatomic, assign) int age; - (void)eat; @end
@interface Animal (Play) - (void)play; @end
|
以Eat分类为例,请出 clang
命令:clang -rewrite-objc Animal+Eat.m
,生成.cpp文件。
category的真面目
在.cpp文件最下面可以找到category被编译后的结构:
1 2 3 4 5 6 7 8
| 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; };
|
name
这里的name表示的是 类名
而不是category的名字。
cls
要扩展的类对象,编译期间值为空,在被runtime加载时根据name对应到类对象。
instance_methods
category所有的实例方法。
class_methods
category所有的类方法。
protocols
category实现的所有协议。
properties
category的所有属性。
再来看看我们的Animal+Eat被编译成了什么:
1 2 3 4 5 6 7 8
| static struct _category_t _OBJC_$_CATEGORY_Animal_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = { "Animal", 0, (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Animal_$_Eat, 0, (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Animal_$_Eat, (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Animal_$_Eat, };
|
看一下结构体的名称:_OBJC_$_CATEGORY_Animal_$_Eat
,最后面的Eat就是我们分类的名称,前面有表示CATEGORY和类名Animal,这也就是为什么同一个类的category不能重名的原因了。
再对应一下其他的结构,例如instance_methods:
1 2 3 4 5 6 7 8 9
| static struct /*_method_list_t*/ { unsigned int entsize; unsigned int method_count; struct _objc_method method_list[1]; } _OBJC_$_CATEGORY_INSTANCE_METHODS_Animal_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = { sizeof(_objc_method), 1, {{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_Animal_Eat_eat}} };
|
我们里面只有一个 eat
方法,被编译后为 _I_Animal_Eat_eat
。
最后可以看到所有的category被放到了一个数组中,存在了 __DATA
段下的 __objc_catlist section
里了:
1 2 3
| static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= { &_OBJC_$_CATEGORY_Animal_$_Eat, };
|
这里编译期间的工作就做完了,接下来进入runtime。
runtime加载category
先下载一下苹果官方runtime的源码 这里,当然官方的编译是失败,要想调试runtime的请看 这里。
大致加载的流程如下:
- 找到runtime的入口:
objc-os.mm
的 _objc_init
方法,在library加载前由libSystem dyld调用,进行初始化操作。
- 调用map_images方法将文件中的image map到内存。
- 调用_read_images方法初始化map后的image。
- 找到
Discover categories
可以看到 category_t
是通过 _getObjc2CategoryList
方法初始化的,这个方法拿出来看看:
1 2 3 4 5 6 7 8 9
| type *name(const headerType *mhdr, size_t *outCount) { \ return getDataSection<type>(mhdr, sectname, nil, outCount); \ } \ type *name(const header_info *hi, size_t *outCount) { \ return getDataSection<type>(hi->mhdr(), sectname, nil, outCount); \ } GETSECT(_getObjc2CategoryList, category_t *, "__objc_catlist");
|
看到这里有没有很熟悉,在这里加载的 __objc_catlist
就是在编译期间存放的数据。
来看一下加载的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| for (EACH_HEADER) {
category_t **catlist = _getObjc2CategoryList(hi, &count);
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls); if (cat->instanceMethods || cat->protocols || cat->instanceProperties) { addUnattachedCategoryForClass(cat, cls, hi); if (cls->isRealized()) { remethodizeClass(cls); } } if (cat->classMethods || cat->protocols || (hasClassProperties && cat->_classProperties)) { addUnattachedCategoryForClass(cat, cls->ISA(), hi); if (cls->ISA()->isRealized()) { remethodizeClass(cls->ISA()); } } } }
|
可以看到每次循环中 category_t
的加载 addUnattachedCategoryForClass
方法有两个调用,对比一下参数可以发现第二个参数不同 cls 和 cls->ISA(),再结合判断条件的 cat->instanceMethods 和 cat->classMethods,这两次的加载是将category中的信息分别加载到类和元类中,然后再调用 remethodizeClass
重新组织结构。接下来调用附加信息的方法 attachCategories
,将分类的信息附加到类中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| attachCategories(Class cls, category_list *cats, bool flush_caches) {
bool isMeta = cls->isMetaClass();
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));
while (i--) {
auto& entry = cats->list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta); if (mlist) {
mlists[mcount++] = mlist; }
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; } }
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount); rw->properties.attachLists(proplists, propcount); rw->protocols.attachLists(protolists, protocount); }
|
由上面的while循环可以看到加载方法、协议、属性的时候是 倒序
加载的,是不是想到了什么?如果Animal类和两个分类都有一个 -(void)run
方法,那么最终会调用哪个里面的run方法呢?答案当然是最后加载的那个run方法,不过没有被调用的run方法并没有被 覆盖
,方法还在那里只是按顺序没有被调用。
最后看一下 methods.attachLists
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
void attachLists(List* const * addedLists, uint32_t addedCount) { uint32_t oldCount = array()->count; uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount))); array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0])); memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); }
|
重新布局的时候有两个方法:
memmove
:void *memmove(void *__dst, const void *__src, size_t __len);
可以看到是将src变量的数据移动到dst,所以最终是将 array()->lists 的数据移动到了 array()->lists + addedCount 的位置。
memcpy
:void *memcpy(void *__dst, const void *__src, size_t __n);
可以看到是将src变量的数据copy到dst,所以最终是将分类中的信息 addedLists
copy 到 array()->lists 的位置。
正如我们上面说的run方法,Animal类中的run方法是被最后加载的,因为Animal类中的方法列表被移动到了分类的后面,加载的时候会先调用分类中的方法,而且可以看到Animal中的run方法确实没有被覆盖,只是调用的时候发现分类中有不会再调用Animal的run方法而已。
class extention与category
上面知道了category,我们再来看看class extention,class extention算是一种特殊的分类(匿名分类),那么我们可以思考平时在 .m 文件的匿名分类中写的私有属性、方法等在加载的时候会不会和分类一样呢?我们来验证一下,在Animal的 .m 文件里添加属性 height 和方法 test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @interface Animal () @property (nonatomic, assign) int height; - (void)test; @end
@implementation Animal - (void)animal { }
- (void)test { } @end
|
用clang命令来编译 Animal:clang -rewrite-objc Animal.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
|
static struct _class_ro_t _OBJC_METACLASS_RO_$_Animal __attribute__ ((used, section ("__DATA,__objc_const"))) = { 1, sizeof(struct _class_t), sizeof(struct _class_t), (unsigned int)0, 0, "Animal", 0, 0, 0, 0, 0, };
static struct _class_ro_t _OBJC_CLASS_RO_$_Animal __attribute__ ((used, section ("__DATA,__objc_const"))) = { 0, __OFFSETOFIVAR__(struct Animal, _height), sizeof(struct Animal_IMPL), (unsigned int)0, 0, "Animal", (const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_Animal, 0, (const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_Animal, 0, 0, };
static struct /*_method_list_t*/ { unsigned int entsize; unsigned int method_count; struct _objc_method method_list[4]; } _OBJC_$_INSTANCE_METHODS_Animal __attribute__ ((used, section ("__DATA,__objc_const"))) = { sizeof(_objc_method), 4, {{(struct objc_selector *)"animal", "v16@0:8", (void *)_I_Animal_animal}, {(struct objc_selector *)"test", "v16@0:8", (void *)_I_Animal_test}, {(struct objc_selector *)"height", "i16@0:8", (void *)_I_Animal_height}, {(struct objc_selector *)"setHeight:", "v20@0:8i16", (void *)_I_Animal_setHeight_}} };
|
编译的结果如上,可以看到匿名类别的编译结果并不是 category_t
的类型在 runtime 时加载的,而是直接在编译期间将相关的属性方法等加载到了类中,匿名分类声明的属性方法相当于在类的 .h 文件的声明。