Objective-C Associated Objects 的實(shí)現(xiàn)原理

我們知道,在 Objective-C 中可以通過 Category 給一個(gè)現(xiàn)有的類添加屬性,但是卻不能添加實(shí)例變量,這似乎成為了 Objective-C 的一個(gè)明顯短板。然而值得慶幸的是,我們可以通過 Associated Objects 來彌補(bǔ)這一不足。本文將結(jié)合 runtime 源碼深入探究 Objective-C 中 Associated Objects 的實(shí)現(xiàn)原理。

在閱讀本文的過程中,讀者需要著重關(guān)注以下三個(gè)問題:

關(guān)聯(lián)對(duì)象被存儲(chǔ)在什么地方,是不是存放在被關(guān)聯(lián)對(duì)象本身的內(nèi)存中?
關(guān)聯(lián)對(duì)象的五種關(guān)聯(lián)策略有什么區(qū)別,有什么坑?
關(guān)聯(lián)對(duì)象的生命周期是怎樣的,什么時(shí)候被釋放,什么時(shí)候被移除?
這是我寫這篇文章的初衷,也是本文的價(jià)值所在。

使用場(chǎng)景

按照 Mattt Thompson 大神的文章 Associated Objects 中的說法,Associated Objects 主要有以下三個(gè)使用場(chǎng)景:

為現(xiàn)有的類添加私有變量以幫助實(shí)現(xiàn)細(xì)節(jié);
為現(xiàn)有的類添加公有屬性;
為 KVO 創(chuàng)建一個(gè)關(guān)聯(lián)的觀察者。
從本質(zhì)上看,第 1 、2 個(gè)場(chǎng)景其實(shí)是一個(gè)意思,唯一的區(qū)別就在于新添加的這個(gè)屬性是公有的還是私有的而已。就目前來說,我在實(shí)際工作中使用得最多的是第 2 個(gè)場(chǎng)景,而第 3 個(gè)場(chǎng)景我還沒有使用過。

相關(guān)函數(shù)

與 Associated Objects 相關(guān)的函數(shù)主要有三個(gè),我們可以在 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);

這三個(gè)函數(shù)的命名對(duì)程序員非常友好,可以讓我們一眼就看出函數(shù)的作用:

objc_setAssociatedObject 用于給對(duì)象添加關(guān)聯(lián)對(duì)象,傳入 nil 則可以移除已有的關(guān)聯(lián)對(duì)象;
objc_getAssociatedObject 用于獲取關(guān)聯(lián)對(duì)象;
objc_removeAssociatedObjects 用于移除一個(gè)對(duì)象的所有關(guān)聯(lián)對(duì)象。
注:objc_removeAssociatedObjects 函數(shù)我們一般是用不上的,因?yàn)檫@個(gè)函數(shù)會(huì)移除一個(gè)對(duì)象的所有關(guān)聯(lián)對(duì)象,將該對(duì)象恢復(fù)成“原始”狀態(tài)。這樣做就很有可能把別人添加的關(guān)聯(lián)對(duì)象也一并移除,這并不是我們所希望的。所以一般的做法是通過給 objc_setAssociatedObject 函數(shù)傳入 nil 來移除某個(gè)已有的關(guān)聯(lián)對(duì)象。

key 值

關(guān)于前兩個(gè)函數(shù)中的 key 值是我們需要重點(diǎn)關(guān)注的一個(gè)點(diǎn),這個(gè) key 值必須保證是一個(gè)對(duì)象級(jí)別(為什么是對(duì)象級(jí)別?看完下面的章節(jié)你就會(huì)明白了)的唯一常量。一般來說,有以下三種推薦的 key 值:

聲明 static char kAssociatedObjectKey; ,使用 &kAssociatedObjectKey 作為 key 值;
聲明 static void *kAssociatedObjectKey = &kAssociatedObjectKey; ,使用 kAssociatedObjectKey 作為 key 值;
用 selector ,使用 getter 方法的名稱作為 key 值。
我個(gè)人最喜歡的(沒有之一)是第 3 種方式,因?yàn)樗〉袅艘粋€(gè)變量名,非常優(yōu)雅地解決了計(jì)算科學(xué)中的兩大世界難題之一(命名)。

