今天我们再看一下Category
的底层原理。
先看一下Category
的简单使用,首先新增一个类的Category
,然后添加需要的函数,然后在使用的文件中导入就可以直接使用了。代码如下:
1 | @interface FYPerson : NSObject |
类别使用就是这么简单。
那么类别的本质是什么呢?类的方法是存储在什么地方呢?
第一篇类的本质已经讲过了,运行时中,类对象是有一份,方法都存储在类对象结构体fy_objc_class
中的class_data_bits_t->data()->method_list_t
中的,那么类别方法也是存储在method_list_t
和取元类对象的method_list_t
中的。编译的时候类别编译成结构体_category_t
,然后runtime
在运行时动态将方法添加到method_list_t
中。运行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc FYPerson+test.m -o FYPerson+test.cpp
进入到FYPerson+test.cpp
内部查看编译之后的代码
1 | struct _category_t { |
存储在_category_t
中的数据是什么时间加载到FYPerson
的class_data_bits_t.data
呢?我们探究一下,打开源码下载打开工程阅读源码找到objc-os.mm
,通过查找函数运行顺序得到_objec_init->map_images->map_images_noljock->_read_images->remethodizeClass(cls)->attachCategories(cls, cats, true /*flush caches*/)
,最终进入到attachCategories
关键函数内部:
1 | // Attach method lists and properties and protocols from categories to a class. |
attachCategories
是将所有的分类方法和协议,属性倒序添加到类中,具体添加的优先级是怎么操作的?进入到rw->protocols.attachLists
内部:
1 | void attachLists(List* const * addedLists, uint32_t addedCount) { |
可以看出来:
- 首先通过
runtime
加载某个类的所有Category数据 - 把所有Category的方法,属性,协议数据合并到一个大数组中,后面参与编译的数组会出现在数组前边
- 将合并后的分类数组(方法,属性,协议)插入到类原来的数据的前面。
具体的编译顺序是project文件中->Build Phases->Complile Sources的顺序。
调用顺序
+load加载顺序
每个类和分类都会加载的时候调用+load
方法,具体是怎么调用呢?我们查看源码_objc_init->load_images->call_load_methods
1 | void call_load_methods(void) |
类+load
在Category+load
前边执行,当类的+load
执行完毕然后再去执行Category+load
,而且只有一次。
当class有子类的时候加载顺序呢?其实所有类都是基于NSObject
,那么我们假设按照编译顺序加载Class+load
,就有一个问题是父类+load执行的操作岂不是在子类执行的时候还没有执行吗?这个假设明显不对,基类+load
中的操作是第一个执行的,其他子类是按照superclass->class->sonclass
的顺序执行的。
查看源码_objc_init->load_images->prepare_load_methods((const headerType *)mh)->schedule_class_load
在objc-runtime-new.mm
2856行
1 | /*********************************************************************** |
可以了解到该函数递归调用自己,直到+load
方法已经调用过为止,所以不管编译顺序是高低,+load
的加载顺序始终是NSObject->FYPrson->FYStudent
。多个类平行关系的话,按照编译顺序加载。
下边是稍微复杂点的类关系:
1 |
|
编译顺序是
1 | Person |
那么他们+load
的加载顺序是:
1 |
|
看着不是很明白的 可以再看一下刚才的schedule_class_load
函数。
加载成功之后,是按照objc_msgsend()
流程发送的吗?我们进入到call_class_loads
内部
1 | static void call_class_loads(void) |
可以找到(*load_method)(cls, SEL_load);
该函数,该函数是直接使用IMP
执行的,IMP
就是函数地址,可以直接访问函数而不用消息的转发流程。
+initialize调用
- +initialize方法会在类第一次接收到消息时调用
- 先调用父类的+initialize,再调用子类的+initialize
- 先初始化父类,再初始化子类,每个类只会初始化1次
objc
源码解读过程objc-msg-arm64.x->objc_msgSend->objc->runtime-new->class_getinstanceMethod->lookUpImpOrNil->lookUpImpOrForward->_clas_initialize->callInitialize->objc_msgSend(cls,SEL_Initialize)
在runtime-new.h
4819行
1 | Method class_getInstanceMethod(Class cls, SEL sel) |
根据lookUpImpOrNil
查看4916行
1 | IMP lookUpImpOrForward(Class cls, SEL sel, id inst, |
当第一次收到消息,cls没有初始化,则调用_class_initialize
进行初始化
我们进入到_class_initialize
内部objc-initialize.mm
484行
1 | void _class_initialize(Class cls) |
可以看出来,和+load
方法一样,先父类后子类。然后赋值reallyInitialize = YES;
,后边使用try
主动调用callInitialize(cls);
,来到callInitialize(cls);
内部:
1 | void callInitialize(Class cls) |
可以看到最终还是使用((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize)
主动调用了该函数。
区别
+initialize
和+load
的很大区别是,+initialize
是通过objc_msgSend
进行调用的,所以有以下特点
如果子类没有实现+initialize
,会调用父类的+initialize
(所以父类的+initialize
可能会被调用多次)
如果分类实现了+initialize
,就覆盖类本身的+initialize
调用
用伪代码实现以下思路:
1 | if(class 没有初始化){ |
至于子类没有实现的话是直接调用父类的initialize
,是使用objc-msgsend
的原因。
验证
1 | @interface FYPerson : NSObject |
总结
+load
是根据函数地址直接调用,initialize
是通过objc_msgSend
调用+load
是runtime加载类、分类时候调用(只会调用一次)initialize
是第一次接受消息的时候调用,每个类只会调用一次(子类没实现,父类可能被调用多次)+load
调用优先于initialize
,子类调用+load
之前会调用父类的+load
,再调用分类的+load
,分类之间先编译,先调用。initialize
先初始化父类,再初始化子类(可能最终调用父类的initialize
)
关联对象本质
关联对象的本质-结构体
继承NSObject
是可以可以直接使用@property (nonatomic,assign) int age;
,但是在Category
中会报错,那么怎么实现和继承基类一样的效果呢?
我们查看Category
结构体
1 | struct _category_t { |
其中const struct _prop_list_t *properties;
是存储属性的,但是缺少成员变量,而我们也不能主动在_category_t
插入ivar
,那么我们可以使用objc_setAssociatedObject
将属性的值存储全局的AssociationsHashMap
中,使用的时候objc_getAssociatedObject(id object, const void *key)
,不使用的时候删除使用objc_removeAssociatedObjects
删除。
我们进入到objc_setAssociatedObject
内部,objc-references.mm
275行
1 | void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) { |
通过该函数我们了解到
- 关联对象并不是存储在关联对象的本身内存中
- 关联对象是存储在全局统一的
AssociationsManager
管理的AssociationsHashMap
中 - 传入value =nil,会移除该关联对线
AssociationsManager
其实是管理了已key为id object
对应的AssociationsHashMap
,AssociationsHashMap
存储了key
对应的ObjcAssociation
,ObjcAssociation
是存储了value
和policy
,ObjcAssociation
的数据结构如下:
1 | class ObjcAssociation { |
具体抽象关系见下图
1 | AssociationsManager --> AssociationsHashMap --> ObjectAssociationMap |
简单来讲就是一个全局变量保存了以class
为key
对应的AssociationsHashMap
,这个AssociationsHashMap
存储了一个key
对应的ObjectAssociation
,ObjectAssociation
包含了value
和_policy
。通过2层map保存了数据。
关联对象的使用
objc_setAssociatedObject | obj,key,value,policy |
---|---|
objc_getAssociatedObject | 根据 obj 和 key获取值 |
void objc_removeAssociatedObjects(id object) | 根据obj 删除关联函数 |
objc_AssociationPolicy
的类型:
OBJC_ASSOCIATION_ASSIGN | weak 引用 |
---|---|
OBJC_ASSOCIATION_RETAIN_NONATOMIC | 非原子强引用 |
OBJC_ASSOCIATION_COPY_NONATOMIC | 非原子相当于copy |
OBJC_ASSOCIATION_RETAIN | 强引用 |
OBJC_ASSOCIATION_COPY | 原子操作,相当于copy |
代码示例
1 | @interface NSObject (test) |
这段代码我们实现了给基类添加一个成员变量name
,然后又成功取出了值,标示我们做新增的保存成员变量的值是对的。
总结
- Category
+load
在冷启动时候执行,执行顺序和编译顺序成弱相关,先父类,后子类,而且每个类执行一次,执行是直接调用函数地址。 - Category
+initialize
在第一次接受消息执行,先父类,后子类,子类没实现,会调用父类,利用objc-msgsend
机制调用。 - Category 可以利用
Associative
添加和读取属性的值
资料下载
- 学习资料下载
- demo code
runtime可运行的源码
本文章之所以图片比较少,我觉得还是跟着代码敲一遍,印象比较深刻。
最怕一生碌碌无为,还安慰自己平凡可贵。
广告时间