方便记忆:
- 调用顺序:先调用类的load方法,再调用分类的load方法
- 为分类添加属性:
- 实现:RunTime 为 Category 动态关联对象,objc_setAssociatedObject方法,内部调用_object_set_associative_reference函数
- 原理:关联对象并不是放在了原来的对象里面,而是自己维护了一个全局的map用来存放每一个对象及其对应关联属性表格
目录结构
我们之前讲到过实例对象的isa指针指向类对象,类对象的isa指针指向元类对象,当p调用run方法时,类对象的isa指针找到类对象的isa指针,然后在类对象中查找对象方法,如果没有找到,就通过类对象的superclass指针找到父类对象,接着去寻找run方法。
Category 的底层实现
将Preson+Test.m文件转化为c++文件,查看其中的编译过程
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Test.m
在分类转化为c++文件中可以看出_category_t结构体中,存放着类名,对象方法列表,
类方法列表,协议列表,以及属性列表
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; };
紧接着,我们可以看到_method_list_t类型的结构体
static struct /*_method_list_t*/ { unsigned int entsize; // sizeof(struct _objc_method) unsigned int method_count; struct _objc_method method_list[3]; } _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = { sizeof(_objc_method), 3, {{(struct objc_selector *)"test", "v16@0:8", (void *)_I_Person_Test_test}, {(struct objc_selector *)"setAge:", "v20@0:8i16", (void *)_I_Person_Test_setAge_}, {(struct objc_selector *)"age", "i16@0:8", (void *)_I_Person_Test_age}} };
从中我们发现这个结构体_OBJC_CATEGORY_INSTANCE_METHODS_Preson_Test从名称可以看出是INSTANCE_METHODS对象方法,并且一一对应为上面结构体内赋值。我们可以看到结构体中存储了方法占用的内存,方法数量,以及方法列表。并且找到分类中我们实现对应的对象方法,test , setAge, age三个方法
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_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = { sizeof(_objc_method), 1, {{(struct objc_selector *)"abc", "v16@0:8", (void *)_C_Person_Test_abc}} };
同上面对象方法列表一样,这个我们可以看出是类方法列表结构体 _OBJC_CATEGORY_CLASS_METHODS_Preson_Test,同对象方法结构体相同,同样可以看到我们实现的类方法,abc
接下来是协议方法列表
static struct /*_method_list_t*/ { unsigned int entsize; // sizeof(struct _objc_method) unsigned int method_count; struct _objc_method method_list[1]; } _OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying __attribute__ ((used, section ("__DATA,__objc_const"))) = { sizeof(_objc_method), 1, {{(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", 0}} }; struct _protocol_t _OBJC_PROTOCOL_NSCopying __attribute__ ((used)) = { 0, "NSCopying", 0, (const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying, 0, 0, 0, 0, sizeof(_protocol_t), 0, (const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCopying }; struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_NSCopying = &_OBJC_PROTOCOL_NSCopying; static struct /*_protocol_list_t*/ { long protocol_count; // Note, this is 32/64 bit struct _protocol_t *super_protocols[1]; } _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = { 1, &_OBJC_PROTOCOL_NSCopying };
通过上述源码可以看到先将协议方法通过_method_list_t结构体存储,之后通过_protocol_t结构体存储在_OBJC_CATEGORY_PROTOCOLS_Preson_Test中同_protocol_list_t结构体一一对应,分别为protocol_count 协议数量以及存储了协议方法的_protocol_t结构体
最后我们可以看到属性列表
static struct /*_prop_list_t*/ { unsigned int entsize; // sizeof(struct _prop_t) unsigned int count_of_properties; struct _prop_t prop_list[1]; } _OBJC_$_PROP_LIST_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = { sizeof(_prop_t), 1, {{"age","Ti,N"}} };
属性列表结构体_OBJC_PROP_LIST_Preson_Test同_prop_list_t结构体对应,存储属性的占用空间,属性属性数量,以及属性列表,可以看到我们自己写的age属性。
最后我们可以看到定义了_OBJC_CATEGORY_Preson_Test结构体,并且将我们上面着重分析的结构体一一赋值,我们通过两张图片对照一下。
上下两张图一一对应,并且我们看到定义_class_t类型的OBJC_CLASS_Preson结构体,最后将_OBJC_CATEGORY_Preson_Test的cls指针指向OBJC_CLASS_Preson结构体地址。我们这里可以看出,cls指针指向的应该是分类的主类类对象的地址。
通过以上分析我们发现。分类源码中确实是将我们定义的对象方法,类方法,属性等都存放在catagory_t结构体中。通过 runtime 源码查看catagory_t存储的方法,属性,协议等我们得知,分类的实现原理是将category中的方法,属性,协议数据放在category_t结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。
Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么我们就无法再程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量。
load 和 initialize
load方法会在程序启动就会调用,当装载类信息的时候就会调用。 调用顺序看一下源代码。
通过源码我们发现是优先调用类的load方法,之后调用分类的load方法
我们通过代码验证一下: 我们添加Student继承Presen类,并添加Student+Test分类,分别重写只+load方法,其他什么都不做通过打印发现
确实是优先调用类的load方法之后调用分类的load方法,不过调用类的load方法之前会保证其父类已经调用过load方法。 之后我们为Preson、Student 、Student+Test 添加initialize方法。
我们知道当类第一次接收到消息时,就会调用initialize,相当于第一次使用类的时候就会调用initialize方法。调用子类的initialize之前,会先保证调用父类的initialize方法。如果之前已经调用过initialize,就不会再调用initialize方法了。当分类重写initialize方法时会先调用分类的方法。但是load方法并不会被覆盖,首先我们来看一下initialize的源码。
上图中我们发现,initialize是通过消息发送机制调用的,消息发送机制通过isa指针找到对应的方法与实现,因此先找到分类方法中的实现,会优先调用分类方法中的实现