關(guān)聯(lián)策略

在給一個(gè)對(duì)象添加關(guān)聯(lián)對(duì)象時(shí)有五種關(guān)聯(lián)策略可供選擇:

其中,第 2 種與第 4 種、第 3 種與第 5 種關(guān)聯(lián)策略的唯一差別就在于操作是否具有原子性。由于操作的原子性不在本文的討論范圍內(nèi),所以下面的實(shí)驗(yàn)和討論就以前三種以例進(jìn)行展開。

實(shí)現(xiàn)原理

在探究 Associated Objects 的實(shí)現(xiàn)原理前,我們還是先來動(dòng)手做一個(gè)小實(shí)驗(yàn),研究一下關(guān)聯(lián)對(duì)象什么時(shí)候會(huì)被釋放。本實(shí)驗(yàn)主要涉及 ViewController 類和它的分類 ViewController+AssociatedObjects 。注:本實(shí)驗(yàn)的完整代碼可以在這里 AssociatedObjects 找到,其中關(guān)鍵代碼如下

@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 中聲明了三個(gè)屬性,限定符分別為 assign, nonatomic 、strong, nonatomic 和 copy, nonatomic ,而在 ViewController+AssociatedObjects.m 中相應(yīng)的分別用 OBJC_ASSOCIATION_ASSIGN 、OBJC_ASSOCIATION_RETAIN_NONATOMIC 、OBJC_ASSOCIATION_COPY_NONATOMIC 三種關(guān)聯(lián)策略為這三個(gè)屬性添加“實(shí)例變量”。

__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 方法中,我們對(duì)三個(gè)屬性進(jìn)行了賦值,并聲明了三個(gè)全局的 __weak 變量來觀察相應(yīng)對(duì)象的釋放時(shí)機(jī)。此外,我們重寫了 touchesBegan:withEvent: 方法,在方法中分別打印了這三個(gè)屬性的當(dāng)前值。

在繼續(xù)閱讀下面章節(jié)前,建議讀者先自行思考一下 self.associatedObject_assign 、self.associatedObject_retain 和 self.associatedObject_copy 指向的對(duì)象分別會(huì)在什么時(shí)候被釋放,以加深理解。

實(shí)驗(yàn)

我們先在 viewDidLoad 方法的第 28 行打上斷點(diǎn),然后運(yùn)行程序,點(diǎn)擊導(dǎo)航欄右上角的按鈕 Push 到 ViewController 界面,程序?qū)⑼T跀帱c(diǎn)處。接著,我們使用 lldb 的 watchpoint 命令來設(shè)置觀察點(diǎn),觀察全局變量 string_weak_assign 、string_weak_retain 和 string_weak_copy 的值的變化。正確設(shè)置好觀察點(diǎn)后,將會(huì)在 console 中看到如下的類似輸出:

點(diǎn)擊繼續(xù)運(yùn)行按鈕,有一個(gè)觀察點(diǎn)將被命中。我們先查看 console 中的輸出,通過將這一步打印的 old value 和上一步的 new value 進(jìn)行對(duì)比,我們可以知道本次命中的觀察點(diǎn)是 string_weak_assign ,string_weak_assign 的值變成了 0x0000000000000000 ,也就是 nil 。換句話說 self.associatedObject_assign 指向的對(duì)象已經(jīng)被釋放了,而通過查看左側(cè)調(diào)用棧我們可以知道,這個(gè)對(duì)象是由于其所在的 autoreleasepool 被 drain 而被釋放的,這與我前面的文章《Objective-C Autorelease Pool 的實(shí)現(xiàn)原理》中的表述是一致的。提示,待會(huì)你也可以放開 touchesBegan:withEvent: 中第 31 行的注釋,在 ViewController 出現(xiàn)后,點(diǎn)擊一下它的 view ,進(jìn)一步驗(yàn)證一下這個(gè)結(jié)論。

