1.Category底层结构
1.1 Category代码转成C++
在项目中新建一个Person的分类为Person+Test,里面添加一些属性和类方法以及对象方法,如下:
@interface Person (Test)
@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *name;
+ (void)test;
+ (void)test2;
- (void)test3;
@end
用命令行将OC的代码转成C++,先进入项目的文件夹,再用下面的命令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Test.m
回车后,目录下就会自动生成一个Person+Test.cpp的文件如下图:
将文件拖入工程中,但是不要让其参与编译:
查看其代码,如下图:
可知,底层将Person+Test这个类转成了 _category_t 结构体类型的 _OBJC_$_CATEGORY_Person_$_Test 变量,变量里面存放一些值。
1.2 Category的结构体了解
在cpp那个文件中查找_category_t,可以看到_category_t的定义如下:
其中结构体存储的信息如下:
- 哪个分类的名字
- 类
- 对象方法列表
- 类方法列表
- 协议方法列表
- 属性列表
在看,结构体是如何赋值的:
(1)ame = "Person"
(2)cls = 0 (这个我也不知道为什么)
(3)instance_methods = _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test,
查找_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test这个结构体如下:
可知,这个结构体主要存放了:方法的大小、方法的个数、_obect_method结构体类型的方法实现的数组。
_obect_method的结构体定义如下:
_obect_method这个结构体存储的信息主要是:方法名、方法类型、方法地址。
(4)class_methods = _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test,这个结构体存放的数据跟上一个原理一样。
(5)protocols = 0 ,因为我遵守协议,所以为0
(6)properties = _OBJC_$_PROP_LIST_Person_$_Test,如下图:
_prop_t 的结构体定义如下:
attributes是指属性类型。
通过以上分心,可以总结,Xcode编译的时候,都把分类对应的信息转成结构体存储起来。当开始加载类的时候,才会把分类的信息合并到类中。
1.3 Category的信息是如何合并到类信息中?
我们只能通过源码去了解,首先先从源码的_objc_init这个方法入手,_objc_init()方法是runtime被加载后第一个执行的方法。这个方法有如下几个函数:
其中:
environ_init()
方法是环境配置tls_init()
方法这里先不多做介绍,因为涉及的知识面比较广,后面的文章会给于详细解释static_init()
也非常有意思lock_init()
从名字可以看出是跟锁相关exception_init()
跟异常相关_dyld_objc_notify_register
是这个方法的主角,它会先调用 map_images 做解析和处理,把 Category 的实例方法、协议以及属性添加到类上,把 Category 的类方法和协议添加到类的 metaclass 上;接下来 load_images 中调用 call_load_methods 方法,遍历所有加载进来的 Class,按继承层级依次调用 Class 的 load 方法和其 Category 的 load 方法。
map_images的方法如下:
map_images_nolock()的方法如下:
....
_read_images()的方法如下:
找到该方法,往下找,会看到一个注释:// Discover categories.,如下图:
如果分类中有实例方法、属性或者协议方法,就会调用remethodizeClass()这个方法把这些信息与类对象合并:
attachCategories()方法的源码如下:
因为遍历的时候,i是从大到小的,所以,后编译的会先加入数组中。
attachLists的方法如下:
查看memmove 移动 和memcpy复制 的方法如下:
也就说分类的方法会放在类的方法的前面,也就导致了分类会覆盖父类的方法。分类是后编译先调用。如何查看编译的顺序呢?
1.4 load加载的顺序
查看load加载方法如下:
从源码可知:先调用类的load方法,再调用分类的load的方法。
那么类的load方法调用的顺序又是什么呢?查看源码如下:
从上图可知,调用_getObjc2NonlazyClassList这个方法把项目中的类加载出来,这个类加载的顺序是根据类编译的先后顺序来的。
查看schedule_class_load()方法的代码如下:
从上面的源码可知:把一个类加载到数组前,会用递归的方式把父类的方法加载进去。
那么load的方法为什么不会被分类或者子类的load方法覆盖呢?查看源码如下:
load_method_t的结构体定义如下:
从源码可知,调用load方法之前,是把load方法的IMP地址获取出来,直接调用,而不是通过object_msgSend的方法来调用的。object_msgSend的调用机制是先查找本类的方法列表(该方法列表:分类的方法会加载在类的方法的前面),找到对应的方法名,再取出对应的方法地址调用。当通过消息机制获取到的方法,可能是分类的方法实现而不是类本身的方法实现,但是直接通过load的IMP实现地址的话,就不会出现这个问题。
总结load调用顺序如下:
(1)先根据编译的先后顺序调用类的load方法,类的load方法会先调用父类的load方法,再调用子类的load方法
(2)类的load方法调用完后,在根据编译的先后顺序调用分类的load方法
1.4 initialize加载的顺序
+initialize方法会在类第一次接收到消息时调用。查看_class_initialize()的源码如下:
得出调用顺序:
- 先调用父类的+initialize,再调用子类的+initialize
- (先初始化父类,再初始化子类,每个类只会初始化1次)。
- 因为在加载分类的时候,会把分类的方法放在类的方法的前面,所以如果分类重写了initialize,那么会调用分类的initialize方法。
+initialize和+load的很大区别:
- +initialize是通过objc_msgSend进行调用的,而+load方法是通过load的函数指针调用的,所以有以下特点:
- 如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
- 如果分类实现了+initialize,就覆盖类本身的+initialize调用