AssociatedObject
在 Objective-C 中可以通过 Category 给一个现有的类添加属性,但是却不能添加实例变量,值得庆幸的是,我们可以通过 Associated Objects 来弥补这一不足。
在阅读本文的过程中,读者需要着重关注以下三个问题:
- 关联对象被存储在什么地方,是不是存放在被关联对象本身的内存中?
- 关联对象的五种关联策略有什么区别,有什么坑?
- 关联对象的生命周期是怎样的,什么时候被释放,什么时候被移除?
一、使用场景
按照 Mattt Thompson 大神的文章 Associated Objects 中的说法,Associated Objects 主要有以下三个使用场景:
- 为现有的类添加私有变量以帮助实现细节;
- 为现有的类添加公有属性;
- 为 KVO 创建一个关联的观察者。
从本质上看,第 1 、2 个场景其实是一个意思,唯一的区别就在于新添加的这个属性是公有的还是私有的而已。
二、相关函数
与 Associated Objects 相关的函数主要有三个,我们可以在 runtime 源码的 runtime.h 文件中找到它们的声明:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object);
这三个函数的命名对程序员非常友好,可以一眼就看出函数的作用:
objc_setAssociatedObject
用于给对象添加关联对象,传入 nil 则可以移除已有的关联对象;objc_getAssociatedObject
用于获取关联对象;objc_removeAssociatedObjects
用于移除一个对象的所有关联对象。
objc_removeAssociatedObjects 函数一般用不上,因为这个函数会移除一个对象的所有关联对象,将该对象恢复成“原始”状态。这样做就很有可能把别人添加的关联对象也一并移除,这并不是我们所希望的。所以一般的做法是通过给 objc_setAssociatedObject 函数传入 nil
来移除某个已有的关联对象。
三、key 值
关于前两个函数中的 key 值必须保证是一个对象级别的唯一常量。一般来说,有以下三种推荐的 key 值:
- 声明
static char kAssociatedObjectKey;
,使用 &kAssociatedObjectKey 作为 key 值; - 声明
static void *kAssociatedObjectKey = &kAssociatedObjectKey;
,使用 kAssociatedObjectKey 作为 key 值; - 用
selector
,使用 getter 方法的名称作为 key 值。
第 3 种方式省掉了一个变量名。
四、关联策略
在给一个对象添加关联对象时有五种关联策略可供选择:
关联策略 | 等价属性 | 说明 |
---|---|---|
OBJC_ASSOCIATION_ASSIGN | @property (assign) @property (unsafe_unretained) |
弱引用关联对象 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property (strong, nonatomic) | 强引用关联对象,且为非原子操作 |
OBJC_ASSOCIATION_COPY_NONATOMIC | @property (copy, nonatomic) | 复制关联对象,且为非原子操作 |
OBJC_ASSOCIATION_RETAIN | @property (strong, atomic) | 强引用关联对象,且为原子操作 |
OBJC_ASSOCIATION_COPY | @property (copy, atomic) | 复制关联对象,且为原子操作 |
其中,第 2 种与第 4 种、第 3 种与第 5 种关联策略的唯一差别就在于操作是否具有原子性。由于操作的原子性不在本文的讨论范围内,所以下面的实验和讨论就以前三种以例进行展开。
五、实现原理
在探究 Associated Objects 的实现原理前,还是先来动手做一个小实验,研究一下关联对象什么时候会被释放。本实验主要涉及 ViewController 类和它的分类 ViewController+AssociatedObjects 。注:本实验的完整代码可以在这里 AssociatedObjects 找到,其中关键代码如下:
@interface ViewController (AssociatedObjects)
@property (assign, nonatomic) NSString *associatedObject_assign;
@property (strong, nonatomic) NSString *associatedObject_retain;
@property (copy, nonatomic) NSString *associatedObject_copy;
@end
@implementation ViewController (AssociatedObjects)
- (NSString *)associatedObject_assign {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setAssociatedObject_assign:(NSString *)associatedObject_assign {
objc_setAssociatedObject(self, @selector(associatedObject_assign), associatedObject_assign, OBJC_ASSOCIATION_ASSIGN);
}
- (NSString *)associatedObject_retain {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setAssociatedObject_retain:(NSString *)associatedObject_retain {
objc_setAssociatedObject(self, @selector(associatedObject_retain), associatedObject_retain, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)associatedObject_copy {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setAssociatedObject_copy:(NSString *)associatedObject_copy {
objc_setAssociatedObject(self, @selector(associatedObject_copy), associatedObject_copy, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
在 ViewController+AssociatedObjects.h
中声明了三个属性,限定符分别为 assign, nonatomic 、strong, nonatomic 和 copy, nonatomic ,而在 ViewController+AssociatedObjects.m 中相应的分别用 OBJC_ASSOCIATION_ASSIGN
、OBJC_ASSOCIATION_RETAIN_NONATOMIC
、OBJC_ASSOCIATION_COPY_NONATOMIC
三种关联策略为这三个属性添加“实例变量”。
__weak NSString *string_weak_assign = nil;
__weak NSString *string_weak_retain = nil;
__weak NSString *string_weak_copy = nil;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.associatedObject_assign = [NSString stringWithFormat:@"leichunfeng1"];
self.associatedObject_retain = [NSString stringWithFormat:@"leichunfeng2"];
self.associatedObject_copy = [NSString stringWithFormat:@"leichunfeng3"];
string_weak_assign = self.associatedObject_assign;
string_weak_retain = self.associatedObject_retain;
string_weak_copy = self.associatedObject_copy;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// NSLog(@"self.associatedObject_assign: %@", self.associatedObject_assign); // Will Crash
NSLog(@"self.associatedObject_retain: %@", self.associatedObject_retain);
NSLog(@"self.associatedObject_copy: %@", self.associatedObject_copy);
}
@end
在 ViewController 的 viewDidLoad 方法中,对三个属性进行了赋值,并声明了三个全局的 __weak
变量来观察相应对象的释放时机。
六、实验
如图打上断点,然后运行程序。断点触发后,使用 lldb 的 watchpoint
命令来设置观察点,观察全局变量 string_weak_assign
、string_weak_retain
和 string_weak_copy
的值的变化。console 中看到如下的类似输出:
继续运行,有一个观察点将被命中。我们先查看 console 中的输出,通过将这一步打印的 old value
和上一步的 new value
进行对比,可以知道本次命中的观察点是 string_weak_assign ,string_weak_assign 的值变成了 0x0000000000000000
,也就是 nil。
换句话说 self.associatedObject_assign 指向的对象已经被释放了,而通过查看左侧调用栈我们可以知道,这个对象是由于其所在的 autoreleasepool 被 drain 而被释放的。
接下来,点击 ViewController 导航栏左上角的按钮,返回前一个界面,此时,又将有一个观察点被命中。同理,我们可以知道这个观察点是 string_weak_retain 。我们查看左侧的调用栈,将会发现一个非常敏感的函数调用 _object_remove_assocations ,调用这个函数后 ViewController 的所有关联对象被全部移除。最终,self.associatedObject_retain 指向的对象被释放。
继续运行,最后一个观察点 string_weak_copy 被命中。同理,self.associatedObject_copy 指向的对象也由于关联对象的移除被最终释放。
七、结论
由这个实验,我们可以得出以下结论:
- 关联对象的释放时机与被移除的时机并不总是一致的,比如上面的 self.associatedObject_assign 所指向的对象在 ViewController 出现后就被释放了,但是 self.associatedObject_assign 仍然有值,还是保存的原对象的地址。如果之后再使用 self.associatedObject_assign 就会造成 Crash ,所以我们在使用弱引用的关联对象时要非常小心;
- 一个对象的所有关联对象是在这个对象被释放时调用的 _object_remove_assocations 函数中被移除的。
接下来,一起看看 runtime 中的源码,来验证下我们的实验结论。
八、objc_setAssociatedObject
我们可以在 objc-references.mm 文件中找到 objc_setAssociatedObject 函数最终调用的函数:
/**
* @brief 向 object 添加关联对象
*/
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
/**
* @brief 向 object 添加关联对象
*/
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy)
{
// retain the new value (if any) outside the lock.
// 新建 ObjcAssociation 对象:policy = 0(OBJC_ASSOCIATION_ASSIGN),value = nil
ObjcAssociation old_association(0, nil);
// value 根据策略执行 retain 或 copy 操作,并返回新对象
id new_value = value ? acquireValue(value, policy) : nil;
{
// 获取管理者
AssociationsManager manager;
// 拿到管理者内部的 AssociationsHashMap,即 associations。
AssociationsHashMap &associations(manager.associations());
// 传入的 object 经过 DISGUISE 函数被转化为了 disguised_ptr_t 类型的 disguised_object
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association. 查找 hashMap 中是否已经有了 disguised_object 这个 key,即受关联的对象是否已经有了关联
AssociationsHashMap::iterator i = associations.find(disguised_object);
// 受关联对象已经有过关联
if (i != associations.end()) {
// secondary table exists Map 已经存在
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
// object 相同的 key 有过关联对象
if (j != refs->end()) {
old_association = j->second;
// 保存
j->second = ObjcAssociation(policy, new_value);
}
else {
// 新建一个 key-value 键值对,value 是 ObjcAssociation 对象
(*refs)[key] = ObjcAssociation(policy, new_value);
}
}
else {
// create the new association (first time). 新建一个 ObjectAssociationMap
ObjectAssociationMap *refs = new ObjectAssociationMap;
// 存储到 AssociationsHashMap 中
associations[disguised_object] = refs;
// 新建 ObjcAssociation 对象,并存储到 ObjectAssociationMap 对象
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
}
else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
在看这段代码前,我们需要先了解一下几个数据结构以及它们之间的关系:
AssociationsManager
是顶级的对象,维护了一个从 spinlock_t 锁到 AssociationsHashMap 哈希表的单例键值对映射;AssociationsHashMap
是一个无序的哈希表,维护了从对象地址到 ObjectAssociationMap 的映射;ObjectAssociationMap
是一个 C++ 中的 map ,维护了从 key 到 ObjcAssociation 的映射,即关联记录;ObjcAssociation
是一个 C++ 的类,表示一个具体的关联结构,主要包括两个实例变量,_policy 表示关联策略,_value 表示关联对象。
每一个对象地址对应一个 ObjectAssociationMap 对象,而一个 ObjectAssociationMap 对象保存着这个对象的若干个关联记录。
流程图:
九、objc_getAssociatedObject
同样的,我们也可以在 objc-references.mm 文件中找到 objc_getAssociatedObject 函数最终调用的函数:
/**
* @brief 获取 object 的关联对象
*/
id objc_getAssociatedObject(id object, const void *key) {
return _object_get_associative_reference(object, (void *)key);
}
/**
* @brief 获取 object 指定 key 对应的关联对象
*/
id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
// 获取全局的管理者
AssociationsManager manager;
// 拿到管理者内部的 AssociationsHashMap,即 associations。
AssociationsHashMap &associations(manager.associations());
// object -》位运算 -》disguised_ptr_t
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
// object 有关联对象
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
// 对应 key 值有 value
if (j != refs->end()) {
ObjcAssociation &entry = j->second;
// 获取 value 和 policy
value = entry.value();
policy = entry.policy();
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
objc_retain(value);
}
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
objc_autorelease(value);
}
return value;
}
- 根据对象地址在 AssociationsHashMap 中查找其对应的 ObjectAssociationMap 对象;
- 如果能找到,则进一步根据 key 在 ObjectAssociationMap 对象中查找这个 key 所对应的关联结构 ObjcAssociation;
- 如果能找到则返回 ObjcAssociation 对象的 value 值,否则返回 nil。
十、objc_removeAssociatedObjects
同理,我们也可以在 objc-references.mm 文件中找到 objc_removeAssociatedObjects 函数最终调用的函数:
/**
* @brief 移除 object 的所有关联对象
*/
void objc_removeAssociatedObjects(id object)
{
if (object && object->hasAssociatedObjects()) {
_object_remove_assocations(object);
}
}
/**
* @brief 移除 object 的所有关联对象
*/
void _object_remove_assocations(id object) {
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
{
// 获取管理者
AssociationsManager manager;
// 拿到管理者内部的 AssociationsHashMap,即 associations。
AssociationsHashMap &associations(manager.associations());
if (associations.size() == 0) return;
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
// 当前对象有关联对象
if (i != associations.end()) {
// copy all of the associations that need to be removed.
ObjectAssociationMap *refs = i->second;
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
// 移除
elements.push_back(j->second);
}
// remove the secondary table.
// delete 内部调用 free 释放内存
delete refs;
associations.erase(i);
}
}
// the calls to releaseValue() happen outside of the lock.
for_each(elements.begin(), elements.end(), ReleaseValue());
}
这个函数负责移除一个对象的所有关联对象,具体实现也是先根据对象的地址获取其对应的 ObjectAssociationMap 对象,然后将所有的关联结构保存到一个 vector 中,最终释放 vector 中保存的所有关联对象。根据前面的实验观察到的情况,在一个对象被释放时,也正是调用的这个函数来移除其所有的关联对象。
十一、给类对象添加关联对象
看完源代码后,我们知道对象地址与 AssociationsHashMap 哈希表是一一对应的。
那么是否可以给类对象添加关联对象呢?
我们完全可以用同样的方式给类对象添加关联对象,只不过一般情况下不会这样做,因为更多时候我们可以通过 static 变量来实现类级别的变量。
十二、总结
- 关联对象与被关联对象本身的存储并没有直接的关系,它是存储在单独的哈希表中的;
- 关联对象的五种关联策略与属性的限定符非常类似,在绝大多数情况下,我们都会使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC 的关联策略,这可以保证我们持有关联对象;
- 关联对象的释放时机与移除时机并不总是一致,比如实验中用关联策略 OBJC_ASSOCIATION_ASSIGN 进行关联的对象,很早就已经被释放了,但是并没有被移除,而再使用这个关联对象时就会造成 Crash 。
一个实例对象就对应一个 ObjectAssociationMap,而 ObjectAssociationMap 中存储着多个此实例对象的关联对象的 key 以及 ObjcAssociation,为ObjcAssociation中存储着关联对象的 value 和 policy 策略。
关联对象并不是存在受关联的对象本身内存中,而是存储在全局的统一的一个AssociationsManager 中,如果设置关联对象为 nil,就相当于是移除关联对象。
十三、内容来源
雷纯锋的技术博客 - Objective-C Associated Objects 的实现原理
petyou - objc_setAssociatedObject等关联对象原理探究
xx_cc - iOS底层原理总结 - 关联对象实现原理
VanchChen - AssociatedObject关联对象原理实现
AssociatedObject的更多相关文章
- 使用关联对象(AssociatedObject)为UIButton添加Block响应
在开发中,要给UIButton添加点击事件的话,通常的做法是这样的 UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; [ ...
- AssociatedObject关联对象原理实现
介绍 关联对象(AssociatedObject)是Objective-C 2.0运行时的一个特性,允许开发者对已经存在的类在扩展中添加自定义的属性.在实际生产过程中,比较常用的方式是给分类(Cate ...
- 【OC底层】AssociatedObject 关联对象
如何实现给分类“添加成员变量”? 默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中.但可以通过关联对象来间接实现 关联对象提供了以下API 1> 添加关联对象 void objc_s ...
- 关联对象 AssociatedObject 完全解析
我们在 iOS 开发中经常需要使用分类(Category),为已经存在的类添加属性的需求,但是使用 @property 并不能在分类中正确创建实例变量和存取方法. 不过,通过 Objective-C ...
- iOS开发——高级特性&Runtime运行时特性详解
Runtime运行时特性详解 本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的动态特性,使这门古老的语言焕发生机.主要内容如下: 引言 ...
- [Objective-C]关联(objc_setAssociatedObject、objc_getAssociatedObject、objc_removeAssociatedObjects)
关联 关联是指把两个对象相互关联起来,使得其中的一个对象作为另外一个对象的一部分. 关联特性只有在Mac OS X V10.6以及以后的版本上才是可用的. 在类的定义之外为类增加额外的存储空间 ...
- iOS Run_time
Runtime是想要做好iOS开发,或者说是真正的深刻的掌握OC这门语言所必需理解的东西.最近在学习Runtime,有自己的一些心得,整理如下,一为 查阅方便二为 或许能给他人一些启发,三为 希望得到 ...
- WPF MVVM 验证
WPF MVVM(Caliburn.Micro) 数据验证 书接前文 前文中仅是WPF验证中的一种,我们暂且称之为View端的验证(因为其验证规是写在Xaml文件中的). 还有一种我们称之为Model ...
- iOS Runtime学习笔记
Associated Objects: @interface NSObject (AssociatedObject) @property (nonatomic, strong) id associat ...
随机推荐
- 4款java快速开发平台推荐
JBoss Seam JBoss Seam,算得上是Java开源框架里面最优秀的快速开发框架之一. Seam框架非常出色,尤其是他的组件机制设计的很有匠心,真不愧是Gavin King精心打造的框架了 ...
- 全差分运算放大器ADA4930的分析(1)
AD转换芯片的模拟信号输入端方式为:全差分.伪差分.单端输入,其中全差分输入的效果最佳,现阶段ADC转换器为了提高其性能,建议用户使用全差分的输入方式.(AD7982.ADS8317等都能实现信号的全 ...
- CSS 学习笔记——CSS Selector
CSS1 中定义的选择器 类型选择器 用于选择指定类型的元素(其实他就是 html 标签选择器),常见用法如下: body { /*对 body 元素定义样式*/ } body,div { /*同时选 ...
- https信任库采坑记
最近在客户现场遇到一个棘手的http问题,现象很直接,访问某https的时候报错: javax.net.ssl.SSLPeerUnverifiedException: peer not authent ...
- seo搜索优化教程05-SEO常用专业术语
SEO常用的专业术语很多,星辉信息科技专门抽空进行了整理,主要如下:. SEO 根据搜索引擎规则来进行搜索引擎优化,进而使得在搜索结果中获得较好的排名 关键词 关键词也叫keywords,表示在搜索引 ...
- DEV GridControl控件使用(CheckBox全选、操作按钮、事件处理,获取值)
1.GridControl控件使用 (1)绑定数据源 //绑定DataTable gridControl1.DataSource = DbHelper.ExecuteDataTable("S ...
- 推荐一个学习python非常好的网站
推荐一个入门python非常好的网站(也可以学习JAVA)非常适合入门,不说多易于理解,也是比较亲民的0基础学习教程,还免费…… 网址:https://www.liaoxuefeng.com/(廖雪峰 ...
- oracle使用expdp定时备份数据库
目录 oracle使用expdp备份数据库 备份shell脚本 创建定时任务 oracle使用expdp备份数据库 备份shell脚本 #!/bin/sh #获取当前时间 BACKUPTIME=$(d ...
- 数据科学中的常见的6种概率分布(Python实现)
作者:Pier Paolo Ippolito@南安普敦大学 编译:机器学习算法与Python实战(微信公众号:tjxj666) 原文:https://towardsdatascience.com/pr ...
- Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks
将 RCN 中下面 3 个独立模块整合在一起,减少计算量: CNN:提取图像特征 SVM:目标分类识别 Regression 模型:定位 不对每个候选区域独立通过 CN 提取特征,将整个图像通过 CN ...