關聯物件的實現原理【OC】

Ginhhor大帥發表於2019-03-14

前言

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 long		uintptr_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(關聯物件)的關係。

2019-03-14-AssociationedObject-1

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時,會釋放持有的關聯物件。

相關文章