俗话说:“金无足赤,人无完人。”对于每一个Class也是这样,尽管我们说这个Class的代码规范、逻辑清晰合理等等,但是总会有它的短板,或者随着需求演进而无法订制实现功能。于是在Objective-C 2.0中引入了category这个特性,用以动态地为已有类添加新行为。面向对象的设计用来描述事物的组成往往是使用Class中的属性成员,这也就局限了方法的广度(在官方文档称之为An otherwise notable shortcoming for Objective-C,译为:Objc的一个显著缺陷)。所以在Runtime中引入了Associated Objects来弥补这一缺陷。
另外,请带着以下疑问来阅读此文:
- Associated Objects 使用场景。
- Associated Objects 五种
objc_AssociationPolicy
有什么区别。
- Associated Objects 的存储结构。
Associated Objects Introduction
Associated Objects是Objective-C 2.0中Runtime的特性之一。最早开始使用是在OS X Snow Leopard和iOS 4中。在<objc/runtime.h>
中定义的三个方法,也是我们深入探究Associated Objects的突破口:
- objc_setAssociatedObject
- objc_getAssociatedObject
- objc_removeAssociatedObjects
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
object
:传入关联对象的所属对象,也就是增加成员的实例对象,一般来说传入self。
key
:一个唯一标记。在官方文档中推荐使用static char
,当然更推荐是指针。为了便捷,一般使用selector
,这样在后面getter中,我们就可以利用_cmd
来方便的取出selector
。
value
:传入关联对象。
policy
:objc_AssociationPolicy
是一个ObjC枚举类型,也代表关联策略。
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
void objc_removeAssociatedObjects(id object)
从参数类型参数类型上,我们可以轻易的得出getter和remove方法传入参数的含义。要注意的是,objc_removeAssociatedObjects这个方法会移除一个对象的所有关联对象。其实,该方法我们一般是用不到的,移除所有关联意味着将类恢复成无任何关联的原始状态,这不是我们希望的。所以一般的做法是通过objc_setAssociatedObject
来传入nil
,从而移除某个已有的关联对象。
我们用Associated Objects这篇文中的例子来举例:
这时我们已经发现associatedObject
这个属性已经添加至NSObject
的实例中了。并且我们可以通过category指定的getter和setter方法对这个属性进行存取操作。(注:这里使用@dynamic
关键字是为了告知编译器:在编译期不要自动创建实现属性所用的存取方法。因为对于Associated Objects我们必须手动添加。当然,不写这个关键字,使用同名方法进行override也是可以达到相同效果的。但从编码规范和优化效率来讲,显式声明是最好的。)
AssociationPolicy
通过上面的例子,我们注意到了OBJC_ASSOCIATION_RETAIN_NONATOMIC
这个参数,它的枚举类型各个元素的含义如下:
Behavior |
@property Equivalent |
Description |
OBJC_ASSOCIATION_ASSIGN |
@property (assign) 或 @property (unsafe_unretained) |
指定一个关联对象的弱引用。 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC |
@property (nonatomic, strong) |
指定一个关联对象的强引用,不能被原子化使用。 |
OBJC_ASSOCIATION_COPY_NONATOMIC |
@property (nonatomic, copy) |
指定一个关联对象的copy引用,不能被原子化使用。 |
OBJC_ASSOCIATION_RETAIN |
@property (atomic, strong) |
指定一个关联对象的强引用,能被原子化使用。 |
OBJC_ASSOCIATION_COPY |
@property (atomic, copy) |
指定一个关联对象的copy引用,能被原子化使用。 |
OBJC_ASSOCIATION_GETTER_AUTORELEASE |
|
自动释放类型 |
OBJC_ASSOCIATION_ASSIGN类型的关联对象和weak
有一定差别,而更加接近于unsafe_unretained
,即当目标对象遭到摧毁时,属性值不会自动清空。(翻译自Associated Objects)
Usage Sample
同样是Associated Objects文中,总结了三个关于Associated Objects用法:
Analysis Source Code
在Objective-C Associated Objects 的实现原理这篇文中,作者有一个例子,作者分析了在Associated Objects中弱引用的区别。其代码片段如下:
在测试时候,我们发现有些情况下不至于导致crash。我猜想可能是因为[NSString stringWithFormat:]
方法的持有字符串可能会被编译器优化成compile-time constant。你可以尝试着做如下修改:
你会发现全部正常输出。因为所有字符串都变成了编译期常量而存储起来。所以探究方法,应该是讲类型更改成NSObject进行试验。
Setter Source Code
我们一直有个疑问,就是关联对象是如何存储的。下面我们看下Runtime的源码。
以下源码来自于opensource.apple.com的objc4-680.tar.gz。
我们读过代码后发现是其储存结构是这样的一个逻辑:
- 橙色的是
AssociationsManager
是顶级结构体,维护了一个spinlock_t
锁和一个_map
的哈希表。这个哈希表中的键为disguised_ptr_t
,在得到这个指针的时候,源码中执行了DISGUISE
方法,这个方法的功能是获得指向self地址的指针,即为指向对象地址的指针。通过地址这个唯一标识,可以找到对应的value,即一个子哈希表。(@饶志臻 勘误)
- 子哈希表是
ObjectAssociationMap
,键就是我们传入的Key
,而值是ObjcAssociation
,即这个成员对象。从而维护一个成员的所有属性。
在每次执行setter方法的时候,我们会逐层遍历Key,逐层判断。并且当持有Class有了关联属性的时候,在执行成员的Getter方法时,会优先查找Category中的关联成员。
这样会带来一个问题:如果category中的一个关联对象与Class中的某个成员同名,虽然key值不一定相同,自身的Class不一定相同,policy也不一定相同,但是我这样做会直接覆盖之前的成员,造成无法访问,但是其内部所有信息及数据全部存在。例如我们对ViewController
做一个Category,来创建一个叫做view的成员,我们会发现在运行工程的时候,模拟器直接黑屏。
我们在viewDidLoad中下断点,甚至无法进入debug模式。因为view属性已经被覆盖,所以不会继续进行viewController的生命周期。
这一点很危险,所以我们要杜绝覆盖Class原来的属性,这会破坏Class原有的功能。(当然,我是十分不推荐在业务项目中使用Runtime的,因为这样的代码可读性和维护性太低。)
Getter Source Code & Remove
这两种方法我们直接看源码,在看过Setter中的遍历嵌套map结构的代码片段后,你会很容易理解这两个方法。
另外,对于remove有一点补充。在Runtime的销毁对象函数objc_destructInstance里面会判断这个对象有没有关联对象,如果有,会调用_object_remove_assocations
做关联对象的清理工作。
Thinking About Hash Table
不光是本文讲述的关于Class关联对象的存储方式,还是Apple中其他的Souce Code(例如引用计数管理),我们能感受到Apple对Hash Table(本文中的map数据结构)这种数据结构情有独钟。在大量的实践中可以说明,Hash Table对于优化效率的提升,这是毋庸置疑的。
细究使用这种数据结构的原因,唯一的Key可对应指定的Value。我们从计算机存储的角度考虑,因为每个内存地址是唯一的,也就可以假象成Key,通过唯一的Key来读写数据,这是效率最高的方式。
The End
通过阅读此文,想必你已经知道那三个问题的答案。笔者原本想对UITableView-FDTemplateLayoutCell进行源码分析来撰写一篇文,但是发现里面存储cell的Key值使用到了Associated Objects该技术,所以对此进行了学习探究。后面,我会分析一下UITableView-FDTemplateLayoutCell的源码,这些将收录在我的这个Github仓库中。