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

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

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

關(guān)聯(lián)對象被存儲在什么地方,是不是存放在被關(guān)聯(lián)對象本身的內(nèi)存中?

關(guān)聯(lián)對象的五種關(guān)聯(lián)策略有什么區(qū)別,有什么坑?

關(guān)聯(lián)對象的生命周期是怎樣的,什么時候被釋放,什么時候被移除?

這是我寫這篇文章的初衷,也是本文的價值所在。

使用場景

按照 Mattt Thompson 大神的文章Associated Objects中的說法,Associated Objects 主要有以下三個使用場景:

為現(xiàn)有的類添加私有變量以幫助實現(xiàn)細節(jié);

為現(xiàn)有的類添加公有屬性;

為 KVO 創(chuàng)建一個關(guān)聯(lián)的觀察者。

從本質(zhì)上看,第 1 、2 個場景其實是一個意思,唯一的區(qū)別就在于新添加的這個屬性是公有的還是私有的而已。就目前來說,我在實際工作中使用得最多的是第 2 個場景,而第 3 個場景我還沒有使用過。

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

與 Associated Objects 相關(guān)的函數(shù)主要有三個,我們可以在 runtime 源碼的 runtime.h 文件中找到它們的聲明:

1

2

3void?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);

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

objc_setAssociatedObject 用于給對象添加關(guān)聯(lián)對象,傳入 nil 則可以移除已有的關(guān)聯(lián)對象;

objc_getAssociatedObject 用于獲取關(guān)聯(lián)對象;

objc_removeAssociatedObjects 用于移除一個對象的所有關(guān)聯(lián)對象。

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

key 值

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

聲明 static char kAssociatedObjectKey; ,使用 &kAssociatedObjectKey 作為 key 值;

聲明 static void *kAssociatedObjectKey = &kAssociatedObjectKey; ,使用 kAssociatedObjectKey 作為 key 值;

用 selector ,使用 getter 方法的名稱作為 key 值。

我個人最喜歡的(沒有之一)是第 3 種方式,因為它省掉了一個變量名,非常優(yōu)雅地解決了計算科學中的兩大世界難題之一(命名)。

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

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

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

實現(xiàn)原理

在探究 Associated Objects 的實現(xiàn)原理前,我們還是先來動手做一個小實驗,研究一下關(guān)聯(lián)對象什么時候會被釋放。本實驗主要涉及 ViewController 類和它的分類 ViewController+AssociatedObjects 。注:本實驗的完整代碼可以在這里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?{

returnobjc_getAssociatedObject(self,?_cmd);

}

-?(void)setAssociatedObject_assign:(NSString?*)associatedObject_assign?{

objc_setAssociatedObject(self,?@selector(associatedObject_assign),?associatedObject_assign,?OBJC_ASSOCIATION_ASSIGN);

}

-?(NSString?*)associatedObject_retain?{

returnobjc_getAssociatedObject(self,?_cmd);

}

-?(void)setAssociatedObject_retain:(NSString?*)associatedObject_retain?{

objc_setAssociatedObject(self,?@selector(associatedObject_retain),?associatedObject_retain,?OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

-?(NSString?*)associatedObject_copy?{

returnobjc_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 三種關(guān)聯(lián)策略為這三個屬性添加“實例變量”。

__weak?NSString?*string_weak_assign?=?nil;

__weak?NSString?*string_weak_retain?=?nil;

__weak?NSString?*string_weak_copy???=?nil;

@implementation?ViewController

-?(void)viewDidLoad?{

[superviewDidLoad];

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 變量來觀察相應對象的釋放時機。此外,我們重寫了 touchesBegan:withEvent: 方法,在方法中分別打印了這三個屬性的當前值。

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

實驗

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

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

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

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

結(jié)論

由這個實驗,我們可以得出以下結(jié)論:

關(guān)聯(lián)對象的釋放時機與被移除的時機并不總是一致的,比如上面的 self.associatedObject_assign 所指向的對象在 ViewController 出現(xiàn)后就被釋放了,但是 self.associatedObject_assign 仍然有值,還是保存的原對象的地址。如果之后再使用 self.associatedObject_assign 就會造成 Crash ,所以我們在使用弱引用的關(guān)聯(lián)對象時要非常小心;

一個對象的所有關(guān)聯(lián)對象是在這個對象被釋放時調(diào)用的 _object_remove_assocations 函數(shù)中被移除的。

接下來,我們就一起看看 runtime 中的源碼,來驗證下我們的實驗結(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?=newObjectAssociationMap;

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

}

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

AssociationsManager 是頂級的對象,維護了一個從 spinlock_t 鎖到 AssociationsHashMap 哈希表的單例鍵值對映射;

AssociationsHashMap 是一個無序的哈希表,維護了從對象地址到 ObjectAssociationMap 的映射;

ObjectAssociationMap 是一個 C++ 中的 map ,維護了從 key 到 ObjcAssociation 的映射,即關(guān)聯(lián)記錄;

ObjcAssociation 是一個 C++ 的類,表示一個具體的關(guān)聯(lián)結(jié)構(gòu),主要包括兩個實例變量,_policy 表示關(guān)聯(lián)策略,_value 表示關(guān)聯(lián)對象。

每一個對象地址對應一個 ObjectAssociationMap 對象,而一個 ObjectAssociationMap 對象保存著這個對象的若干個關(guān)聯(lián)記錄。

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

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

}

returnvalue;

}

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

deleterefs;

associations.erase(i);

}

}

//?the?calls?to?releaseValue()?happen?outside?of?the?lock.

for_each(elements.begin(),?elements.end(),?ReleaseValue());

}

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

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

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

+?(NSString?*)associatedObject;

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

總結(jié)

讀到這里,相信你對開篇的那三個問題已經(jīng)有了一定的認識,下面我們再梳理一下:

關(guān)聯(lián)對象與被關(guān)聯(lián)對象本身的存儲并沒有直接的關(guān)系,它是存儲在單獨的哈希表中的;

關(guān)聯(lián)對象的五種關(guān)聯(lián)策略與屬性的限定符非常類似,在絕大多數(shù)情況下,我們都會使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC 的關(guān)聯(lián)策略,這可以保證我們持有關(guān)聯(lián)對象;

關(guān)聯(lián)對象的釋放時機與移除時機并不總是一致,比如實驗中用關(guān)聯(lián)策略 OBJC_ASSOCIATION_ASSIGN 進行關(guān)聯(lián)的對象,很早就已經(jīng)被釋放了,但是并沒有被移除,而再使用這個關(guān)聯(lián)對象時就會造成 Crash 。

在弄懂 Associated Objects 的實現(xiàn)原理后,可以幫助我們更好地使用它,在出現(xiàn)問題時也能盡快地定位問題,最后希望本文能夠?qū)δ阌兴鶐椭?/p>

參考鏈接

http://nshipster.com/associated-objects/

http://kingscocoa.com/tutorials/associated-objects/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容