1. Category的底层结构
通过runtime动态的将分类的方法合并到类对象或元类对象中,程序编译的时:category会生成,category中的信息会存储在struct _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;//属性
};
- 从结构体可以知道,有属性列表,所以分类可以声明属性,但是分类只会生成该属性对应的get和set的声明,没有去实现该方法。
- 结构体没有成员变量列表,所以不能声明成员变量。
- 分类并不会改变原有类的内存分布的情况,它是在运行期间决定的,此时内存的分布已经确定,若此时再添加实例会改变内存的分布情况,这对编译性语言是灾难,是不允许的。
category的源码阅读轨迹:
- objc-os.mm
- _objc_init
- map_images
- map_images_nolock
- objc-runtime-new.mm
- _read_images
- remethodizeClass
- attachCategories
- attachLists
- realloc、memmove、 memcpy
category的源码分析
① readimges 是读取模块的意思,参数有totalClass是所有的类的意思.
②.remethodizeClass是重新组织类的方法的意思
③.attachCategories是将分类重新规整,参数有两个:一个类名,一个是分类的数组,在内部有一个方法数组的二维数组,一个属性数组的二维数组,一个协议的二维数组,把所有Category的方法、属性、协议数据,合并到一个大数组中.(attachCategories)
④.attachLists有两个参数一个所有category的方法列表或属性列表或协议列表的数组和第二个参数传入的数组的count,realloc重新计算方法列表分配的内存大小,memmove移动原来的方法数据到末尾,memcpy分类的数据插入到原来数据的前面.
顺序的总体的概括:
- 通过Runtime加载某个类的所有Category数据.
- 把所有Category的方法、属性、协议数据,合并到一个大数组中
后面参与编译的Category数据,会在数组的前面.- 将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面.
category memmove到memcpy的过程流程图:
代码例子:
// 原来的类和分类看Demo,这里就不列举出来了
// 开始调用
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Person *person = [[Person alloc] init];
[person run];
[person test];
[person eat];
// 通过runtime动态将分类的方法合并到类对象、元类对象zhong
}
return 0;
}
通过运行结果可知,分类方法会覆盖原来类对象方法,并且最后参与编译的会在调用顺序最前面。(其实不是覆盖,只是寻找方法时的顺序.)
这里有2个关于category的问题
1.分类中能不能添加属性,为什么?
不能。类的内存布局在编译时期就已经确定了,category是运行时才加载的早已经确定了内存布局所以无法添加实例变量,如果添加实例变量就会破坏category的内部布局。
2.为什么说category是在运行时加载的?不能添加实例变量,那为什么能添加属性?
根据category_t的结构
①.category小括号里写的名字
②.要扩展的类对象,编译期间这个值是不会有的,在app被runtime加载时才会根据name对应到类对象
③.这个category所有的-方法
④.这个category所有的+方法
⑤.这个category实现的protocol,比较不常用在category里面实现协议,但是确实支持的
⑥.这个category所有的property,这也是category里面可以定义属性的原因,不过这个property不会@synthesize实例变量,一般有需求添加实例变量属性时会采用objc_setAssociatedObject和objc_getAssociatedObject方法绑定方法绑定,不过这种方法生成的与一个普通的实例变量完全是两码事。
2. +load方法和initialize方法
一. +load
+load方法会在runtime加载类、分类时调用,并且只调用一次.
- load方法的调用顺序
- 先调用类的+load.
- 按照编译先后顺序调用(先编译,先调用).
- 调用子类的+load之前会先调用父类的+load.
- 再调用分类的+load.
- 按照编译先后顺序调用(先编译,先调用)
- load方法为什么类和分类都调用,原因是load方法是根据方法地址直接调用.
代码佐证:
- objc4源码解读过程:objc-os.mm
- _objc_init
- load_images
- prepare_load_methods
schedule_class_load
add_class_to_loadable_list
add_category_to_loadable_list- call_load_methods
call_class_loads
call_category_loads
(*load_method)(cls, SEL_load)
+load方法是根据方法地址直接调用,并不是经过objc_msgSend函数调用。
二. +initialize方法
+initialize方法会在类第一次接收到消息时调用.(initialize应该是objc_msgSend实现的)
- initialize调用顺序:
- 先调用父类的+initialize,再调用子类的+initialize.
- (先初始化父类,再初始化子类,每个类只会初始化1次).
- 如果分类实现了+initialize,就覆盖类本身的+initialize调用.
- objc4源码解读过程
- objc-msg-arm64.s
objc_msgSend- objc-runtime-new.mm
class_getInstanceMethod
lookUpImpOrNil
lookUpImpOrForward
_class_initialize
callInitialize
objc_msgSend(cls, SEL_initialize)
调用的详情解析:
①.class_getInstanceMethod这个是找到对象方法的方法.
②.lookUpImporNil.
③.lookUpImporForward在查找方法的时候调用_class_initialize看类是否初始化,如果没有初始化就调用callInitialize初始化.
④._class_initialize 是一个递归来判断父类是否初始化.
调用顺序证明:
解析:
1.[Student alloc]会调用+initialize方法,因为他有父类Person,所以先调用Person的+initialize方法,又因为分类在前面,所以调用了Person(Test2)的+initialize方法。但是他自己本身没有实现+initialize方法,所以会去父类查找,然后分类方法在前面,所以调用了Person(Test2)的+initialize方法。
2.[Teacher alloc]会调用+initialize方法,因为他有父类Person,所以先调用Person的+initialize方法,但是前面已经初始化过了,所以跳过,调用自己的+initialize方法,但是因为他自己没有实现+initialize方法,所以调用父类的+initialize方法,又因为分类方法在前面,所以调用Person(Test) +initialize方法。
3.[Person alloc],因为前面已经初始化过了,所以不会再调+initialize方法,所以这里不打印。
+initialize和+load的很大区别是,+initialize是通过objc_msgSend进行调用的,所以有以下特点:
- 如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次).
- 如果分类实现了+initialize,就覆盖类本身的+initialize调用.
想了解更多iOS学习知识请联系:QQ(814299221)