首发于我的个人博客
从给分类添加属性说起
在详解iOS中分类Cateogry 一文中,我们提出一个问题,
Category能否添加成员变量?如果可以,如何给Category添加成员变量?
- 不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果,用关联对象技术
那这里就详细说明
添加属性,实际上都做了什么
首先我们要回忆一下,添加属性,实际上做了三件事
- 生成成员变量
- 生成set方法和get方法的声明
- 生成set方法和get方法的实现
eg:
定义一个 YZPerson 类,并定义age属性
| 1 | 
 | 
就相当于干了三件事
- 生成成员变量_age
- 生成set方法和get方法的声明
- 生成set方法和get方法的实现
 如下
| 1 | 
 | 
那在分类中添加属性怎么就不行?
先说结论
- 生成成员变量_age
- 会生成set方法和get方法的声明
- 不会生成set方法和get方法的实现
不会生成set方法和get方法的实现
定义一个分类 YZPerson+Ext.h,然后添加属性weight
| 1 | 
 | 
使用
| 1 | YZPerson *person = [[YZPerson alloc] init]; | 
会直接报错,
| 1 | iOS-关联对象[1009:10944] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', | 
从 reason: '-[YZPerson setWeight:]: unrecognized selector sent to instance 0x10182bd10' 可知,分类中添加属性,没有生成set方法和get方法的实现
会生成set方法和get方法的声明
| 1 | 
 | 
然后再调用
| 1 | YZPerson *person = [[YZPerson alloc] init]; | 
输出
| 1 | 2019-07-10 08:28:04.406972+0800 iOS-关联对象[1620:18520] person.age = 25 | 
进一步证明了,不会生成set方法和get方法的实现,但是会生成set方法和get方法的声明,因为如果没有生成set方法和get方法的声明,这个方法就不能调用。
我们还可以这样:在YZPerson+Ext.h文件中声明了weight,然后再YZPerson+Ext.m中写实现的时候,会有提示的
更加说明了是有声明的。
分类中不能直接定义成员变量
| 1 | 
 | 