接下來,我們點(diǎn)擊 ViewController 導(dǎo)航欄左上角的按鈕,返回前一個(gè)界面,此時(shí),又將有一個(gè)觀察點(diǎn)被命中。同理,我們可以知道這個(gè)觀察點(diǎn)是 string_weak_retain 。我們查看左側(cè)的調(diào)用棧,將會(huì)發(fā)現(xiàn)一個(gè)非常敏感的函數(shù)調(diào)用 _object_remove_assocations ,調(diào)用這個(gè)函數(shù)后 ViewController 的所有關(guān)聯(lián)對(duì)象被全部移除。最終,self.associatedObject_retain 指向的對(duì)象被釋放。

點(diǎn)擊繼續(xù)運(yùn)行按鈕,最后一個(gè)觀察點(diǎn) string_weak_copy 被命中。同理,self.associatedObject_copy 指向的對(duì)象也由于關(guān)聯(lián)對(duì)象的移除被最終釋放。

結(jié)論

由這個(gè)實(shí)驗(yàn),我們可以得出以下結(jié)論:

關(guān)聯(lián)對(duì)象的釋放時(shí)機(jī)與被移除的時(shí)機(jī)并不總是一致的,比如上面的 self.associatedObject_assign 所指向的對(duì)象在 ViewController 出現(xiàn)后就被釋放了,但是 self.associatedObject_assign 仍然有值,還是保存的原對(duì)象的地址。如果之后再使用 self.associatedObject_assign 就會(huì)造成 Crash ,所以我們?cè)谑褂萌跻玫年P(guān)聯(lián)對(duì)象時(shí)要非常小心;
一個(gè)對(duì)象的所有關(guān)聯(lián)對(duì)象是在這個(gè)對(duì)象被釋放時(shí)調(diào)用的 _object_remove_assocations 函數(shù)中被移除的。
接下來,我們就一起看看 runtime 中的源碼,來驗(yàn)證下我們的實(shí)驗(yàn)結(jié)論。

objc_setAssociatedObject

我們可以在 objc-references.mm 文件中找到 objc_setAssociatedObject 函數(shù)最終調(diào)用的函數(shù):

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);
}

在看這段代碼前,我們需要先了解一下幾個(gè)數(shù)據(jù)結(jié)構(gòu)以及它們之間的關(guān)系:

AssociationsManager 是頂級(jí)的對(duì)象,維護(hù)了一個(gè)從 spinlock_t 鎖到 AssociationsHashMap 哈希表的單例鍵值對(duì)映射;
AssociationsHashMap 是一個(gè)無序的哈希表,維護(hù)了從對(duì)象地址到 ObjectAssociationMap 的映射;
ObjectAssociationMap 是一個(gè) C++ 中的 map ,維護(hù)了從 key 到 ObjcAssociation 的映射,即關(guān)聯(lián)記錄;
ObjcAssociation 是一個(gè) C++ 的類,表示一個(gè)具體的關(guān)聯(lián)結(jié)構(gòu),主要包括兩個(gè)實(shí)例變量,_policy 表示關(guān)聯(lián)策略,_value 表示關(guān)聯(lián)對(duì)象。
每一個(gè)對(duì)象地址對(duì)應(yīng)一個(gè) ObjectAssociationMap 對(duì)象,而一個(gè) ObjectAssociationMap 對(duì)象保存著這個(gè)對(duì)象的若干個(gè)關(guān)聯(lián)記錄。

弄清楚這些數(shù)據(jù)結(jié)構(gòu)之間的關(guān)系后,再回過頭來看上面的代碼就不難了。我們發(fā)現(xiàn),在蘋果的底層代碼中一般都會(huì)充斥著各種 if else ,可見寫好 if else 后我們就距離成為高手不遠(yuǎn)了。開個(gè)玩笑,我們來看下面的流程圖,一圖勝千言:

objc_getAssociatedObject

