你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> iOS Objective-C Associated Objects 的實現原理

iOS Objective-C Associated Objects 的實現原理

編輯:IOS開發綜合

總結:

 

  1. 關聯對象與被關聯對象本身的存儲並沒有直接的關系,它是存儲在單獨的哈希表中的;
  2.  
  3. 關聯對象的五種關聯策略與屬性的限定符非常類似,在絕大多數情況下,我們都會使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC 的關聯策略,這可以保證我們持有關聯對象;
  4.  
  5. 關聯對象的釋放時機與移除時機並不總是一致,比如實驗中用關聯策略 OBJC_ASSOCIATION_ASSIGN 進行關聯的對象,很早就已經被釋放了,但是並沒有被移除,而再使用這個關聯對象時就會造成 Crash 。

 

/********************************************************************/

 

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

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

  1. 關聯對象被存儲在什麼地方,是不是存放在被關聯對象本身的內存中?

  2. 關聯對象的五種關聯策略有什麼區別,有什麼坑?

  3. 關聯對象的生命周期是怎樣的,什麼時候被釋放,什麼時候被移除?

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

使用場景

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

  1. 為現有的類添加私有變量以幫助實現細節;

  2. 為現有的類添加公有屬性;

  3. 為 KVO 創建一個關聯的觀察者。

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

相關函數

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

  voidobjc_setAssociatedObject(idobject,constvoid*key,idvalue,objc_AssociationPolicypolicy); idobjc_getAssociatedObject(idobject,constvoid*key); voidobjc_removeAssociatedObjects(idobject);

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

  1. objc_setAssociatedObject 用於給對象添加關聯對象,傳入 nil 則可以移除已有的關聯對象;

  2. objc_getAssociatedObject 用於獲取關聯對象;

  3. objc_removeAssociatedObjects 用於移除一個對象的所有關聯對象。

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

key 值

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

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

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

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

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

關聯策略

在給一個對象添加關聯對象時有五種關聯策略可供選擇:

blob.png

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

實現原理