会直接报错Instance variables may not be placed in categories,成员变量不能定义在分类中
源码角度证明
前面的文章详解iOS中分类Cateogry 中分析过源码,objc-runtime-new.h中分类结构体是这样的
| 1 | struct category_t { | 
可知,这个结构体中,没有数组存放成员变量,只有属性,协议等。
怎么来完善属性
有什么办法可以实现在分类中添加属性和在类中添加属性一样的效果么?答案是有的
方案一 用全局变量
分类YZPerson+Ext.m中定义全局变量 _weight
| 1 | 
 | 
使用时候
| 1 | YZPerson *person = [[YZPerson alloc] init]; | 
输出为
| 1 | iOS-关联对象[1983:23793] person.weight = 103 | 
看起来确实可以,然后实际上我们不能这么用,因为,全局变量是共享的,假设有两个 Person,第二个Person修改了weight属性,然后打印第一个Person.weight
| 1 | YZPerson *person = [[YZPerson alloc] init]; | 
输出为
| 1 | iOS-关联对象[1983:23793] person.weight = 103 | 
可知,修改了Person2.weight 会改变Person.weight的值,因为是全局变量的缘故。所以这种方法不行
方案二 用字典
既然前面方案不能用的原因是全局变量,共享一份,那我们是不是只要保证,一对一的关系,是不是就可以了呢?
定义 字典weights_ 以对象的地址值作为key来,weight的值作为value来存储和使用
| 1 | 
 | 
这样的话,使用起来,就不会因为不同对象而干扰了
结果如下
存在的问题
- 因为是全局的,存在内存泄露问题
- 线程安全问题,多个线程同时访问的话,有线程安全问题
- 代码太多,如果每次增加一个属性,都要写好多代码。不利于维护
关联对象方案
关联对象的使用
下面先简单说明关联对象的使用
动态添加
| 1 | objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) | 
- 参数一:id object: 给哪个对象添加属性,这里要给自己添加属性,用self。
- 参数二:void * == id key:key值,根据key获取关联对象的属性的值,在objc_getAssociatedObject中通过次key获得属性的值并返回。
- 参数三:id value: 关联的值,也就是set方法传入的值给属性去保存。
- 参数四:objc_AssociationPolicy policy: 策略,属性以什么形式保存。
| 1 | typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) { | 
整理成表格如下
| objc_AssociationPolicy | 对应的修饰符 | 
|---|---|
| OBJC_ASSOCIATION_ASSIGN | assign | 
| OBJC_ASSOCIATION_RETAIN_NONATOMIC | strong, nonatomic | 
| OBJC_ASSOCIATION_COPY_NONATOMIC | copy, nonatomic | 
| OBJC_ASSOCIATION_RETAIN | strong, atomic | 
| OBJC_ASSOCIATION_COPY | copy, atomic | 
eg: 我们在代码中使用了 OBJC_ASSOCIATION_RETAIN_NONATOMIC 就相当于使用了 nonatomic 和 strong 修饰符。
注意点
上面列表中,没有对应weak修饰的策略,
原因是object经过DISGUISE函数被转化为了disguised_ptr_t类型的disguised_object。
| 1 | disguised_ptr_t disguised_object = DISGUISE(object); | 
而weak修饰的属性,当没有拥有对象之后就会被销毁,并且指针置为nil,那么在对象销毁之后,虽然在map中仍然存在值object对应的AssociationsHashMap,但是因为object地址已经被置为nil,会造成坏地址访问而无法根据object对象的地址转化为disguised_object了,这段话可以再看完全文之后,再回来体会下。
取值
| 1 | objc_getAssociatedObject(id object, const void *key); | 
- 参数一:id object: 获取哪个对象里面的关联的属性。
- 参数二:void * == id key: 什么属性,与objc_setAssociatedObject中的key相对应,即通过key值取出value。
移除关联对象
| 1 | - (void)removeAssociatedObjects | 
具体应用
| 1 | 
 | 
使用的时候,正常使用,就可以了
| 1 | YZPerson *person = [[YZPerson alloc] init]; | 
输出
| 1 | iOS-关联对象[4266:52285] person.name = jack | 
使用起来就是这么简单
关联对象原理
四个核心对象
实现关联对象技术的核心对象有
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
- ObjcAssociation
源码解读
关联对象的源码在 Runtime源码中
objc_setAssociatedObject
查看objc-runtime.mm类,首先找到objc_setAssociatedObject函数,看一下其实现
| 1 | void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) { | 
_object_set_associative_reference
查看
| 1 | 
 | 
如图所示
_object_set_associative_reference函数内部我们可以找到我们上面说过的实现关联对象技术的四个核心对象。接下来我们来一个一个看其内部实现原理探寻他们之间的关系。
AssociationsManager
查看 AssociationsManager 我们知道AssociationsManager 内部有static AssociationsHashMap *_map;
| 1 | 
 | 
AssociationsHashMap
接下来看 AssociationsHashMap
上图中 AssociationsHashMap的源码我们发现AssociationsHashMap继承自unordered_map首先来看一下unordered_map内的源码
从unordered_map源码中我们可以看出 参数 _Key和_Tp 对应着map中的Key和Value,那么对照上面AssociationsHashMap的源码,可以发现_Key中传入的是unordered_map<disguised_ptr_t,_Tp中传入的值则为ObjectAssociationMap *。
然后 我们查看ObjectAssociationMap的源码,上图中ObjectAssociationMap已经标记出,我们可以知道ObjectAssociationMap中同样以key、Value的方式存储着ObjcAssociation。
ObjcAssociation
接着我们来到ObjcAssociation中,可以看到
| 1 | class ObjcAssociation { | 
从上面的代码中,我们发现ObjcAssociation存储着_policy和_value,而这两个值我们可以发现正是我们调用objc_setAssociatedObject函数传入的值,换句话说我们在调用objc_setAssociatedObject函数中传入value和policy这两个值最终是存储在ObjcAssociation中的。
现在我们已经对四个核心对象AssociationsManager、 AssociationsHashMap、 ObjectAssociationMap、ObjcAssociation之间的关系有了初步的了解,那么接下继续仔细阅读源码,看一下objc_setAssociatedObject函数中传入的四个参数分别放在哪个对象中充当什么作用
细读 _object_set_associative_reference
_object_set_associative_reference的代码中
| 1 | 
 | 
acquireValue内部实现 通过对策略的判断返回不同的值
| 1 | static id acquireValue(id value, uintptr_t policy) { | 
- 首先根据我们传入的value经过acquireValue函数处理返回了new_value。acquireValue函数内部其实是通过对策略的判断返回不同的值
| 1 | typedef uintptr_t disguised_ptr_t; | 
- 之后创建AssociationsManager manager,得到manager内部的AssociationsHashMap即associations。
 之后我们看到了我们传入的第一个参数object经过DISGUISE函数被转化为了disguised_ptr_t类型的disguised_object。
| 1 | typedef uintptr_t disguised_ptr_t; | 
- 之后被处理成new_value的value,和policy一起被存入了ObjcAssociation中。
 而ObjcAssociation对应我们传入的key被存入了ObjectAssociationMap中。disguised_object和ObjectAssociationMap则以key-value的形式对应存储在associations中也就是AssociationsHashMap中。
value为空
如果传入的value为空,那么就删除这个关联对象
| 1 | // 来到这里说明,value为空 | 
表格总结
用表格总结来展示这几个核心类的关系如下
小结
- 关联对象并不存储在被关联对象本身内存中,而是有一个全局统一的 AssociationsManager中
- 一个实例对象就对应一个ObjectAssociationMap,
- 而ObjectAssociationMap中存储着多个此实例对象的关联对象的key以及ObjcAssociation,
- ObjcAssociation中存储着关联对象的- value和- policy策略
objc_getAssociatedObject
objc_getAssociatedObject内部调用的是_object_get_associative_reference
| 1 | id objc_getAssociatedObject(id object, const void *key) { | 
_object_get_associative_reference函数
| 1 | id _object_get_associative_reference(id object, void *key) { | 
关键代码已经在上文中给了注释
objc_removeAssociatedObjects函数
objc_removeAssociatedObjects函数用来删除所有关联对象,内部调用了_object_remove_assocations
| 1 | void objc_removeAssociatedObjects(id object) | 
_object_remove_assocations
再来看看_object_remove_assocations
| 1 | void _object_remove_assocations(id object) { | 
代码中可以看出,接受一个object对象,然后遍历删除该对象所有的关联对象
总结
用表格总结来展示这几个核心类的关系如下
- 关联对象并不存储在被关联对象本身内存中,而是有一个全局统一的 AssociationsManager中
- 一个实例对象就对应一个ObjectAssociationMap,
- 而ObjectAssociationMap中存储着多个此实例对象的关联对象的key以及ObjcAssociation,
- ObjcAssociation中存储着关联对象的- value和- policy策略
- 删除的时候接收一个object对象,然后遍历删除该对象所有的关联对象
- 设置关联对象_object_set_associative_reference的是时候,如果传入的value为空就删除这个关联对象
本文参考资料:
本文相关代码github地址 github