關聯(lián)對象底層原理探究

通常我們會在分類中添加方法,而無法在在分類中添加屬性,我們在分類中添加@property(nonatomic, copy) NSString *name;時編譯器并不會在編譯時幫我們自動生成setter和getter方法,也不會生成”_屬性名“的成員變量。

但我們可以通過關聯(lián)對象技術給類添加屬性。例如,我們要給Animal類添加一個name屬性,可以這么實現(xiàn):

@interface Animal (Cate)

@property(nonatomic, copy) NSString *name;

@end

@implementation Animal (Cate)

- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, "name");
}

@end

關聯(lián)對象技術我們使用了使用了兩個api -- objc_setAssociatedObjectobjc_getAssociatedObject,它的底層是如何實現(xiàn)的呢,我們可以通過objc-7.8.1探究。

objc_setAssociatedObject

objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    SetAssocHook.get()(object, key, value, policy);
}

SetAssocHook是一個封裝了一個函數(shù)指針的對象,他在源碼里是這么定義的

static ChainedHookFunction<objc_hook_setAssociatedObject> SetAssocHook{_base_objc_setAssociatedObject};

關于ChainedHookFunction,點進去查看它的實現(xiàn)

// Storage for a thread-safe chained hook function.
// get() returns the value for calling.
// set() installs a new function and returns the old one for chaining.
// More precisely, set() writes the old value to a variable supplied by
// the caller. get() and set() use appropriate barriers so that the
// old value is safely written to the variable before the new value is
// called to use it.
//
// T1: store to old variable; store-release to hook variable
// T2: load-acquire from hook variable; call it; called hook loads old variable

template <typename Fn>
class ChainedHookFunction {
    std::atomic<Fn> hook{nil};

public:
    ChainedHookFunction(Fn f) : hook{f} { };

    Fn get() {
        return hook.load(std::memory_order_acquire);
    }

    void set(Fn newValue, Fn *oldVariable)
    {
        Fn oldValue = hook.load(std::memory_order_relaxed);
        do {
            *oldVariable = oldValue;
        } while (!hook.compare_exchange_weak(oldValue, newValue,
                                             std::memory_order_release,
                                             std::memory_order_relaxed));
    }
};

通過它的注釋可以了解到,ChainedHookFunction是用于線程安全鏈構(gòu)函數(shù)的存儲,通過get()返回調(diào)用值,通過set()安裝一個新的函數(shù),并返回舊函數(shù),更確切的說,set()將舊值寫入調(diào)用方提供的變量,get()set()使用適當?shù)臇艡谑沟迷谛轮嫡{(diào)用前安全地寫入變量。

所以,SetAssocHook.get()返回的是傳入的函數(shù)指針_base_objc_setAssociatedObject,objc_setAssociatedObject底層調(diào)用的其實是_base_objc_setAssociatedObject,我們也可以通過符號斷點驗證調(diào)用的是否正確,這里就不做演示。

進入_base_objc_setAssociatedObject的實現(xiàn)查看,它的底層調(diào)用了_object_set_associative_reference。

static void
_base_objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
  _object_set_associative_reference(object, key, value, policy);
}

_object_set_associative_reference中便有關聯(lián)對象實現(xiàn)的具體代碼

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // rdar://problem/44094390
    if (!object && !value) return;

    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));

    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    association.acquireValue();

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());

        if (value) {
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                /* it's the first association we make */
                object->setHasAssociatedObjects();
            }

            /* establish or replace the association */
            auto &refs = refs_result.first->second;
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else {
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);

                    }
                }
            }
        }
    }

    // release the old value (outside of the lock).
    association.releaseHeldValue();
}