同樣的,我們也可以在 objc-references.mm 文件中找到 objc_getAssociatedObject 函數(shù)最終調(diào)用的函數(shù):

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_setAssociatedObject 函數(shù)后,objc_getAssociatedObject 函數(shù)對(duì)我們來說就是小菜一碟了。這個(gè)函數(shù)先根據(jù)對(duì)象地址在 AssociationsHashMap 中查找其對(duì)應(yīng)的 ObjectAssociationMap 對(duì)象,如果能找到則進(jìn)一步根據(jù) key 在 ObjectAssociationMap 對(duì)象中查找這個(gè) key 所對(duì)應(yīng)的關(guān)聯(lián)結(jié)構(gòu) ObjcAssociation ,如果能找到則返回 ObjcAssociation 對(duì)象的 value 值,否則返回 nil 。

objc_removeAssociatedObjects

同理,我們也可以在 objc-references.mm 文件中找到 objc_removeAssociatedObjects 函數(shù)最終調(diào)用的函數(shù):

void _object_remove_assocations(id object) {
    vector< ObjcAssociation,ObjcAllocator > 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());
}

這個(gè)函數(shù)負(fù)責(zé)移除一個(gè)對(duì)象的所有關(guān)聯(lián)對(duì)象,具體實(shí)現(xiàn)也是先根據(jù)對(duì)象的地址獲取其對(duì)應(yīng)的 ObjectAssociationMap 對(duì)象,然后將所有的關(guān)聯(lián)結(jié)構(gòu)保存到一個(gè) vector 中,最終釋放 vector 中保存的所有關(guān)聯(lián)對(duì)象。根據(jù)前面的實(shí)驗(yàn)觀察到的情況,在一個(gè)對(duì)象被釋放時(shí),也正是調(diào)用的這個(gè)函數(shù)來移除其所有的關(guān)聯(lián)對(duì)象。

給類對(duì)象添加關(guān)聯(lián)對(duì)象

看完源代碼后,我們知道對(duì)象地址與 AssociationsHashMap 哈希表是一一對(duì)應(yīng)的。那么我們可能就會(huì)思考這樣一個(gè)問題,是否可以給類對(duì)象添加關(guān)聯(lián)對(duì)象呢?答案是肯定的。我們完全可以用同樣的方式給類對(duì)象添加關(guān)聯(lián)對(duì)象,只不過我們一般情況下不會(huì)這樣做,因?yàn)楦鄷r(shí)候我們可以通過 static 變量來實(shí)現(xiàn)類級(jí)別的變量。我在分類 ViewController+AssociatedObjects 中給 ViewController 類對(duì)象添加了一個(gè)關(guān)聯(lián)對(duì)象 associatedObject ,讀者可以親自在 viewDidLoad 方法中調(diào)用一下以下兩個(gè)方法驗(yàn)證一下:

+ (NSString *)associatedObject;
+ (void)setAssociatedObject:(NSString *)associatedObject;

總結(jié)

讀到這里,相信你對(duì)開篇的那三個(gè)問題已經(jīng)有了一定的認(rèn)識(shí),下面我們?cè)偈崂硪幌拢?/p>

關(guān)聯(lián)對(duì)象與被關(guān)聯(lián)對(duì)象本身的存儲(chǔ)并沒有直接的關(guān)系,它是存儲(chǔ)在單獨(dú)的哈希表中的;
關(guān)聯(lián)對(duì)象的五種關(guān)聯(lián)策略與屬性的限定符非常類似,在絕大多數(shù)情況下,我們都會(huì)使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC 的關(guān)聯(lián)策略,這可以保證我們持有關(guān)聯(lián)對(duì)象;
關(guān)聯(lián)對(duì)象的釋放時(shí)機(jī)與移除時(shí)機(jī)并不總是一致,比如實(shí)驗(yàn)中用關(guān)聯(lián)策略 OBJC_ASSOCIATION_ASSIGN 進(jìn)行關(guān)聯(lián)的對(duì)象,很早就已經(jīng)被釋放了,但是并沒有被移除,而再使用這個(gè)關(guān)聯(lián)對(duì)象時(shí)就會(huì)造成 Crash 。
在弄懂 Associated Objects 的實(shí)現(xiàn)原理后,可以幫助我們更好地使用它,在出現(xiàn)問題時(shí)也能盡快地定位問題,最后希望本文能夠?qū)δ阌兴鶐椭?/p>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容