關聯物件的實現原理【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的時候不會清理關聯物件,需要手動維護關聯物件的記憶體管理。

相關文章