通過閱讀源碼,關聯(lián)對象設值流程為:

  1. 將被關聯(lián)對象封裝成DisguisedPtr類型,將策略和關聯(lián)值封裝成ObjcAssociation類型,并根據(jù)策略處理關聯(lián)值。
  2. 創(chuàng)建一個AssociationsManager管理類
  3. 獲取唯一的全局靜態(tài)哈希Map
  4. 判斷是否插入的關聯(lián)值是否存在,如果存在走第4步,如果不存在則執(zhí)行關聯(lián)對象插入空流程
  5. 創(chuàng)建一個空的ObjectAssociationMap去取查詢的鍵值對
  6. 如果發(fā)現(xiàn)沒有這個key就插入一個空的BucketT進去返回
  7. 標記對象存在關聯(lián)對象
  8. 用當前修飾策略和值組成了一個ObjcAssociation替換原來BucketT中的空
  9. 標記一下ObjectAssociation的第一次為false

關聯(lián)對象插入空流程為:

  1. 根據(jù)DisguisedPtr找到AssociationsHashMap中的迭代查詢器、
  2. 清理迭代器
  3. 插入空值(相當于清除)

處理傳入變量

DisguisedPtr<objc_object> disguised{(objc_object *)object};
ObjcAssociation association{policy, value};

// retain the new value (if any) outside the lock.
association.acquireValue();

這部分代碼比較簡單,可以進入association.acquireValue看看關聯(lián)值是如何處理的。

inline void acquireValue() {
    if (_value) {
        switch (_policy & 0xFF) {
        case OBJC_ASSOCIATION_SETTER_RETAIN:
            _value = objc_retain(_value);
            break;
        case OBJC_ASSOCIATION_SETTER_COPY:
            _value = ((id(*)(id, SEL))objc_msgSend)(_value, @selector(copy));
            break;
        }
    }
}

這份代碼中,如果_policyOBJC_ASSOCIATION_SETTER_RETAIN,則對關聯(lián)值進行retain操作,如果是OBJC_ASSOCIATION_SETTER_COPY,則對關聯(lián)值進行copy操作,其他的則不做任何處理。

創(chuàng)建哈希表管理類獲取全局哈希表

class AssociationsManager {
    using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
    static Storage _mapStorage;

public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }

    AssociationsHashMap &get() {
        return _mapStorage.get();
    }

    static void init() {
        _mapStorage.init();
    }
};

AssociationsManager的構(gòu)造方法里,通過AssociationsManagerLock.lock加了一把鎖,在當前AssociationsManager釋放之前,后續(xù)創(chuàng)建的AssociationsManager都無法對其管理的資源進行操作,從而保證了線程安全,在通過get()拿到全局唯一的哈希表(因為_mapStoragestatic修飾的)

關聯(lián)非空值流程

當要關聯(lián)的值非空時,我們需要將這個值與當前對象關聯(lián)起來,這一部分代碼為

auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
if (refs_result.second) {
    /* it's the first association we make */
    object->setHasAssociatedObjects();
}

/* establish or replace the association */
auto &refs = refs_result.first->second;
auto result = refs.try_emplace(key, std::move(association));
if (!result.second) {
    association.swap(result.first->second);
}

通過源碼,一步步分析它的原理

分析 try_emplace 實現(xiàn)流程

try_emplaceDenseMap類的一個方法,這個方法在這個流程中被調(diào)用兩次,第一次調(diào)用的是全局關聯(lián)對象哈希表的try_emplace,傳了封裝了當前對象的disguised作為key和一個空的ObjectAssociationMap作為第二個元素,第二次,調(diào)用的第一次try_emplace得到的map,傳遞了關聯(lián)對象的key值作為key,傳遞了封裝value和策略的association作為第二個參數(shù)。

try_emplace內(nèi)部到底做了什么事情呢,我們可以去源碼一探究竟。

// Inserts key,value pair into the map if the key isn't already in the map.
// The value is constructed in-place if the key is not in the map, otherwise
// it is not moved.
template <typename... Ts>
std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
BucketT *TheBucket;
if (LookupBucketFor(Key, TheBucket))
  return std::make_pair(
           makeIterator(TheBucket, getBucketsEnd(), true),
           false); // Already in map.

