淺談Associated Objects

發表於2016-08-02

俗話說:“金無足赤,人無完人。”對於每一個Class也是這樣,儘管我們說這個Class的程式碼規範、邏輯清晰合理等等,但是總會有它的短板,或者隨著需求演進而無法訂製實現功能。於是在Objective-C 2.0中引入了category這個特性,用以動態地為已有類新增新行為。物件導向的設計用來描述事物的組成往往是使用Class中的屬性成員,這也就侷限了方法的廣度(在官方文件稱之為An otherwise notable shortcoming for Objective-C,譯為:Objc的一個顯著缺陷)。所以在Runtime中引入了Associated Objects來彌補這一缺陷。

另外,請帶著以下疑問來閱讀此文:

  • Associated Objects 使用場景。
  • Associated Objects 五種objc_AssociationPolicy有什麼區別。
  • Associated Objects 的儲存結構。

Associated Objects Introduction

Associated Objects是Objective-C 2.0中Runtime的特性之一。最早開始使用是在OS X Snow LeopardiOS 4中。在中定義的三個方法,也是我們深入探究Associated Objects的突破口:

  • objc_setAssociatedObject
  • objc_getAssociatedObject
  • objc_removeAssociatedObjects

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

  • object:傳入關聯物件的所屬物件,也就是增加成員的例項物件,一般來說傳入self。
  • key:一個唯一標記。在官方文件中推薦使用static char,當然更推薦是指標。為了便捷,一般使用selector,這樣在後面getter中,我們就可以利用_cmd來方便的取出selector
  • value:傳入關聯物件。
  • policyobjc_AssociationPolicy是一個ObjC列舉型別,也代表關聯策略。

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

void objc_removeAssociatedObjects(id object)

從引數型別引數型別上,我們可以輕易的得出getter和remove方法傳入引數的含義。要注意的是,objc_removeAssociatedObjects這個方法會移除一個物件的所有關聯物件。其實,該方法我們一般是用不到的,移除所有關聯意味著將類恢復成無任何關聯的原始狀態,這不是我們希望的。所以一般的做法是通過objc_setAssociatedObject來傳入nil,從而移除某個已有的關聯物件。

我們用Associated Objects這篇文中的例子來舉例:

這時我們已經發現associatedObject這個屬性已經新增至NSObject的例項中了。並且我們可以通過category指定的getter和setter方法對這個屬性進行存取操作。(注:這裡使用@dynamic關鍵字是為了告知編譯器:在編譯期不要自動建立實現屬性所用的存取方法。因為對於Associated Objects我們必須手動新增。當然,不寫這個關鍵字,使用同名方法進行override也是可以達到相同效果的。但從編碼規範和優化效率來講,顯式宣告是最好的。)

11208988-10a9d08b532258d3

AssociationPolicy

通過上面的例子,我們注意到了OBJC_ASSOCIATION_RETAIN_NONATOMIC這個引數,它的列舉型別各個元素的含義如下:

BEHAVIOR @PROPERTY EQUIVALENT DESCRIPTION
OBJC_ASSOCIATION_ASSIGN @property (assign) 或 @property (unsafe_unretained) 指定一個關聯物件的弱引用。
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (nonatomic, strong) 指定一個關聯物件的強引用,不能被原子化使用。
OBJC_ASSOCIATION_COPY_NONATOMIC @property (nonatomic, copy) 指定一個關聯物件的copy引用,不能被原子化使用。
OBJC_ASSOCIATION_RETAIN @property (atomic, strong) 指定一個關聯物件的強引用,能被原子化使用。
OBJC_ASSOCIATION_COPY @property (atomic, copy) 指定一個關聯物件的copy引用,能被原子化使用。
OBJC_ASSOCIATION_GETTER_AUTORELEASE 自動釋放型別

