iOS關聯物件技術原理

大兵布萊恩特0409發表於2018-07-13

iOS 通過 runtime 的 API 可以給分類新增屬性,關聯屬性總共有下邊3個 API

///獲取某個物件的關聯屬性
id objc_getAssociatedObject(id object, const void *key) {
    return _object_get_associative_reference(object, (void *)key);
}

///給某個物件新增關聯屬性
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}

///移除物件所有的關聯屬性
void objc_removeAssociatedObjects(id object) 

複製程式碼

通過 runtime 的原始碼可以看出關聯屬性並沒有新增到 category_t(分類)裡邊,執行時也不會合併到元類物件裡邊,而是儲存在一個全域性的AssociationsManager 裡邊,下邊是這個 AssociationsManager 包含的層級關係.

image.png

所有的關聯屬性 和 獲取關聯屬性 移除關聯屬性都是通過一個 AssociationsManager來操作,類似於 OC 中 NSFileManager 的角色,通過傳遞進來的物件作為地址 取出這個物件所對應的關聯列表,然後通過key 取出這個關聯列表的關聯屬性 ObjcAssociation, ObjcAssociation 包含了關聯策略 和 關聯值.

下邊我會通過解讀原始碼 來分析AssociationsManager是如何要關聯的值和物件建立聯絡的.

AssociationsManager 是一個 C++的類 用來進行對關聯物件的屬性新增 和 查詢 移除等操作


class AssociationsManager {
    static spinlock_t _lock;
    static AssociationsHashMap *_map;               // associative references:  object pointer -> PtrPtrHashMap. 這個_ map 裡邊儲存的有關聯列表
public:
    AssociationsManager()   { _lock.lock(); }
    ~AssociationsManager()  { _lock.unlock(); }
    
    AssociationsHashMap &associations() { //可以看成是隻初始化一次 類似與單例
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

複製程式碼

關聯列表是一個 hashMap 類似於 OC 的 NSDictionary ,其中用 disguised_ptr_t 作為 key , ObjectAssociationMap * 作為一個 value

disguised_ptr_t 是 uintptr_t 的型別

intptr_t 和uintptr_t 型別用來存放指標地址。它們提供了一種可移植且安全的方法宣告指標,而且和系統中使用的指標長度相同,對於把指標轉化成整數形式來說很有用。 可以把disguised_ptr_t理解為一個指標型別的變數


   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); }
    };

複製程式碼

ObjectAssociationMap 也是一個 HashMap 存放的是 一個 void * key 就是關聯屬性時傳進來的 key , ObjcAssociation 存放的關聯屬性策略和值的資訊


    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); }
    };

複製程式碼

ObjcAssociation 關聯屬性資訊類 存放了關聯策略 和 傳遞進來關聯的值 id 型別

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; }
    };

複製程式碼

下邊是對 objc_getAssociatedObject , objc_setAssociatedObject , objc_removeAssociatedObjects 具體實現的分析

objc_setAssociatedObject 新增關聯屬性的 API


void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
   /// 舊的關聯物件 因為關聯屬性時如果傳 nil 可能會替換舊的關聯屬性 ,這就是移除某個關聯屬性時傳 nil 的原因
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
       ///獲取關聯屬性列表 ,取出來的列表是以物件為單位的 ,即某個物件的關聯列表 ,這樣就可以單獨的關聯某個物件的關聯屬性 而不與其他物件隔離開
        AssociationsHashMap &associations(manager.associations()); 
      /// 將要新增關聯屬性的物件產生一個記憶體地址 做 key 儲存 它的關聯屬性
        disguised_ptr_t disguised_object = DISGUISE(object);
      /// 如果要關聯的值不為空 ,不為空時 就需要判斷這個屬性和 key 是不是第一天新增 ,即  void *key, id value 都是第一次傳遞進來 
        if (new_value) {
            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; //取出舊值 後邊對這個舊值進行 release 操作
                   ///將新值存放到 key 對應的字典中去 
                    j->second = ObjcAssociation(policy, new_value);
                } else { ///沒有舊值直接將新值新增到字典裡
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else { 
                  如果 key 物件的字典不存在 就建立一個字典 (hashMap 類似於字典的功能,本文為了方便理解將它稱為字典)
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs; 
              ///將要關聯屬性和策略封裝到一個ObjcAssociation類裡邊 並根據 key 新增到這個字典裡
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
          ///如果新增關聯的屬性為空時 就需要取出之前關聯的值 並把它擦除掉 相當於removeObjectForKey 
        ///還是根據物件記憶體地址找到它的關聯屬性列表 ,然後通過 key 找到它關聯屬性的實體(ObjcAssociation這個類) 最後擦除掉 相當於 free 從記憶體中移除
            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_getAssociatedObject 關聯物件取值的操作


id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN; 
    {
      ///還是通過 AssociationsManager 找到所有關聯物件類別 ,然後通過傳入 object 找到某個物件的關聯列表 ,然後通過 key 找到這個物件關聯屬性列表的某個實體(ObjcAssociation) 最後根據關聯策略返回這個屬性的值 
        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();
                ///取出關聯值和策略 傳送訊息 類似與 [obj retain]
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
            }
        }
    }
   /// 如果這個物件是延時釋放的型別 類似與 OC Array String 這些不是 alloc 來的物件 都要執行 [obj autorelease]來釋放 
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
    }
    return value;
}


複製程式碼

objc_removeAssociatedObjects 移除該物件所有的關聯屬性列表


void _object_remove_assocations(id object) {
    vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > 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);
       ///如果這個物件有關聯的屬性列表 那麼久便利它關聯的屬性列表 然後通過便利將這些關聯內容 一個個從字典裡邊擦除  先擦除物件列表關聯的屬性列表 然後將這個物件關聯屬性的 hashMap 擦除掉 相當於 [dict removeAllObjects] 然後再從全域性 AssociationsManager 移除 這個物件關聯的字典 , 又相當於 從一個全域性大字典裡 把 dict這個物件的小字典 給移除了 
        if (i != associations.end()) {
            // copy all of the associations that need to be removed.
            ObjectAssociationMap *refs = i->second;
            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());
}

複製程式碼

以上程式碼看起來並不難 除了一些 C++ 語法難以理解外 也並不需要完全知道每行程式碼怎麼實現 ,大概思路就是 通過全域性大字典 ,找到某個物件相關的小字典 ,然後這個小字典裡存放了 一個key 對應一個屬性值,最後取出這個管理屬性的策略和值

寫到最後 AssociationsManager 這個類不是一個單例類

class AssociationsManager { static spinlock_t _lock; static AssociationsHashMap *_map; // associative references: object pointer -> PtrPtrHashMap. public: AssociationsManager() { _lock.lock(); } //預設無參建構函式 物件建立時自動呼叫 執行加鎖 這樣多個執行緒訪問 _map 時不會出現問題 ~AssociationsManager() { _lock.unlock(); } //解構函式 物件是否時候進行解鎖的操作

///可以看做單例物件 在 AssociationsManager 建立時候 加鎖 當 AssociationsManager 釋放時候 解鎖 ,防止多執行緒訪問時候 對同一個 _map 多次建立 是一種懶漢模式單例 AssociationsHashMap &associations() { if (_map == NULL) _map = new AssociationsHashMap(); return *_map; } };

它裡邊有個 spinlock_t鎖 對 _map 這個全域性唯一的例項 進行加鎖和解鎖 ,由於懶漢模式的單例 需要在多個執行緒訪問 _map 時候進行加鎖保護

好了,我是大兵布萊恩特,歡迎加入博主技術交流群,iOS 開發交流群

QQ20180712-0.png

相關文章