本文为L_Ares个人写作,以任何形式转载请表明原文出处。
类的扩展作为一种对类的功能和属性进行扩充的方法,经常和分类经常一起被提及。关于分类,已经在前面的一节有过一个总结,清楚了分类的数据加载是分两种情况的。
- 懒加载的分类 : 懒加载的分类的数据是在编译期完成的,所以数据都在
objc_class
的bits
的class_ro_t
结构里面。加载是在主类进行实现的时候开始贴入主类的rwe
。 - 非懒加载的分类 : 要在运行时确定数据。通过
load_images
将数据加载进来,然后贴入主类的rwe
。
那么关于和分类进行对比的扩展,比如有人会说分类和扩展的差别是扩展添加的都是私有的属性、方法、变量,分类添加属性会报错等等,这些都是本节的重点,一个是探索类的扩展,一个是探索关联对象。
准备 :
objc4-781源码请自行配置或下载。
一、类扩展
类扩展英文名是Extention,顾名思义就是对类进行扩展,添加一些属性,变量,方法等,又被称为匿名分类。
那么先在objc4-781源码中创建一个类JDPerson
,然后创建它的扩展。
1. 添加扩展的方式
扩展可以有两种方式。一种是直接在.m
文件中进行扩展,另外一种是创建扩展的文件。
-
.m
文件中进行扩展 :
然后我们可以在这里添加一些私有的属性或者方法。
- 创建分类
.h
文件 :
2. 扩展的探索
在之前的章节中,探索了_objc_init
中的map_images
,发现在read_images
的时候可以找到分类相关的加载源码,有协议的加载源码,但是没有发现有处理扩展的源码。
那么扩展的数据是如何加载进来的呢?
扩展的数据是在编译期就和类的数据一起被编译了。
那么我们有两种方式进行验证。
2.1 通过clang来查看编译的文件
先把创建的扩展.h
文件import
到JDPerson.m
中。
然后commond + B
进行编译。
打开终端(terminal)
,进入到objc4-781源码
所在文件夹,进入到你的main.m
所在的文件夹下,然后输入
clang -rewrite-objc JDPerson.m -o JDPerson.cpp
在当前文件夹下生成编译后的JDPerson.o
文件,打开之后找到在扩展里面定义的属性。
可以找打扩展里面增加的属性。
再看method_list
。
也可以找到定义的方法。这就证明扩展的数据是在编译期就加入到了类里面,直接作为了类的一部分。
2.2 通过ro
判定
进入objc4-781源码
,找到镜像映射map_images
,进入_read_images
,或者全搜索_read_images
,总之进入_read_images
的实现。找到这里。
代码我放在这里。
//自己写的代码。获得cls的ro数据
const class_ro_t *ro = (const class_ro_t *)cls->data();
//获取ro里面的名字
const char *cname = ro->name;
//用char存储我们自己定义的类的名字
const char *oname = "JDPerson";
//如果ro里面的名字和我们定义的类的名字一样且存在于ro里面,打印一下类名
if (cname && (strcmp(cname, oname) == 0)) {
//这里挂上断点
printf("_getObjc2ClassList 类名 : %s - %p\n,",cname,cls);
}
思路就是 :
如果这个时候可以在JDPerson
这个类的ro
里面找到在扩展里面添加的方法和属性的getter
和setter
方法,那么就证明扩展里面的方法和属性在_read_images
之前就已经存在了,只是从类的数据里载入,在dyld
链接动态库之前就存在,那就证明是在更早的编译期就存在的。
运行之后的结果是 :
如果还觉得这一个方法不能证明,那么可以看看有没有属性的getter
和setter
方法,那就需要用lldb
来查看。
先看ivars
里面,ivars
里面存储的是类的属性数据,看看类和类扩展的属性是不是都在这里面存在了。
再看method_list_t
是不是也有扩展里面的方法,还有属性的getter
和setter
方法。
的确是都实现了。可以仔细看一下图,或者自己做一下。
3. 结论
- 类扩展的方法、属性等数据,在编译期,会作为类本身的一部分,和类一起编译。
- 你可以把类扩展理解为一种声明,因为类扩展的数据加载是依赖类的加载,没有
.m
实现文件,只有.h
声明文件。
这也就理解为什么分类“不能”添加属性,而类扩展是可以添加属性的,因为类扩展在编译期就已经将数据放入ro
,分类却要在运行时才可以贴数据进去,而ro
没有操作能力,无法直接添加分类给主类的属性,分类的属性自然不能贴进ro
。
二、关联对象
分类和扩展经常拿来被比较,这是大家熟知的,一般的情况下,都会认为分类是不能添加属性的,因为它处在运行时的情况下是无法对ro
进行写入操作的,这是不被允许的,但是运行时的特点就是不确定性,关联对象就是对于分类不能添加属性而出现的不确定性。
关于关联对象,在objc4-781
中会有两个runtime
的api
体现。
-
objc_setAssociatedObject
: 设置关联对象 -
objc_getAssociatedObject
: 获取关联对象
先创建一个分类,然后再对它们两个进行认识和探索。
创建JDPerson
的分类JDPerson+Category
,随意在其中添加一个属性name
。
如图,创建完成cate_name
这个属性之后,按照之前探索的分类的数据加载,它会在rwe
的prop_list_t
中出现一个cate_name
变量,但是不会生成相应的set
和get
方法。
所以需要在JDPerson+JD.m
中利用关联属性自己实现cate_name
的setter
和getter
。如图 :
1. objc_setAssociatedObject
先从设置关联属性入手。
看一下它的源码。
先说一下苹果为什么这么喜欢这种接口模式。个人感觉最重要的一点是防止hook
,也就是不想让开发者调用私有的api
。这样会方便设计者在底层进行修改的时候不会影响上层调用者的代码,因为对外接口不会发生改变,改变的只有底层实现。
那么继续进入。先看这个掉用的函数get()
。
这里就不多说了,进入之后上面有注释的。可以自行翻译。
进入重点的SetAssocHook
。
_object_set_associative_reference
这里会有真正的代码实现。对于设置关联属性,重点一定是关联属性是怎么存储的。
先看一下源码总体分为4个大块。
先看核心的存储区域。
1 :
AssociationsManager manager
: 关联对象的管理类。
这个管理类不是唯一的,在外部是可以创建多个的,但是里面的实现有加锁的操作,原因是为了防止多线程操作造成重复创建。-
2 :
AssociationsHashMap &associations(manager.get())
:
这里的AssociationsHashMap
是全局唯一的关联关系哈希表。这里看清楚是manager.get()
,是管理者调用的get()
,所以进去看管理者。
3 : 判断我们传入的关联值是否为
nil
,也就是我们有没有给key
传了一个value
是nil
。毕竟这是哈希表。
这里要搞清楚,传入cate_name
不是说把这几个英文字母传进来了,cate_name
是我们对它的赋值,cate_name
代表的是一个地址,地址里面存着我们给cate_name
这个属性赋的值。
如果传入的不是
cate_name
属性的值,而是nil
,我们走else
,这里面下面再单独说。如果传入的不是
nil
。走if
。-
4 : 这里就是
if
的流程。
- (1). 根据传进来的
object
(也就是我创建的JDPerson对象
)去到系统表里面找,有没有它这张表。 - (2).下面是
try_emplace
的源码,主要就是一个找JDPerson对象
这张表的思路——有就直接返回这张表,没有就创建以后再返回,第二个参数就是说我都找到manager
管理的总表(关联对象总表)的底部了,当bool
值理解吧,因为用lldb
调试可以看到,现在先不说,下面会直接上完整的探索流程图。- 如果是
true
,那就是找到底部都没找到,证明没有JDPerson对象
这张表。 - 如果是
false
,就说明我没找到底部,就找到了JDPerson对象
这张表。
- 如果是
- (3). 如果没有
JDPerson的对象
这张表,那么就把JDPerson对象
设置成有关联了,然后会被存入manager
管理的hashmap
,也就是那张关联对象的总表。下图是源码。
- (4). 现在无论如何都会有
JDPerson
这个bucket
存储在关联对象总表里面了,所以开始对比key = name
,也就是我自己定义的键有没有对应的关联属性值。- 如果有,就用新值替换掉旧值。
-
如果没有就插入新值。
- (1). 根据传进来的
-
5 : 这里是
else
的流程。
如果传入的value
是nil
,也就是说对name
在JDPerson
的bucket
中置空。
1.1 举例
-
流程 : 在
main.m
中引入JDPerson
分类的头文件。然后设置cate_name
的值,挂上断点。并且在分类中设置关联对象的set
里面也给objc_setAssociatedObject
挂上断点。然后给_object_set_associative_reference
挂上断点,看看是不是这么进来的。
运行之后可以发现的确是这样进入的,也就是说我们找到的源码是没有问题的。
-
变量 :然后看一下
disguised
、association
、manager
和associations
都是什么。把断点挂在if (value)
这里。lldb
看一下它们。
-
存在
value
,所以走if
里面 :看一下refs_result
是什么。
-
看一下
try_emplace
是怎么找JDPerson
然后返回表的 :进入try_emplace
,调用了LookupBucketFor
,进入它发现有两个。
下面的那个LookupBucketFor
是一个重载,所以它没有const
修饰。
看一下我们调用的是哪个 :段点分别在上面和下面的
LookupBucketFor
都挂上。重新执行项目,会发现try_emplace
是调用了下面的LookupBucketFor
里面。但是下面的LookupBucketFor
还是调用了上面的LookupBucketFor
的实现。存疑的可以走一下断点就会发现跳到上面的LookupBucketFor
。
-
看一下上面的
LookupBucketFor
的实现 :
-
看一下
try_emplace
中的插入新值 :断点挂在try_emplace
中的InsertIntoBucket
上。因为找不到,所以会插入新的值。给JDPerson
一张新的表用来存储关联属性。
-
try_emplace
探索完成,回到外面,看if (refs_result.second)
:因为没有找到JDPerson
这张表,所以会进入if
,然后给JDPerson
设置关联属性。setHasAssociatedObjects()
来完成设置。
-
重新再拿总表 :
refs
现在就有值了。
把关联对象的
key
也就是name
和关联对象的值value
还有关联策略policy
放入JDPerson
的关联属性表 :key
就是hash
的key
,hash
中的value
是{policy, value}
2. objc_getAssociatedObject
看完设置关联对象就知道了,设置关联对象是一个存储的过程,那么get
也就很明显是一个读取的过程。
进入源码 :
接着进 :
图里有注释,我直接说步骤 :
- 1 : 创建
AssociationsManager
管理类的对象。 - 2 : 获取全局唯一的关联对象哈希Map :
AssociationsHashMap
。 - 3 : 用总表的迭代器递归查找被操作的类在不在关联对象总表里面。
- 4 : 找到之后,获取对象的关联属性表
ObjectAssociationMap
,也就是key : {policy ,value}
。 - 5 : 然后根据
key
找出对应{policy ,value}
。 - 6 : 取出
value
并且返回。
获取关联对象的关联属性的值的例子我就不写了。可以自己尝试。下面直接总结。