关联对象底层原理探究

通常我们会在分类中添加方法,而无法在在分类中添加属性,我们在分类中添加@property(nonatomic, copy) NSString *name;时编译器并不会在编译时帮我们自动生成setter和getter方法,也不会生成”_属性名“的成员变量。

但我们可以通过关联对象技术给类添加属性。例如,我们要给Animal类添加一个name属性,可以这么实现:

@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

关联对象技术我们使用了使用了两个api -- objc_setAssociatedObjectobjc_getAssociatedObject,它的底层是如何实现的呢,我们可以通过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是一个封装了一个函数指针的对象,他在源码里是这么定义的

static ChainedHookFunction<objc_hook_setAssociatedObject> SetAssocHook{_base_objc_setAssociatedObject};

关于ChainedHookFunction,点进去查看它的实现

// 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是用于线程安全链构函数的存储,通过get()返回调用值,通过set()安装一个新的函数,并返回旧函数,更确切的说,set()将旧值写入调用方提供的变量,get()set()使用适当的栅栏使得在新值调用前安全地写入变量。

所以,SetAssocHook.get()返回的是传入的函数指针_base_objc_setAssociatedObjectobjc_setAssociatedObject底层调用的其实是_base_objc_setAssociatedObject,我们也可以通过符号断点验证调用的是否正确,这里就不做演示。

进入_base_objc_setAssociatedObject的实现查看,它的底层调用了_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中便有关联对象实现的具体代码

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

通过阅读源码,关联对象设值流程为:

  1. 将被关联对象封装成DisguisedPtr类型,将策略和关联值封装成ObjcAssociation类型,并根据策略处理关联值。
  2. 创建一个AssociationsManager管理类
  3. 获取唯一的全局静态哈希Map
  4. 判断是否插入的关联值是否存在,如果存在走第4步,如果不存在则执行关联对象插入空流程
  5. 创建一个空的ObjectAssociationMap去取查询的键值对
  6. 如果发现没有这个key就插入一个空的BucketT进去返回
  7. 标记对象存在关联对象
  8. 用当前修饰策略和值组成了一个ObjcAssociation替换原来BucketT中的空
  9. 标记一下ObjectAssociation的第一次为false

关联对象插入空流程为:

  1. 根据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看看关联值是如何处理的。

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,则对关联值进行retain操作,如果是OBJC_ASSOCIATION_SETTER_COPY,则对关联值进行copy操作,其他的则不做任何处理。

创建哈希表管理类获取全局哈希表

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的构造方法里,通过AssociationsManagerLock.lock加了一把锁,在当前AssociationsManager释放之前,后续创建的AssociationsManager都无法对其管理的资源进行操作,从而保证了线程安全,在通过get()拿到全局唯一的哈希表(因为_mapStoragestatic修饰的)

关联非空值流程

当要关联的值非空时,我们需要将这个值与当前对象关联起来,这一部分代码为

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 实现流程

try_emplaceDenseMap类的一个方法,这个方法在这个流程中被调用两次,第一次调用的是全局关联对象哈希表的try_emplace,传了封装了当前对象的disguised作为key和一个空的ObjectAssociationMap作为第二个元素,第二次,调用的第一次try_emplace得到的map,传递了关联对象的key值作为key,传递了封装value和策略的association作为第二个参数。

try_emplace内部到底做了什么事情呢,我们可以去源码一探究竟。

// 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. 创建一个空的BucketT,调用LookupBucketFor查找Key对应的BucketT,如果能够找到,将找到的BucketT赋值给刚刚创建的那个空的BucketT,并将BucketT封装成DenseMapIterator作为类对的第一个元素,将false作为第二个元素,并将该类对返回,此时的BucketT存放的是上次存放的keyvalue
  2. 如果没有找到,那么将传入的Key和Value插入创建新的BucketT,同样创建一个DenseMapIteratorBool组成的类对,只不过此时传递布尔值为true,此时的BucketT已经存放了传入的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);
}
}

非空值流程流程分析

通过阅读源码,可以总结出他的流程为:

  1. 尝试通过关联对象全局hashMap的try_emplace方法找到当前对象对应的bucket,此时的bucketkey为当前对象,value为一张ObjectAssociationMap,也是一张hashMap
  2. 如果查找返回的refs_result类对第二个元素为true,也就是说当前对象第一次有关联对象,将当前对象标记为有关联对象(修改isa)
  3. 通过refs_result.first->second拿到当前对象对应ObjectAssociationMap,调用这张ObjectAssociationMaptry_emplace找到关联对象标识符对应的value,也就是valuepolicy组装成的ObjcAssociation对象
  4. 如果第二个try_emplace方法返回的result的第二个元素为true说明这是该对象第一次插入该标识符的值,此时的valuepolicy已经在try_emplace插入到ObjectAssociationMap,不需要进一步处理
  5. 如果result.secondfalse,说明原先的对象的该标识符对应的关联对象有值,调用association.swap(result.first->second)交换修改关联对象(result.first->second存放的是valuepolicy组装成的ObjcAssociation对象)

关联空值流程

关联空值其实就是删除关联对象,这部分代码为:

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

        }
    }
}

其实通过非空关键对象流程的探究对关联对象原理了解,这部分代码就显得简单很多。

  1. 以用当前对象封装的DisguisedPtr对象为key在全局关联对象表associations中查找得到refs_it
  2. 如果refs_it不等于associations的最后一个元素,通过refs_it->second拿到当前对象对象的ObjectAssociationMap对象refs,也就是存放标识符和ObjcAssociation对象it(value和policy)的那张哈希表
  3. 通过标识符找到ObjcAssociation
  4. 如果it不是refs最后一个元素,交换原有标识符对应的关联对象,因为传入的为空,所以交换后标识符对应的对象为空
  5. 调用refs.erase(it),删除该标识符对应的bucket
  6. 如果此时对象对应的ObjectAssociationMap大小为0,则删除该对象对应的关联表

objc_getAssociatedObject

objc_getAssociatedObject底层调用的是_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. 创建AssociationsManager对象,通过AssociationsManager对象拿到全局唯一的关联对象管理表
  2. 以对象为key在关联对象表中查找对象的AssociationsHashMap::iterator
  3. 如果找到了,取到iterator的第二个元素,也就是ObjectAssociationMap,用标识符为key在这个ObjectAssociationMap查找
  4. 如果找到了,取找到的refs的第二个元素,也就是set时存放的value(第一个为policy),返回
  5. 以上如果都没有找到,返回nil

总结

关联对象其实使用了两张hashMap,可以用一张图解释他的原理。

ED603188-5741-48F5-AEFD-77EC8A2553DC.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345