// Otherwise, insert the new element.
TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);
return std::make_pair(
         makeIterator(TheBucket, getBucketsEnd(), true),
         true);
}

try_emplace具體流程分析如下:

  1. 創(chuàng)建一個空的BucketT,調(diào)用LookupBucketFor查找Key對應的BucketT,如果能夠找到,將找到的BucketT賦值給剛剛創(chuàng)建的那個空的BucketT,并將BucketT封裝成DenseMapIterator作為類對的第一個元素,將false作為第二個元素,并將該類對返回,此時的BucketT存放的是上次存放的keyvalue
  2. 如果沒有找到,那么將傳入的Key和Value插入創(chuàng)建新的BucketT,同樣創(chuàng)建一個DenseMapIteratorBool組成的類對,只不過此時傳遞布爾值為true,此時的BucketT已經(jīng)存放了傳入的keyvalue
LookupBucketFor

這個方法是在當前DenseMap中通過key查找對應的bucket,如果找到匹配的,并且bucket包含key和value,則返回true,并將找到bucket通過FoundBucket返回,否則,返回false并通過FoundBucket返回一個空的bucket。

另外貼上代碼

/// LookupBucketFor - Lookup the appropriate bucket for Val, returning it in
/// FoundBucket.  If the bucket contains the key and a value, this returns
/// true, otherwise it returns a bucket with an empty marker or tombstone and
/// returns false.
template<typename LookupKeyT>
bool LookupBucketFor(const LookupKeyT &Val,
                   const BucketT *&FoundBucket) const {
const BucketT *BucketsPtr = getBuckets();
const unsigned NumBuckets = getNumBuckets();

if (NumBuckets == 0) {
  FoundBucket = nullptr;
  return false;
}

// FoundTombstone - Keep track of whether we find a tombstone while probing.
const BucketT *FoundTombstone = nullptr;
const KeyT EmptyKey = getEmptyKey();
const KeyT TombstoneKey = getTombstoneKey();
assert(!KeyInfoT::isEqual(Val, EmptyKey) &&
       !KeyInfoT::isEqual(Val, TombstoneKey) &&
       "Empty/Tombstone value shouldn't be inserted into map!");

unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);
unsigned ProbeAmt = 1;
while (true) {
  const BucketT *ThisBucket = BucketsPtr + BucketNo;
  // Found Val's bucket?  If so, return it.
  if (LLVM_LIKELY(KeyInfoT::isEqual(Val, ThisBucket->getFirst()))) {
    FoundBucket = ThisBucket;
    return true;
  }

  // If we found an empty bucket, the key doesn't exist in the set.
  // Insert it and return the default value.
  if (LLVM_LIKELY(KeyInfoT::isEqual(ThisBucket->getFirst(), EmptyKey))) {
    // If we've already seen a tombstone while probing, fill it in instead
    // of the empty bucket we eventually probed to.
    FoundBucket = FoundTombstone ? FoundTombstone : ThisBucket;
    return false;
  }

  // If this is a tombstone, remember it.  If Val ends up not in the map, we
  // prefer to return it than something that would require more probing.
  // Ditto for zero values.
  if (KeyInfoT::isEqual(ThisBucket->getFirst(), TombstoneKey) &&
      !FoundTombstone)
    FoundTombstone = ThisBucket;  // Remember the first tombstone found.
  if (ValueInfoT::isPurgeable(ThisBucket->getSecond())  &&  !FoundTombstone)
    FoundTombstone = ThisBucket;

  // Otherwise, it's a hash collision or a tombstone, continue quadratic
  // probing.
  if (ProbeAmt > NumBuckets) {
    FatalCorruptHashTables(BucketsPtr, NumBuckets);
  }
  BucketNo += ProbeAmt++;
  BucketNo &= (NumBuckets-1);
}
}