OBJC_ASSOCIATION_ASSIGN型別的關聯物件和weak有一定差別,而更加接近於unsafe_unretained,即當目標物件遭到摧毀時,屬性值不會自動清空。(翻譯自Associated Objects

Usage Sample

同樣是Associated Objects文中,總結了三個關於Associated Objects用法:

Analysis Source Code

Objective-C Associated Objects 的實現原理這篇文中,作者有一個例子,作者分析了在Associated Objects中弱引用的區別。其程式碼片段如下:

在測試時候,我們發現有些情況下不至於導致crash。我猜想可能是因為[NSString stringWithFormat:]方法的持有字串可能會被編譯器優化成compile-time constant。你可以嘗試著做如下修改:

你會發現全部正常輸出。因為所有字串都變成了編譯期常量而儲存起來。所以探究方法,應該是講型別更改成NSObject進行試驗。

Setter Source Code

我們一直有個疑問,就是關聯物件是如何儲存的。下面我們看下Runtime的原始碼。

以下原始碼來自於opensource.apple.comobjc4-680.tar.gz

我們讀過程式碼後發現是其儲存結構是這樣的一個邏輯:

12208988-67f51f426f98ce53
  • 橙色的是AssociationsManager是頂級結構體,維護了一個spinlock_t鎖和一個_map的雜湊表。這個雜湊表中的鍵為disguised_ptr_t,在得到這個指標的時候,原始碼中執行了DISGUISE方法,這個方法的功能是獲得指向self地址的指標,即為指向物件地址的指標。通過地址這個唯一標識,可以找到對應的value,即一個子雜湊表。(@饒志臻 勘誤)
  • 子雜湊表是ObjectAssociationMap,鍵就是我們傳入的Key,而值是ObjcAssociation,即這個成員物件。從而維護一個成員的所有屬性。

在每次執行setter方法的時候,我們會逐層遍歷Key,逐層判斷。並且當持有Class有了關聯屬性的時候,在執行成員的Getter方法時,會優先查詢Category中的關聯成員。

這樣會帶來一個問題:如果category中的一個關聯物件與Class中的某個成員同名,雖然key值不一定相同,自身的Class不一定相同,policy也不一定相同,但是我這樣做會直接覆蓋之前的成員,造成無法訪問,但是其內部所有資訊及資料全部存在。例如我們對ViewController做一個Category,來建立一個叫做view的成員,我們會發現在執行工程的時候,模擬器直接黑屏。

13208988-97d8f5bde8f5de41

我們在viewDidLoad中下斷點,甚至無法進入debug模式。因為view屬性已經被覆蓋,所以不會繼續進行viewController的生命週期。

14208988-12aa766163679316

這一點很危險,所以我們要杜絕覆蓋Class原來的屬性,這會破壞Class原有的功能。(當然,我是十分不推薦在業務專案中使用Runtime的,因為這樣的程式碼可讀性和維護性太低。)

Getter Source Code & Remove

這兩種方法我們直接看原始碼,在看過Setter中的遍歷巢狀map結構的程式碼片段後,你會很容易理解這兩個方法。

另外,對於remove有一點補充。在Runtime的銷燬物件函式objc_destructInstance裡面會判斷這個物件有沒有關聯物件,如果有,會呼叫_object_remove_assocations做關聯物件的清理工作。

Thinking About Hash Table

不光是本文講述的關於Class關聯物件的儲存方式,還是Apple中其他的Souce Code(例如引用計數管理),我們能感受到Apple對Hash Table(本文中的map資料結構)這種資料結構情有獨鍾。在大量的實踐中可以說明,Hash Table對於優化效率的提升,這是毋庸置疑的。

細究使用這種資料結構的原因,唯一的Key可對應指定的Value。我們從計算機儲存的角度考慮,因為每個記憶體地址是唯一的,也就可以假象成Key,通過唯一的Key來讀寫資料,這是效率最高的方式。

The End

通過閱讀此文,想必你已經知道那三個問題的答案。筆者原本想對UITableView-FDTemplateLayoutCell進行原始碼分析來撰寫一篇文,但是發現裡面儲存cell的Key值使用到了Associated Objects該技術,所以對此進行了學習探究。後面,我會分析一下UITableView-FDTemplateLayoutCell的原始碼,這些將收錄在我的這個Github倉庫中