我们知道,在 Objective-C 中可以通过 Category 给一个现有的类添加属性,但是却不能添加实例变量,这似乎成为了 Objective-C 的一个明显短板。然而值得庆幸的是,我们可以通过 Associated Objects 来弥补这一不足。本文将结合 runtime 源码深入探究 Objective-C 中 Associated Objects 的实现原理。
在阅读本文的过程中,读者需要着重关注以下三个问题:
这是我写这篇文章的初衷,也是本文的价值所在。
按照 Mattt Thompson 大神的文章 Associated Objects 中的说法,Associated Objects 主要有以下三个使用场景:
从本质上看,第1、2 个场景其实是一个意思,唯一的区别就在于新添加的这个属性是公有的还是私有的而已。就目前来说,我在实际工作中使用得最多的是第2个场景,而第 3个场景我还没有使用过。
与 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_removeAssociatedObjects函数我们一般是用不上的,因为这个函数会移除一个对象的所有关联对象,将该对象恢复成“原始”状态。这样做就很有可能把别人添加的关联对象也一并移除,这并不是我们所希望的。所以一般的做法是通过给objc_setAssociatedObject函数传入 nil 来移除某个已有的关联对象。
关于前两个函数中的 key 值是我们需要重点关注的一个点,这个key 值必须保证是一个对象级别(为什么是对象级别?看完下面的章节你就会明白了)的唯一常量。一般来说,有以下三种推荐的key 值:
我个人最喜欢的(没有之一)是第3种方式,因为它省掉了一个变量名,非常优雅地解决了计算科学中的两大世界难题之一(命名)。
在给一个对象添加关联对象时有五种关联策略可供选择:
关联策略 | 等价属性 | 说明 |
---|---|---|
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) | 复制关联对象,且为原子操作 |
其中,第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
__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
在继续阅读下面章节前,建议读者先自行思考一下self.associatedObject_assign、self.associatedObject_retain和 self.associatedObject_copy指向的对象分别会在什么时候被释放,以加深理解。
我们先在viewDidLoad方法的第28行打上断点,然后运行程序,点击导航栏右上角的按钮Push到ViewController界面,程序将停在断点处。接着,我们使用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而被释放的,这与我前面的文章《Objective-C Autorelease Pool 的实现原理》中的表述是一致的。提示,待会你也可以放开touchesBegan:withEvent:中第31行的注释,在ViewController出现后,点击一下它的view,进一步验证一下这个结论。
接下来,我们点击ViewController导航栏左上角的按钮,返回前一个界面,此时,又将有一个观察点被命中。同理,我们可以知道这个观察点是string_weak_retain。我们查看左侧的调用栈,将会发现一个非常敏感的函数调用 _object_remove_assocations,调用这个函数后ViewController的所有关联对象被全部移除。最终,self.associatedObject_retain指向的对象被释放。
点击继续运行按钮,最后一个观察点string_weak_copy被命中。同理,self.associatedObject_copy指向的对象也由于关联对象的移除被最终释放。
由这个实验,我们可以得出以下结论:
接下来,我们就一起看看 runtime 中的源码,来验证下我们的实验结论。
我们可以在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); }
在看这段代码前,我们需要先了解一下几个数据结构以及它们之间的关系:
AssociationsManager是顶级的对象,维护了一个从spinlock_t锁到AssociationsHashMap哈希表的单例键值对映射;
AssociationsHashMap是一个无序的哈希表,维护了从对象地址到ObjectAssociationMap的映射;
ObjectAssociationMap是一个C++中的map,维护了从key到ObjcAssociation的映射,即关联记录;
_value表示关联对象。
每一个对象地址对应一个ObjectAssociationMap对象,而一个ObjectAssociationMap对象保存着这个对象的若干个关联记录。
弄清楚这些数据结构之间的关系后,再回过头来看上面的代码就不难了。我们发现,在苹果的底层代码中一般都会充斥着各种if else,可见写好if else后我们就距离成为高手不远了。开个玩笑,我们来看下面的流程图,一图胜千言:
同样的,我们也可以在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; }
对象的value值,否则返回nil。
同理,我们也可以在objc-references.mm文件中找到objc_removeAssociatedObjects函数最终调用的函数:
void _object_remove_assocations(id object) { vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements; { AssociationsManager manager; 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 refs; associations.erase(i); } } // the calls to releaseValue() happen outside of the lock. for_each(elements.begin(), elements.end(), ReleaseValue()); }
看完源代码后,我们知道对象地址与AssociationsHashMap哈希表是一一对应的。那么我们可能就会思考这样一个问题,是否可以给类对象添加关联对象呢?答案是肯定的。我们完全可以用同样的方式给类对象添加关联对象,只不过我们一般情况下不会这样做,因为更多时候我们可以通过static变量来实现类级别的变量。我在分类ViewController+AssociatedObjects中给ViewController类对象添加了一个关联对象associatedObject,读者可以亲自在viewDidLoad方法中调用一下以下两个方法验证一下:
+ (NSString *)associatedObject; + (void)setAssociatedObject:(NSString *)associatedObject;
读到这里,相信你对开篇的那三个问题已经有了一定的认识,下面我们再梳理一下:
在弄懂 Associated Objects 的实现原理后,可以帮助我们更好地使用它,在出现问题时也能尽快地定位问题,最后希望本文能够对你有所帮助。
原文地址:http://blog.leichunfeng.com/blog/2015/06/26/objective-c-associated-objects-implementation-principle/