原文连接:http://blog.leichunfeng.com/blog/2015/06/26/objective-c-associated-objects-implementation-principle/ git
咱们知道,在 Objective-C 中能够经过 Category 给一个现有的类添加属性,可是却不能添加实例变量,这彷佛成为了 Objective-C 的一个明显短板。然而值得庆幸的是,咱们能够经过 Associated Objects 来弥补这一不足。github
在阅读本文的过程当中,读者须要着重关注如下三个问题:objective-c
关联对象被存储在什么地方,是否是存放在被关联对象自己的内存中?ide
关联对象的五种关联策略有什么区别,有什么坑?函数
关联对象的生命周期是怎样的,何时被释放,何时被移除?测试
与 Associated Objects 相关的函数主要有三个,咱们能够在 runtime 源码的 runtime.h 文件中找到它们的声明:ui
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
OBJC_EXPORT void objc_removeAssociatedObjects(id object)
这三个函数的命名很容易看懂:atom
objc_setAssociatedObject 用于给对象添加关联对象,传入 nil 则能够移除已有的关联对象;code
objc_getAssociatedObject 用于获取关联对象;对象
objc_removeAssociatedObjects 用于移除一个对象的全部关联对象。
注:objc_removeAssociatedObjects 函数咱们通常是用不上的,由于这个函数会移除一个对象的全部关联对象,将该对象恢复成“原始”状态。这样作就颇有可能把别人添加的关联对象也一并移除,这并非咱们所但愿的。因此通常的作法是经过给 objc_setAssociatedObject 函数传入 nil 来移除某个已有的关联对象。
Key值:
关于前两个函数中的 key 值是咱们须要重点关注的一个点,这个 key 值必须保证是一个对象级别(为何是对象级别?看完下面的章节你就会明白了)的惟一常量。通常来讲,有如下三种推荐的 key 值:
声明 static char kAssociatedObjectKey; ,使用 &kAssociatedObjectKey 做为 key 值;
声明 static void *kAssociatedObjectKey = &kAssociatedObjectKey; ,使用 kAssociatedObjectKey 做为 key 值;
用 selector ,使用 getter 方法的名称做为 key 值。
推荐使用第三种,好用,省代码量。
关联策略
关联策略 等价属性 说明OBJC_ASSOCIATION_ASSIGN @property (assign) or 弱引用关联对象
@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) 复制关联对象,且为原子操做
原子性问题不在这里讨论,下面主要讨论前三种形式:
实现原理
代码Github原文连接:https://github.com/leichunfeng/AssociatedObjects
从测试代码中能够看出:
关联对象的释放时机与被移除的时机并不老是一致的,好比上面的 self.associatedObject_assign 所指向的对象在 ViewController 出现后就被释放了,可是 self.associatedObject_assign 仍然有值,仍是保存的原对象的地址。若是以后再使用 self.associatedObject_assign 就会形成 Crash ,因此咱们在使用弱引用的关联对象时要很是当心;
一个对象的全部关联对象是在这个对象被释放时调用的 _object_remove_assocations 函数中被移除的。
打开 objc-references.mm,找到 objc_setAssociatedObject 函数最终调用的函数;
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) { // retain the new value (if any) outside the lock. ObjcAssociation old_association(0, nil); id new_value = value ? acquireValue(value, policy) : nil; { AssociationsManager manager; AssociationsHashMap &associations(manager.associations()); disguised_ptr_t disguised_object = DISGUISE(object); if (new_value) { // break any existing association. AssociationsHashMap::iterator i = associations.find(disguised_object); if (i != associations.end()) { // secondary table exists ObjectAssociationMap *refs = i->second; ObjectAssociationMap::iterator j = refs->find(key); if (j != refs->end()) { old_association = j->second; j->second = ObjcAssociation(policy, new_value); } else { (*refs)[key] = ObjcAssociation(policy, new_value); } } else { // create the new association (first time). ObjectAssociationMap *refs = new ObjectAssociationMap; associations[disguised_object] = refs; (*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); }
在 objc-references.mm 的 objc_getAssociatedObject 函数最终调用了的函数:
id _object_get_associative_reference(id object, void *key) { id value = nil; uintptr_t policy = OBJC_ASSOCIATION_ASSIGN; { AssociationsManager manager; AssociationsHashMap &associations(manager.associations()); disguised_ptr_t disguised_object = DISGUISE(object); 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()) { ObjcAssociation &entry = j->second; value = entry.value(); policy = entry.policy(); if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain); } } } if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) { ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease); } return value; }
objc_removeAssociatedObjects
这个函数负责移除一个对象的全部关联对象,具体实现也是先根据对象的地址获取其对应的 ObjectAssociationMap 对象,而后将全部的关联结构保存到一个 vector 中,最终释放 vector 中保存的全部关联对象。根据前面的实验观察到的状况,在一个对象被释放时,也正是调用的这个函数来移除其全部的关联对象。
给类对象添加关联对象
看完源代码后,咱们知道对象地址与 AssociationsHashMap 哈希表是一一对应的。那么咱们可能就会思考这样一个问题,是否能够给类对象添加关联对象呢?答案是确定的。咱们彻底能够用一样的方式给类对象添加关联对象,只不过咱们通常状况下不会这样作,由于更多时候咱们能够经过 static 变量来实现类级别的变量。我在分类 ViewController+AssociatedObjects 中给 ViewController 类对象添加了一个关联对象 associatedObject 。
总结
关联对象与被关联对象自己的存储并无直接的关系,它是存储在单独的哈希表中的;
关联对象的五种关联策略与属性的限定符很是相似,在绝大多数状况下,咱们都会使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC 的关联策略,这能够保证咱们持有关联对象;
关联对象的释放时机与移除时机并不老是一致,好比实验中用关联策略 OBJC_ASSOCIATION_ASSIGN 进行关联的对象,很早就已经被释放了,可是并无被移除,而再使用这个关联对象时就会形成 Crash 。
四、demo在个人github有一个例子,是将UIAlertView的delegate实现点击事件回调改为在初始化的时候直接传入,在category中使用本博客中的方法实现block保存以及调用。
https://github.com/caijunrong/JRAlertView.git