探索底层原理,积累从点滴做起。大家好,我是Mars。
往期回顾
iOS底层原理探索—OC对象的本质
iOS底层原理探索—class的本质
iOS底层原理探索—KVO的本质
iOS底层原理探索— KVC的本质
iOS底层原理探索— Category的本质(一)
iOS底层原理探索— Category的本质(二)
今天继续带领大家探索iOS之关联对象的本质。
我们在iOS底层原理探索— Category的本质(一)中提到过在iOS中分类不能直接添加成员变量。在分类中添加的属性并不会帮助我们自动生成成员变量,只会生成set
、get
方法的声明,需要我们自己去实现。,下面我们验证一下:
我们创建一个本类Person
,在类中声明int
类型的age
属性,然后创建一个Person
的分类,并在分类中声明int
类型的weight
属性,然后在main.m
文件中导入分类的头文件,创建一个Person
的对象,给age
和weight
属性赋值,并且打印两个属性值:
通过编译运行,我们发现程序崩溃,崩溃信息就是就找不到Person
的setWeight
方法。而且程序在分类中报出两个警告,找不到Person
分类的setWeight
方法和weight
方法,从而验证我们上面的结论。
这时可能会有人提出,既然系统没有为我们生成set
、get
方法的实现,那我们手动实现set
、get
方法的实现不就可以了吗?我们继续验证一下这个方法可不可行:
我们在分类的.m
文件中声明全局变量_weight
,并实现set
、get
方法,然后运行程序:
通过运行我们发现确实完成了赋值并打印出结果,但是这样有一个问题不知道大家有没有发现,我们声明的全局变量_weight
,无论对象创建与销毁,只要程序在运行_weight
变量就存在。如果我们再创建一个新的Person
对象,给weight
赋不同的值,那么之前我们创建的person
对象的weight
属性的值也会改变:
通过打印看到,我们新创建一个
Person
对象student
,赋值后打印之前创建的person
对象属性值发现,person
对象的weight
属性的值已经改变了。
手动添加set
、get
方法实现的办法失败了,那我们可不可以通过runtime
的一些方式间接实现添加成员变量的效果呢?答案是可以的,下面就给大家介绍关联对象的方法。
关联对象
在OC中,runtime
提供了动态添加属性和获得属性的API:objc_setAssociatedObject
、objc_getAssociatedObject
,下面我们用代码来演示一下具体使用方法:
通过关联对象我们很容易就实现了给分类添加成员变量,我们来分析一下具体的使用。
添加关联对象
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
/* 参数解读
* object : 给哪个对象添加属性。这里要给自己添加属性,用self。
* key : 属性名,根据key获取关联对象的属性的值。传入一个指针即可。在objc_getAssociatedObject中通过次key获得属性的值并返回。
* value : 关联的值,也就是通过set方法传入的值给属性。
* policy : 策略,属性以什么形式保存。
*/
其中参数policy
对应的是枚举值:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
// 指定一个弱引用相关联的对象
OBJC_ASSOCIATION_ASSIGN = 0,
// 指定相关对象的强引用,非原子性
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
// 指定相关的对象被复制,非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
// 指定相关对象的强引用,原子性
OBJC_ASSOCIATION_RETAIN = 01401,
// 指定相关的对象被复制,原子性
OBJC_ASSOCIATION_COPY = 01403
};
分别对应的修饰符为:
获得关联对象
objc_getAssociatedObject(id object, const void *key);
/* 参数解读
* object : 获取哪个对象里面的关联的属性。
* key : 属性,与objc_setAssociatedObject中的key相对应,即通过key值取出value。
移除所有关联对象
- (void)removeAssociatedObjects
{
// 移除所有关联对象
objc_removeAssociatedObjects(self);
}
至此我们就了解了关联对象以及如何使用关联对象。下面我们从底层源码来进一步了解关联对象的实现原理。
关联对象实现原理
添加关联对象
我们在runtime
源码中找到objc_setAssociatedObject
函数:
函数内部调用
_object_set_associative_reference
函数,我们继续追踪_object_set_associative_reference
函数的实现:
通过红框标注我们找到了4个对象,其实这4个对象就是实现关联对象技术的核心对象:
AssociationsManager
AssociationsHashMap
ObjectAssociationMap
ObjcAssociation
我们分别进入这4个对象内部查看它们之间的关系:
AssociationsManager
通过AssociationsManager
内部源码发现,其内部有一个AssociationsHashMap
对象:
AssociationsHashMap
我们继续查看AssociationsHashMap
内部的源码。发现AssociationsHashMap
继承自unordered_map
(均由红框标注):
我们看到里面还包括黄框标注的
ObjectAssociationMap
。我们先继续查看unordered_map
内部源码:从unordered_map
源码中我们可以看出_Key
和_Tp
也就是前两个参数对应着map中的Key
和Value
,那么对照上面AssociationsHashMap
内源码发现_Key
中传入的是disguised_ptr_t
,_Tp
传入的值则为ObjectAssociationMap*
(AssociationsHashMap图中用红线标注)。
同样我们来到黄框标注ObjectAssociationMap
中,发现ObjectAssociationMap
中同样也是奖ObjcAssociation
以参数形式传入,并以key
、Value
的方式存储着ObjcAssociation
(AssociationsHashMap图中用黄线标注)。
我们继续来到ObjcAssociation
源码中查看:
我们发现
ObjcAssociation
存储着_policy
、_value
,而这两个值我们发现正是我们调用添加关联对象objc_setAssociatedObject
函数传入的值。也就是说我们在调用objc_setAssociatedObject
函数中传入的value
和policy
这两个值最终是存储在ObjcAssociation
中的。
现在我们已经对AssociationsManager
、 AssociationsHashMap
、 ObjectAssociationMap
、ObjcAssociation
四个对象之间的关系有了简单的认识,那么接下来我们回到objc_setAssociatedObject
源码,具体查看一下objc_setAssociatedObject
函数中传入的四个参数究竟做了哪些操作:
图中1标注,首先根据传入的
value
经过acquireValue
函数处理获取new_value
。acquireValue
函数内部其实是通过对策略的判断返回不同的值:图中2标注,创建
AssociationsManager
类型的manager
,拿到manager
内部的AssociationsHashMap
,即associations
。图中3标注,传入的
object
参数经过DISGUISE
函数被转化为了disguised_ptr_t
类型的disguised_object
。其实
DISGUISE
函数内部只是对object
做了位运算:图中4标注,我们看到被处理成
new_value
的value
,和policy
一起被存入了ObjcAssociation
中。而ObjcAssociation
对应我们传入的key
被存入了ObjectAssociationMap
中。disguised_object
和ObjectAssociationMap
则以key-value
的形式对应存储在associations
中也就是AssociationsHashMap
中。
如果我们value
传入nil
的话则会执行图中5标注的代码。
我们将以上分析用一张示意图来展示:
分析到这里我们可以总结出:一个实例对象就对应一个ObjectAssociationMap
,而ObjectAssociationMap
里面存储着多个此实例对象的关联对象的key
以及ObjcAssociation
,ObjcAssociation
中存储着关联对象的value
和policy
。
我们也可以得出结论:关联对象并不是放在了原来的对象里面,而是自己维护了一个全局的map用来存放每一个对象及其对应关联属性。
获得关联对象
我们进入objc_getAssociatedObject
函数内部查看,发现其内部调用的是_object_get_associative_reference
函数:
进入
_object_get_associative_reference
函数查看:分析源码可知,先拿到
AssociationsManager
中的AssociationsHashMap
,在里面查找disguised_object
,即我们传入的object
参数,如果存在就取出ObjectAssociationMap
,并在ObjectAssociationMap
中查找key
对应的value
,即ObjcAssociation
,然后再ObjcAssociation
中取出value
和policy
,最后返回value
。
移除关联对象
通过调用objc_removeAssociatedObjects
用来删除所有的关联对象。其内部调用的是_object_remove_assocations
函数。我们直接查看_object_remove_assocations
函数内部源码:
大概逻辑就是遍历所有
ObjectAssociationMap
,然后进行delete
删除操作。
当然,即使我们没有调用移除关联对象的方法,在对象销毁的时候,对应的关联对象也会被销毁。因为在dealloc
执行的时候,底层会检查是否有关联对象。
至此我们就完成了关联对象的底层探索,如有疑问欢迎大家留言。