原文連接:http://blog.leichunfeng.com/blog/2015/06/26/objective-c-associated-objects-implementation-principle/ git
咱們知道,在 Objective-C 中能夠經過 Category 給一個現有的類添加屬性,可是卻不能添加實例變量,這彷佛成爲了 Objective-C 的一個明顯短板。然而值得慶幸的是,咱們能夠經過 Associated Objects 來彌補這一不足。github
在閱讀本文的過程當中,讀者須要着重關注如下三個問題:objective-c
關聯對象被存儲在什麼地方,是否是存放在被關聯對象自己的內存中?ide
關聯對象的五種關聯策略有什麼區別,有什麼坑?函數
關聯對象的生命週期是怎樣的,何時被釋放,何時被移除?測試
與 Associated Objects 相關的函數主要有三個,咱們能夠在 runtime 源碼的 runtime.h 文件中找到它們的聲明:ui
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
OBJC_EXPORT void objc_removeAssociatedObjects(id object)
這三個函數的命名很容易看懂:atom
objc_setAssociatedObject 用於給對象添加關聯對象,傳入 nil 則能夠移除已有的關聯對象;code
objc_getAssociatedObject 用於獲取關聯對象;對象
objc_removeAssociatedObjects 用於移除一個對象的全部關聯對象。
注:objc_removeAssociatedObjects 函數咱們通常是用不上的,由於這個函數會移除一個對象的全部關聯對象,將該對象恢復成「原始」狀態。這樣作就頗有可能把別人添加的關聯對象也一併移除,這並非咱們所但願的。因此通常的作法是經過給 objc_setAssociatedObject 函數傳入 nil 來移除某個已有的關聯對象。
Key值:
關於前兩個函數中的 key 值是咱們須要重點關注的一個點,這個 key 值必須保證是一個對象級別(爲何是對象級別?看完下面的章節你就會明白了)的惟一常量。通常來講,有如下三種推薦的 key 值:
聲明 static char kAssociatedObjectKey; ,使用 &kAssociatedObjectKey 做爲 key 值;
聲明 static void *kAssociatedObjectKey = &kAssociatedObjectKey; ,使用 kAssociatedObjectKey 做爲 key 值;
用 selector ,使用 getter 方法的名稱做爲 key 值。
推薦使用第三種,好用,省代碼量。
關聯策略
關聯策略 等價屬性 說明OBJC_ASSOCIATION_ASSIGN @property (assign) or 弱引用關聯對象
@property (unsafe_unretained)
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (strong, nonatomic) 強引用關聯對象,且爲非原子操做
OBJC_ASSOCIATION_COPY_NONATOMIC @property (copy, nonatomic) 複製關聯對象,且爲非原子操做
OBJC_ASSOCIATION_RETAIN @property (strong, atomic) 強引用關聯對象,且爲原子操做
OBJC_ASSOCIATION_COPY @property (copy, atomic) 複製關聯對象,且爲原子操做
原子性問題不在這裏討論,下面主要討論前三種形式:
實現原理
代碼Github原文連接:https://github.com/leichunfeng/AssociatedObjects
從測試代碼中能夠看出:
關聯對象的釋放時機與被移除的時機並不老是一致的,好比上面的 self.associatedObject_assign 所指向的對象在 ViewController 出現後就被釋放了,可是 self.associatedObject_assign 仍然有值,仍是保存的原對象的地址。若是以後再使用 self.associatedObject_assign 就會形成 Crash ,因此咱們在使用弱引用的關聯對象時要很是當心;
一個對象的全部關聯對象是在這個對象被釋放時調用的 _object_remove_assocations 函數中被移除的。
打開 objc-references.mm,找到 objc_setAssociatedObject 函數最終調用的函數;
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); }
在 objc-references.mm 的 objc_getAssociatedObject 函數最終調用了的函數:
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_removeAssociatedObjects
這個函數負責移除一個對象的全部關聯對象,具體實現也是先根據對象的地址獲取其對應的 ObjectAssociationMap 對象,而後將全部的關聯結構保存到一個 vector 中,最終釋放 vector 中保存的全部關聯對象。根據前面的實驗觀察到的狀況,在一個對象被釋放時,也正是調用的這個函數來移除其全部的關聯對象。
給類對象添加關聯對象
看完源代碼後,咱們知道對象地址與 AssociationsHashMap 哈希表是一一對應的。那麼咱們可能就會思考這樣一個問題,是否能夠給類對象添加關聯對象呢?答案是確定的。咱們徹底能夠用一樣的方式給類對象添加關聯對象,只不過咱們通常狀況下不會這樣作,由於更多時候咱們能夠經過 static 變量來實現類級別的變量。我在分類 ViewController+AssociatedObjects 中給 ViewController 類對象添加了一個關聯對象 associatedObject 。
總結
關聯對象與被關聯對象自己的存儲並無直接的關係,它是存儲在單獨的哈希表中的;
關聯對象的五種關聯策略與屬性的限定符很是相似,在絕大多數狀況下,咱們都會使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC 的關聯策略,這能夠保證咱們持有關聯對象;
關聯對象的釋放時機與移除時機並不老是一致,好比實驗中用關聯策略 OBJC_ASSOCIATION_ASSIGN 進行關聯的對象,很早就已經被釋放了,可是並無被移除,而再使用這個關聯對象時就會形成 Crash 。
四、demo在個人github有一個例子,是將UIAlertView的delegate實現點擊事件回調改爲在初始化的時候直接傳入,在category中使用本博客中的方法實現block保存以及調用。
https://github.com/caijunrong/JRAlertView.git