俗話說:“金無足赤,人無完人。”對於每一個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 Leopard和iOS 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
:傳入關聯物件。policy
:objc_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這篇文中的例子來舉例:
1 2 3 4 5 |
// NSObject+AssociatedObject.h @interface NSObject (AssociatedObject) @property (nonatomic, strong) id associatedObject; @end |
1 2 3 4 5 6 7 8 9 10 11 12 |
// NSObject+AssociatedObject.m @implementation NSObject (AssociatedObject) @dynamic associatedObject; - (void)setAssociatedObject:(id)object { objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (id)associatedObject { return objc_getAssociatedObject(self, @selector(associatedObject)); } |
這時我們已經發現associatedObject
這個屬性已經新增至NSObject
的例項中了。並且我們可以通過category指定的getter和setter方法對這個屬性進行存取操作。(注:這裡使用@dynamic
關鍵字是為了告知編譯器:在編譯期不要自動建立實現屬性所用的存取方法。因為對於Associated Objects我們必須手動新增。當然,不寫這個關鍵字,使用同名方法進行override也是可以達到相同效果的。但從編碼規範和優化效率來講,顯式宣告是最好的。)
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用法:
- 為Class新增私有成員:例如在AFNetworking中,在UIImageView裡新增了imageRequestOperation物件,從而保證了非同步載入圖片。
- 為Class新增共有成員:例如在FDTemplateLayoutCell中,使用Associated Objects來快取每個cell的高度(程式碼片段1、程式碼片段2)。通過分配不同的key,在複用cell的時候即時取出,增加效率。
- 建立KVO物件:建議使用category來建立關聯物件作為觀察者。可以參考Objective-C Associated Objects這篇文的例子。
Analysis Source Code
在Objective-C Associated Objects 的實現原理這篇文中,作者有一個例子,作者分析了在Associated Objects中弱引用的區別。其程式碼片段如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#import "ViewController.h" #import "ViewController+AssociatedObjects.h" __weak NSString *string_weak_assign = nil; __weak NSString *string_weak_retain = nil; __weak NSString *string_weak_copy = nil; @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 通過[NSString stringWithFormat:]來持有一個字串物件 self.associatedObject_assign = [NSString stringWithFormat:@"associatedObject_assign"]; self.associatedObject_retain = [NSString stringWithFormat:@"associatedObject_retain"]; self.associatedObject_copy = [NSString stringWithFormat:@"associatedObject_copy"]; // 強調指向各個屬性的指標均為弱型別指標 // 以保證weak、assign型別屬性會被釋放 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 |
在測試時候,我們發現有些情況下不至於導致crash。我猜想可能是因為[NSString stringWithFormat:]
方法的持有字串可能會被編譯器優化成compile-time constant。你可以嘗試著做如下修改:
1 2 3 |
self.associatedObject_assign = @"associatedObject_assign"; self.associatedObject_retain = @"associatedObject_retain"; self.associatedObject_copy = @"associatedObject_copy"; |
你會發現全部正常輸出。因為所有字串都變成了編譯期常量而儲存起來。所以探究方法,應該是講型別更改成NSObject進行試驗。
Setter Source Code
我們一直有個疑問,就是關聯物件是如何儲存的。下面我們看下Runtime的原始碼。
以下原始碼來自於opensource.apple.com的objc4-680.tar.gz。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
class ObjcAssociation { uintptr_t _policy; id _value; public: ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {} ObjcAssociation() : _policy(0), _value(nil) {} uintptr_t policy() const { return _policy; } id value() const { return _value; } bool hasValue() { return _value != nil; } }; class AssociationsHashMap : public unordered_map { public: void *operator new(size_t n) { return ::malloc(n); } void operator delete(void *ptr) { ::free(ptr); } }; class AssociationsManager { static spinlock_t _lock; static AssociationsHashMap *_map; // associative references: object pointer -> PtrPtrHashMap. public: AssociationsManager() { _lock.lock(); } ~AssociationsManager() { _lock.unlock(); } AssociationsHashMap &associations() { if (_map == NULL) _map = new AssociationsHashMap(); return *_map; } }; static id acquireValue(id value, uintptr_t policy) { // 遇見不合法policy或者assign直接返回,也就是說將其他無效policy當做assign處理 switch (policy & 0xFF) { case OBJC_ASSOCIATION_SETTER_RETAIN: return ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain); case OBJC_ASSOCIATION_SETTER_COPY: return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy); } return value; } inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); } void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) { // retain the new value (if any) outside the lock. // 建立一個ObjcAssociation物件 ObjcAssociation old_association(0, nil); // 通過policy為value建立對應屬性,如果policy不存在,則預設為assign id new_value = value ? acquireValue(value, policy) : nil; { // 建立AssociationsManager物件 AssociationsManager manager; // 在manager取_map成員,其實是一個map型別的對映 AssociationsHashMap &associations(manager.associations()); // 建立指標指向即將擁有成員的Class // 至此該類已經包含這個關聯物件 disguised_ptr_t disguised_object = DISGUISE(object); // 以下是記錄強引用型別成員的過程 if (new_value) { // break any existing association. // 在即將擁有成員的Class中查詢是否已經存在改關聯屬性 AssociationsHashMap::iterator i = associations.find(disguised_object); if (i != associations.end()) { // secondary table exists // 當存在時候,訪問這個空間的map ObjectAssociationMap *refs = i->second; // 遍歷其成員對應的key ObjectAssociationMap::iterator j = refs->find(key); if (j != refs->end()) { // 如果存在key,重新更改Key的指向到新關聯屬性 old_association = j->second; j->second = ObjcAssociation(policy, new_value); } else { // 否則以新的key建立一個關聯 (*refs)[key] = ObjcAssociation(policy, new_value); } } else { // create the new association (first time). // key不存在的時候,直接建立關聯 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. // 這種情況是policy不存在或者為assign的時候 // 在即將擁有的Class中查詢是否已經存在Class // 其實這裡的意思就是如果之前有這個關聯物件,並且是非assign形的,直接erase AssociationsHashMap::iterator i = associations.find(disguised_object); if (i != associations.end()) { // 如果有該型別成員檢查是否有key ObjectAssociationMap *refs = i->second; ObjectAssociationMap::iterator j = refs->find(key); if (j != refs->end()) { // 如果有key,記錄舊物件,釋放 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
鎖和一個_map
的雜湊表。這個雜湊表中的鍵為disguised_ptr_t
,在得到這個指標的時候,原始碼中執行了DISGUISE
方法,這個方法的功能是獲得指向self地址的指標,即為指向物件地址的指標。通過地址這個唯一標識,可以找到對應的value,即一個子雜湊表。(@饒志臻 勘誤) - 子雜湊表是
ObjectAssociationMap
,鍵就是我們傳入的Key
,而值是ObjcAssociation
,即這個成員物件。從而維護一個成員的所有屬性。
在每次執行setter方法的時候,我們會逐層遍歷Key,逐層判斷。並且當持有Class有了關聯屬性的時候,在執行成員的Getter方法時,會優先查詢Category中的關聯成員。
這樣會帶來一個問題:如果category中的一個關聯物件與Class中的某個成員同名,雖然key值不一定相同,自身的Class不一定相同,policy也不一定相同,但是我這樣做會直接覆蓋之前的成員,造成無法訪問,但是其內部所有資訊及資料全部存在。例如我們對ViewController
做一個Category,來建立一個叫做view的成員,我們會發現在執行工程的時候,模擬器直接黑屏。
我們在viewDidLoad中下斷點,甚至無法進入debug模式。因為view屬性已經被覆蓋,所以不會繼續進行viewController的生命週期。
這一點很危險,所以我們要杜絕覆蓋Class原來的屬性,這會破壞Class原有的功能。(當然,我是十分不推薦在業務專案中使用Runtime的,因為這樣的程式碼可讀性和維護性太低。)
Getter Source Code & Remove
這兩種方法我們直接看原始碼,在看過Setter中的遍歷巢狀map結構的程式碼片段後,你會很容易理解這兩個方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
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; } void _object_remove_assocations(id object) { vector > 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; // 將所有的關聯成員放到一個vector,然後統一清理 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()); } |
另外,對於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倉庫中。