在探究 Associated Objects 的實現原理前,我們還是先來動手做一個小實驗,研究一下關聯對象什麼時候會被釋放。本實驗主要涉及 ViewController 類和它的分類 ViewController+AssociatedObjects 。注:本實驗的完整代碼可以在這裡AssociatedObjects找到,其中關鍵代碼如下:

  @interfaceViewController(AssociatedObjects) @property(assign,nonatomic)NSString*associatedObject_assign; @property(strong,nonatomic)NSString*associatedObject_retain; @property(copy,nonatomic)NSString*associatedObject_copy; @end @implementationViewController(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 三種關聯策略為這三個屬性添加“實例變量”。

  __weakNSString*string_weak_assign=nil; __weakNSString*string_weak_retain=nil; __weakNSString*string_weak_copy=nil; @implementationViewController -(void)viewDidLoad{ [superviewDidLoad]; self.associatedObject_assign=[NSStringstringWithFormat:@"leichunfeng1"]; self.associatedObject_retain=[NSStringstringWithFormat:@"leichunfeng2"]; self.associatedObject_copy=[NSStringstringWithFormat:@"leichunfeng3"]; string_weak_assign=self.associatedObject_assign; string_weak_retain=self.associatedObject_retain; string_weak_copy=self.associatedObject_copy; } -(void)touchesBegan:(NSSet*)toucheswithEvent:(UIEvent*)event{ //NSLog(@"self.associatedObject_assign:%@",self.associatedObject_assign);//WillCrash NSLog(@"self.associatedObject_retain:%@",self.associatedObject_retain); NSLog(@"self.associatedObject_copy:%@",self.associatedObject_copy); } @end

在 ViewController 的 viewDidLoad 方法中,我們對三個屬性進行了賦值,並聲明了三個全局的 __weak 變量來觀察相應對象的釋放時機。此外,我們重寫了 touchesBegan:withEvent: 方法,在方法中分別打印了這三個屬性的當前值。

在繼續閱讀下面章節前,建議讀者先自行思考一下 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 中看到如下的類似輸出:

blob.png

點擊繼續運行按鈕,有一個觀察點將被命中。我們先查看 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 ,進一步驗證一下這個結論。

blob.png

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

blob.png

點擊繼續運行按鈕,最後一個觀察點 string_weak_copy 被命中。同理,self.associatedObject_copy 指向的對象也由於關聯對象的移除被最終釋放。

blob.png

結論

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

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

  2. 一個對象的所有關聯對象是在這個對象被釋放時調用的 _object_remove_assocations 函數中被移除的。

接下來,我們就一起看看 runtime 中的源碼,來驗證下我們的實驗結論。

objc_setAssociatedObject

我們可以在 objc-references.mm 文件中找到 objc_setAssociatedObject 函數最終調用的函數:

  void_object_set_associative_reference(idobject,void*key,idvalue,uintptr_tpolicy){ //retainthenewvalue(ifany)outsidethelock. ObjcAssociationold_association(0,nil); idnew_value=value?acquireValue(value,policy):nil; { AssociationsManagermanager; AssociationsHashMap&associations(manager.associations()); disguised_ptr_tdisguised_object=DISGUISE(object); if(new_value){ //breakanyexistingassociation. AssociationsHashMap::iteratori=associations.find(disguised_object); if(i!=associations.end()){ //secondarytableexists ObjectAssociationMap*refs=i->second; ObjectAssociationMap::iteratorj=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{ //createthenewassociation(firsttime). ObjectAssociationMap*refs=newObjectAssociationMap; associations[disguised_object]=refs; (*refs)[key]=ObjcAssociation(policy,new_value); object->setHasAssociatedObjects(); } }else{ //settingtheassociationtonilbreakstheassociation. AssociationsHashMap::iteratori=associations.find(disguised_object); if(i!=associations.end()){ ObjectAssociationMap*refs=i->second; ObjectAssociationMap::iteratorj=refs->find(key); if(j!=refs->end()){ old_association=j->second; refs->erase(j); } } } } //releasetheoldvalue(outsideofthelock). if(old_association.hasValue())ReleaseValue()(old_association); }

在看這段代碼前,我們需要先了解一下幾個數據結構以及它們之間的關系:

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

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

  3. ObjectAssociationMap 是一個 C++ 中的 map ,維護了從 key 到 ObjcAssociation 的映射,即關聯記錄;

  4. ObjcAssociation 是一個 C++ 的類,表示一個具體的關聯結構,主要包括兩個實例變量,_policy 表示關聯策略,_value 表示關聯對象。

每一個對象地址對應一個 ObjectAssociationMap 對象,而一個 ObjectAssociationMap 對象保存著這個對象的若干個關聯記錄。

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

chart.jpg

objc_getAssociatedObject

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

  id_object_get_associative_reference(idobject,void*key){ idvalue=nil; uintptr_tpolicy=OBJC_ASSOCIATION_ASSIGN; { AssociationsManagermanager; AssociationsHashMap&associations(manager.associations()); disguised_ptr_tdisguised_object=DISGUISE(object); AssociationsHashMap::iteratori=associations.find(disguised_object); if(i!=associations.end()){ ObjectAssociationMap*refs=i->second; ObjectAssociationMap::iteratorj=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 函數後,objc_getAssociatedObject 函數對我們來說就是小菜一碟了。這個函數先根據對象地址在 AssociationsHashMap 中查找其對應的 ObjectAssociationMap 對象,如果能找到則進一步根據 key 在 ObjectAssociationMap 對象中查找這個 key 所對應的關聯結構 ObjcAssociation ,如果能找到則返回 ObjcAssociation 對象的 value 值,否則返回 nil 。

objc_removeAssociatedObjects

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

  void_object_remove_assocations(idobject){ vectorelements; { AssociationsManagermanager; AssociationsHashMap&associations(manager.associations()); if(associations.size()==0)return; disguised_ptr_tdisguised_object=DISGUISE(object); AssociationsHashMap::iteratori=associations.find(disguised_object); if(i!=associations.end()){ //copyalloftheassociationsthatneedtoberemoved. ObjectAssociationMap*refs=i->second; for(ObjectAssociationMap::iteratorj=refs->begin(),end=refs->end();j!=end;++j){ elements.push_back(j->second); } //removethesecondarytable. deleterefs; associations.erase(i); } } //thecallstoreleaseValue()happenoutsideofthelock. for_each(elements.begin(),elements.end(),ReleaseValue()); }

這個函數負責移除一個對象的所有關聯對象,具體實現也是先根據對象的地址獲取其對應的 ObjectAssociationMap 對象,然後將所有的關聯結構保存到一個 vector 中,最終釋放 vector 中保存的所有關聯對象。根據前面的實驗觀察到的情況,在一個對象被釋放時,也正是調用的這個函數來移除其所有的關聯對象。

給類對象添加關聯對象

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

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

總結

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

  1. 關聯對象與被關聯對象本身的存儲並沒有直接的關系,它是存儲在單獨的哈希表中的;

  2. 關聯對象的五種關聯策略與屬性的限定符非常類似,在絕大多數情況下,我們都會使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC 的關聯策略,這可以保證我們持有關聯對象;

  3. 關聯對象的釋放時機與移除時機並不總是一致,比如實驗中用關聯策略 OBJC_ASSOCIATION_ASSIGN 進行關聯的對象,很早就已經被釋放了,但是並沒有被移除,而再使用這個關聯對象時就會造成 Crash 。
 
  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved