關聯物件的實現原理【OC】
AssociationedObject多用於在Category中為特定類擴充套件成員變數,也有用於在執行時為某些物件動態建立成員變數。AssociationedObject可以說是一種特殊的成員變數。 這篇文章是來詳細解釋AssociationedObject的實現原理,篇幅較長。
相關方法
objc_AssociationPolicy
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) { OBJC_ASSOCIATION_ASSIGN = 0, OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, OBJC_ASSOCIATION_COPY_NONATOMIC = 3, OBJC_ASSOCIATION_RETAIN = 01401, OBJC_ASSOCIATION_COPY = 01403 }; 複製程式碼
這是在呼叫 objc_setAssociatedObject
時需要用到的引數,用於指定關聯引數的引用策略。
-
OBJC_ASSOCIATION_ASSIGN
將關聯引用描述為弱引用
-
OBJC_ASSOCIATION_RETAIN_NONATOMIC
將關聯引用描述為強引用,並且非原子性。
-
OBJC_ASSOCIATION_COPY_NONATOMIC
將關聯引用描述為拷貝引用,並且非原子性。
-
OBJC_ASSOCIATION_RETAIN
將關聯引用描述為強引用,並且為原子性。
-
OBJC_ASSOCIATION_COPY
將關聯引用表述為拷貝引用,並且為原子性。
這一段比較好理解,與Property一樣,關聯引用也可以設定引用描述。
objc_setAssociatedObject
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1); 複製程式碼
建立和設定一個關聯物件的方法。
將一個選定值(value)通過Key-Value的形式掛載在目標物件(object)上,同時指定關聯的策略(policy),這樣就能生成一個關聯物件。
通過將目標物件(object)上指定的Key對應的值(value)設定nil,即可以將已存在的關聯物件清除。
objc_getAssociatedObject
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1); 複製程式碼
關聯物件值的獲取方法。
通過關聯Key,在目標物件(object)中,獲取到關聯物件的值(返回值)。
objc_removeAssociatedObjects
/** * Removes all associations for a given object. * * @param object An object that maintains associated objects. * * @note The main purpose of this function is to make it easy to return an object *to a "pristine state”. You should not use this function for general removal of *associations from objects, since it also removes associations that other clients *may have added to the object. Typically you should use \c objc_setAssociatedObject *with a nil value to clear an association. * * @see objc_setAssociatedObject * @see objc_getAssociatedObject */ OBJC_EXPORT void objc_removeAssociatedObjects(id object) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1); 複製程式碼
移除目標物件(objct)的所有關聯物件。
官方在這做了提示:這個方法的主要目的是為了讓物件更容易返回到原始狀態, 呼叫此方法會將繫結在這個目標物件上的所有關聯物件都清除 。如果別的開發也為這個object設定了關聯物件,或者你在別的模組中也為其設定了不同的關聯物件,呼叫此方法後會被一併刪除。通常清除關聯物件,請通過objc_setAssociatedObject將關聯物件設定為nil的方式來清除關聯物件。
底層實現
在底層實現上,分為GC版和無GC版,GC是MacOS的垃圾回收機制,不過現在也被棄用了,推薦使用ARC。而iOS上只有MRC和ARC,因此這裡只截取了無GC版的程式碼。
objc_setAssociatedObject
在這個方法中有很多的資料結構,首先介紹下資料結構,幫助理解,也可以先往下翻看主要流程,有看不明白的再回來查詢。
AssociationsManager
// class AssociationsManager manages a lock / hash table singleton pair. // Allocating an instance acquires the lock, and calling its assocations() // method lazily allocates the hash table. spinlock_t AssociationsManagerLock; class AssociationsManager { // associative references: object pointer -> PtrPtrHashMap. static AssociationsHashMap *_map; public: AssociationsManager(){ AssociationsManagerLock.lock(); } ~AssociationsManager(){ AssociationsManagerLock.unlock(); } AssociationsHashMap &associations() { if (_map == NULL) _map = new AssociationsHashMap(); return *_map; } }; AssociationsHashMap *AssociationsManager::_map = NULL; 複製程式碼
AssociationsManager的作用管理lock,當它被建立的時候會加鎖,在它被析構的時候會釋放鎖,同時,有一個全域性變數_map,管理的是目標物件與HashMap(Key-Value都為指標)的的關係。
AssociationsManager中有一個全域性變數型別為AssociationsHashMap的_map引用。
提供了一個建立AssociationsHashMap的左值引用方法,_map是引用,*_map則是解引用又變成了AssociationsHashMap物件,函式前加了&(取地址符),返回的是AssociationsHashMap物件引用地址。
同時聲明瞭一個型別為AssociationsHashMap的全域性變數引用。
AssociationsHashMap
typedef ObjcAllocator<std::pair<const disguised_ptr_t, ObjectAssociationMap*> > AssociationsHashMapAllocator; class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> { public: void *operator new(size_t n) { return ::malloc(n); } void operator delete(void *ptr) { ::free(ptr); } }; //unordered_map的泛型定義 template <class _Key, class _Tp, class _Hash = hash<_Key>, class _Pred = equal_to<_Key>, class _Alloc = allocator<pair<const _Key, _Tp> > > 複製程式碼
AssociationsHashMap
繼承自 unordered_map <disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator>
解釋下這個泛型的意思
- _Key(key_type)對應disguised_ptr_t,disguised_ptr_t實際上是unsigned long型別,這是用來存放指標的。
- _Tp(mapped_type)對應ObjectAssociationMap *,這個表明了Map中儲存的值型別,後面會介紹ObjectAssociationMap。
- _Hash(hasher)對應DisguisedPointerHash,提供hash演算法。
- _Pred(key_equal)對應的是DisguisedPointerEqual,提供equal演算法(兩個指標相同則相等)。
- _Alloc(allocator_type)對應的是建構函式的型別,這裡建構函式型別是ObjcAllocator<std::pair<const disguised_ptr_t, ObjectAssociationMap *> >,也是一個泛型,用std::pair實現Key是disguised_ptr_t,Value是ObjectAssociationMap *的鍵值對。
它實現了建立和刪除的功能。
disguised_ptr_t
typedef unsigned longuintptr_t; typedef uintptr_t disguised_ptr_t; inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); } inline id UNDISGUISE(disguised_ptr_t dptr) { return id(~dptr); } 複製程式碼
這裡可以看出 disguised_ptr_t
實際上是一個unsigned long,它的長度與指標相同,所以被當做指標使用。 ~uintptr_t(value)
:value本身也是個物件指標,將它包裝成unsigned long型別,載逐位取反後返回。 id(~dptr)
:將disguised_ptr_t逐位取反後,返回物件(id)指標。
ObjectAssociationMap
typedef ObjcAllocator<std::pair<void * const, ObjcAssociation> > ObjectAssociationMapAllocator; class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> { public: void *operator new(size_t n) { return ::malloc(n); } void operator delete(void *ptr) { ::free(ptr); } }; //std::map的泛型定義 template <class _Key, class _Tp, class _Compare = less<_Key>, class _Allocator = allocator<pair<const _Key, _Tp> > > 複製程式碼
與AssociationsHashMap類似, ObjectAssociationMap
繼承自 public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator>
泛型的意思是:
-
_key(key_type)對應的是void *,即無型別指標,表示記憶體地址。
-
_Tp(mapped_type)對應[ObjcAssociation](#### ObjcAssociation),這個上面提到過,用來存放關聯物件的值與關聯策略。
-
_Compare(key_compare)對應的是ObjectPointerLess,提供比較演算法(比對指標地址)。
-
_Allocator(allocator_type)對應的是ObjcAllocator<std::pair<void * const, ObjcAssociation> >,用std::pair實現Key是void * const(記憶體地址),Value是ObjcAssociation的鍵值對。
ObjcAssociation
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; } }; 複製程式碼
這是關聯物件的結構,儲存著關聯策略 _policy
和關聯物件的值 _value
。
acquireValue
static id acquireValue(id value, uintptr_t policy) { switch (policy & 0xFF) { case OBJC_ASSOCIATION_SETTER_RETAIN: return objc_retain(value); case OBJC_ASSOCIATION_SETTER_COPY: return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy); } return value; } 複製程式碼
判斷引用策略,如果為Retian的,則會呼叫 objc_retain
將value的引用計數加一,如果是Copy,則會呼叫Value的copy方法生成新的拷貝,其他的策略不作處理。
setHasAssociatedObjects
inline void objc_object::setHasAssociatedObjects() { if (isTaggedPointer()) return; retry: isa_t oldisa = LoadExclusive(&isa.bits); isa_t newisa = oldisa; if (!newisa.nonpointer||newisa.has_assoc) { ClearExclusive(&isa.bits); return; } newisa.has_assoc = true; if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry; } 複製程式碼
這裡主要做的是將 isa
中的 has_assoc
標誌位設定為true。
Tagged Pointer
這裡涉及到了Tagged Pointer的概念,Tagged Pointer是一種為了節約記憶體佔用的策略,它的原理是將指標物件的資料直接存於指標中,在64位系統中被引入。
比如,在64位系統中,一個指標為8位元組,當指標關聯的值小於8位的時候,系統會將指標轉化成Tagged Pointer,並在最後一個bit位加入TaggedPoint標識,這個指標的結構就變成了“0x儲存資料+TaggedPoint標識”的結構。
0xb000000000000032 |---------------|-| 0x|----儲存資料----|標識| 複製程式碼
這麼做的好處是,減少了記憶體的佔用,又因為資料不需要放入堆中,所以不需要malloc和free,所以讀取和執行速度都得到了提升。
這個時候指標已經變成了一個值,而不再是一個地址,所以它也沒有isa指標,所以要先做判斷。
ReleaseValue 和 releaseValue
struct ReleaseValue { void operator() (ObjcAssociation &association) { releaseValue(association.value(), association.policy()); } }; static void releaseValue(id value, uintptr_t policy) { if (policy & OBJC_ASSOCIATION_SETTER_RETAIN) { return objc_release(value); } } 複製程式碼
這裡是關聯物件釋放的相關程式碼,如果引用策略為Retian的話,釋放時,會將引用計數減一。
主要實現
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) { // 建立一個 ObjcAssociation物件,初始化policy為OBJC_ASSOCIATION_ASSIGN,value為nil。 ObjcAssociation old_association(0, nil); // 根據引用策略處理value。如果Copy,呼叫value的copy方法獲取new_value,如果是Retain的,則持有value,value的引用計數加一,其他策略不作處理。 id new_value = value ? acquireValue(value, policy) : nil; { // 建立一個AssociationsManager,在初始化時會加鎖,在析構時會解鎖。 // C++會預設為物件進行初始化,即已經加鎖。 AssociationsManager manager; // 獲取AssociationsManager中全域性變數AssociationsHashMap的引用, // 呼叫AssociationsHashMap的拷貝建構函式 // 用上面的引用直接初始化新的associations變數。 AssociationsHashMap &associations(manager.associations()); // 建立一個disguised_ptr_t,呼叫DISGUISE(object),使用目標物件初始化。 disguised_ptr_t disguised_object = DISGUISE(object); // 如果new_value 不為nil,情景是設定新的關聯值、或者修改關聯值。 if (new_value) { // 在AssociationsHashMap中通過迭代器查詢, // 與disguised_ptr_t相對應的ObjectAssociationMap*(i) // 注:map的結構為std::pair<const disguised_ptr_t, ObjectAssociationMap*>。 AssociationsHashMap::iterator i = associations.find(disguised_object); // 如果查詢到Map中對應的ObjectAssociationMap*(i),即修改關聯值的情況。 // 注:在C++的map中,如果未查詢到元素迭代器會返回map.end(); if (i != associations.end()) { // 將ObjectAssociationMap*(refs) // 指向AssociationsHashMap(i)的second, // 這是Map的Value。 ObjectAssociationMap *refs = i->second; // 在ObjectAssociationMap*(refs)中通過key, // 來查詢對應存在的ObjectAssociationMap物件(j)。 ObjectAssociationMap::iterator j = refs->find(key); // 如果查詢到了對應的ObjcAssociation(j)。 // 注:map的結構為std::pair<void * const, ObjcAssociation> if (j != refs->end()) { // 將ObjectAssociationMap的value, // 即ObjcAssociation,賦值給old_association old_association = j->second; // 將這個Map的Value更新為新構建的ObjcAssociation。 j->second = ObjcAssociation(policy, new_value); } else { // 如果j是容器唯一的元素, // 為ObjectAssociationMap的引用(*refs)設定一個Map, // key為傳入的key,Value為新構建的ObjcAssociation。 (*refs)[key] = ObjcAssociation(policy, new_value); } } else { // 如果Object是容器裡唯一的元素 // 建立一個ObjectAssociationMap ObjectAssociationMap *refs = new ObjectAssociationMap; // 在AssociationsHashMap(associations)中, // 以Key為disguised_object儲存。 associations[disguised_object] = refs; // 為ObjectAssociationMap建立,Key為傳入值key, // value為新構建的ObjcAssociation。 (*refs)[key] = ObjcAssociation(policy, new_value); // 呼叫關聯目標物件的setHasAssociatedObjects方法。 // 將物件的isa中的has_assoc設定為true // 即標識這個object有關聯物件存在。 // 這個方法位於objc-objct內。 object->setHasAssociatedObjects(); } } else { // 當new_value = nil時, // 通過disguised_object查詢對應的ObjectAssociationMap(i) AssociationsHashMap::iterator i = associations.find(disguised_object); // 如果查到了對應的ObjectAssociationMap(i) if (i !=associations.end()) { // 取出ObjectAssociationMap *refs ObjectAssociationMap *refs = i->second; // 通過傳入的key查詢對應的ObjcAssociation(j) ObjectAssociationMap::iterator j = refs->find(key); // 如果查到了對應的ObjcAssociation(j) if (j != refs->end()) { // 將舊值取出,賦值給old_association。 old_association = j->second; // 將ObjcAssociation(j)從ObjectAssociationMap中移除。 refs->erase(j); } } } //到這裡釋放鎖。 } // 如果存在舊值,則將舊值釋放。 if (old_association.hasValue()) ReleaseValue()(old_association); } 複製程式碼
關聯物件的儲存結構
看到這裡可以總結一下關聯關係的儲存結構了。
AssociationsHashMap是管理目標物件(object)與ObjectAssociationMap的關係
ObjectAssociationMap是管理Key與ObjectAssociation(關聯物件)的關係。

objc_getAssociatedObject
下面分析的是關於關聯物件的取值邏輯,大部分結構體部分在設定值部分都說了,又遇到不理解的回頭去看,這裡直接將主要實現了。
主要實現
id _object_get_associative_reference(id object, void *key) { // 建立一個value。 id value = nil; // 預設引用策略為assign。 uintptr_t policy = OBJC_ASSOCIATION_ASSIGN; { // 建立AssociationsManager並加鎖。 AssociationsManager manager; // 獲取全域性AssociationsHashMap。 AssociationsHashMap &associations(manager.associations()); // 通過object構建disguised_object。 disguised_ptr_t disguised_object = DISGUISE(object); // 根據disguised_object查詢對應的ObjectAssociationMap。 AssociationsHashMap::iterator i = associations.find(disguised_object); // 如果查到了ObjectAssociationMap。 if (i != associations.end()) { // 提取ObjectAssociationMap。 ObjectAssociationMap *refs = i->second; // 根據key查詢對應的ObjcAssociation。 ObjectAssociationMap::iterator j = refs->find(key); // 如果ObjcAssociation存在。 if (j != refs->end()) { // 獲取ObjcAssociation ObjcAssociation &entry = j->second; // 將value設定為ObjcAssociation的value。 value = entry.value(); // 將policy設定為ObjcAssociation的policy。 policy = entry.policy(); // 如果引用策略是Retain模式 if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) { // 呼叫value的retian方法,引用計數加一。 objc_retain(value); } } } //釋放鎖 } // 如果值存在,並且引用策略是AutoRelease模式。 if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) { // 呼叫value的autorelease方法,將value設定為autorelease。 objc_autorelease(value); } // 返回取到的關聯物件 return value; } 複製程式碼
objc_retain
id objc_retain(id obj) { if (!obj) return obj; if (obj->isTaggedPointer()) return obj; return obj->retain(); } 複製程式碼
在retian的時候也判斷了是否為Tagged Pointer。
這裡會呼叫一次obj的retian方法,引用計數會加一。
objec_autorelease
id objc_autorelease(id obj) { if (!obj) return obj; if (obj->isTaggedPointer()) return obj; return obj->autorelease(); } 複製程式碼
與retian幾乎相同,呼叫的是obj的autorelease方法。
objc_removeAssociatedObjects
void _object_remove_assocations(id object) { // 宣告一個vector容器,key型別是ObjcAssociation,value型別是ObjcAllocator<ObjcAssociation>,這是一個構造器結構體,在runtime時內部使用。 // vector在C++中是一個動態長度的順序容器。 vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements; { //建立一個AssociationsManager並加鎖。 AssociationsManager manager; // 獲取AssociationsHashMap。 AssociationsHashMap &associations(manager.associations()); // 如果AssociationsHashMap中沒有沒有元素,則結束流程。 if (associations.size() == 0) return; // 通過object構造disguised_object。 disguised_ptr_t disguised_object = DISGUISE(object); // 在AssociationsHashMap中查詢disguised_object對應的ObjectAssociationMap。 AssociationsHashMap::iterator i = associations.find(disguised_object); // 如果ObjectAssociationMap存在。 if (i != associations.end()) { // 獲得ObjectAssociationMap。 ObjectAssociationMap *refs = i->second; // 迴圈迭代ObjectAssociationMap for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) { //將所有的ObjectAssociation都存入vector中。 elements.push_back(j->second); } // 釋放ObjectAssociationMap的記憶體空間。 delete refs; // 在AssociationsHashMap中刪除這個ObjectAssociationMap元素。 associations.erase(i); } } // 迴圈釋放vector中儲存的每一個ObjectAssociation for_each(elements.begin(), elements.end(), ReleaseValue()); } 複製程式碼
生命週期
//檔案:runtime.h /** * Destroys an instance of a class without freeing memory and removes any * associated references this instance might have had. * 摧毀一個例項物件,不會釋放和移除任何此例項物件的關聯物件。 * * @param obj The class instance to destroy. * * @return \e obj. Does nothing if \e obj is nil. * * @note CF and other clients do call this under GC. */ OBJC_EXPORT void * _Nullable objc_destructInstance(id _Nullable obj) OBJC_AVAILABLE(10.6, 3.0, 9.0, 1.0, 2.0) OBJC_ARC_UNAVAILABLE; /*********************************************************************** * object_dispose * fixme * Locking: none 檔案:objc-runtime-new.m **********************************************************************/ id object_dispose(id obj) { if (!obj) return nil; objc_destructInstance(obj); free(obj); return nil; } // ===============檔案:objc-private.h ======================== inline void objc_object::rootDealloc() { if (isTaggedPointer()) return;// fixme necessary? if (fastpath(isa.nonpointer&& !isa.weakly_referenced&& !isa.has_assoc&& !isa.has_cxx_dtor&& !isa.has_sidetable_rc)) { assert(!sidetable_present()); free(this); } else { object_dispose((id)this); } } // ===============檔案:NSObject.mm ======================== void _objc_rootDealloc(id obj) { assert(obj); obj->rootDealloc(); } // Replaced by NSZombies - (void)dealloc { _objc_rootDealloc(self); } 複製程式碼
從上面的程式碼可以看出,當NSObject執行dealloc的時候,並不會清理掉關聯物件,所以關聯物件是需要我們手動維護的,用完及時清理,避免出現記憶體洩露。
總結
- 可以看出Runtime對於關聯物件的管理都是執行緒安全的,增刪查改都是加鎖的。
- 在App執行期間,由AssociationsHashMap來管理所有被新增到物件中的關聯物件。
- NSObject在dealloc的時候不會清理關聯物件,需要手動維護關聯物件的記憶體管理。