非空值流程流程分析

通過閱讀源碼,可以總結(jié)出他的流程為:

  1. 嘗試通過關聯(lián)對象全局hashMap的try_emplace方法找到當前對象對應的bucket,此時的bucketkey為當前對象,value為一張ObjectAssociationMap,也是一張hashMap
  2. 如果查找返回的refs_result類對第二個元素為true,也就是說當前對象第一次有關聯(lián)對象,將當前對象標記為有關聯(lián)對象(修改isa)
  3. 通過refs_result.first->second拿到當前對象對應ObjectAssociationMap,調(diào)用這張ObjectAssociationMaptry_emplace找到關聯(lián)對象標識符對應的value,也就是valuepolicy組裝成的ObjcAssociation對象
  4. 如果第二個try_emplace方法返回的result的第二個元素為true說明這是該對象第一次插入該標識符的值,此時的valuepolicy已經(jīng)在try_emplace插入到ObjectAssociationMap,不需要進一步處理
  5. 如果result.secondfalse,說明原先的對象的該標識符對應的關聯(lián)對象有值,調(diào)用association.swap(result.first->second)交換修改關聯(lián)對象(result.first->second存放的是valuepolicy組裝成的ObjcAssociation對象)

關聯(lián)空值流程

關聯(lián)空值其實就是刪除關聯(lián)對象,這部分代碼為:

auto refs_it = associations.find(disguised);
if (refs_it != associations.end()) {
    auto &refs = refs_it->second;
    auto it = refs.find(key);
    if (it != refs.end()) {
        association.swap(it->second);
        refs.erase(it);
        if (refs.size() == 0) {
            associations.erase(refs_it);

        }
    }
}

其實通過非空關鍵對象流程的探究對關聯(lián)對象原理了解,這部分代碼就顯得簡單很多。

  1. 以用當前對象封裝的DisguisedPtr對象為key在全局關聯(lián)對象表associations中查找得到refs_it
  2. 如果refs_it不等于associations的最后一個元素,通過refs_it->second拿到當前對象對象的ObjectAssociationMap對象refs,也就是存放標識符和ObjcAssociation對象it(value和policy)的那張哈希表
  3. 通過標識符找到ObjcAssociation
  4. 如果it不是refs最后一個元素,交換原有標識符對應的關聯(lián)對象,因為傳入的為空,所以交換后標識符對應的對象為空
  5. 調(diào)用refs.erase(it),刪除該標識符對應的bucket
  6. 如果此時對象對應的ObjectAssociationMap大小為0,則刪除該對象對應的關聯(lián)表

objc_getAssociatedObject

objc_getAssociatedObject底層調(diào)用的是_object_get_associative_reference

id
objc_getAssociatedObject(id object, const void *key)
{
    return _object_get_associative_reference(object, key);
}

_object_get_associative_reference

進入_object_get_associative_reference

id
_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            ObjectAssociationMap &refs = i->second;
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                association = j->second;
                association.retainReturnedValue();
            }
        }
    }

    return association.autoreleaseReturnedValue();
}

這個部分代碼比較簡單,大致可以分為以下幾個步驟:

  1. 創(chuàng)建AssociationsManager對象,通過AssociationsManager對象拿到全局唯一的關聯(lián)對象管理表
  2. 以對象為key在關聯(lián)對象表中查找對象的AssociationsHashMap::iterator
  3. 如果找到了,取到iterator的第二個元素,也就是ObjectAssociationMap,用標識符為key在這個ObjectAssociationMap查找
  4. 如果找到了,取找到的refs的第二個元素,也就是set時存放的value(第一個為policy),返回
  5. 以上如果都沒有找到,返回nil

總結(jié)

關聯(lián)對象其實使用了兩張hashMap,可以用一張圖解釋他的原理。

ED603188-5741-48F5-AEFD-77EC8A2553DC.png
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容