首发于我的个人博客
从给分类添加属性说起
在详解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