浅谈Associated Objects

俗话说:“金无足赤,人无完人。”对于每一个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 LeopardiOS 4中。在中定义的三个方法,也是我们深入探究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:传入关联对象。
  • policyobjc_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也是可以达到相同效果的。但从编码规范和优化效率来讲,显式声明是最好的。)

11208988-10a9d08b532258d3

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.comobjc4-680.tar.gz

我们读过代码后发现是其储存结构是这样的一个逻辑:

12208988-67f51f426f98ce53

  • 橙色的是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的成员,我们会发现在运行工程的时候,模拟器直接黑屏。

13208988-97d8f5bde8f5de41

我们在viewDidLoad中下断点,甚至无法进入debug模式。因为view属性已经被覆盖,所以不会继续进行viewController的生命周期。

14208988-12aa766163679316

这一点很危险,所以我们要杜绝覆盖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仓库中

1 2 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部