05-关联对象的底层分析

OC底层原理探索文档汇总

通常我们在分类中使用关联对象来实现setter和getter方法中对同一属性的操作,但是关联对象的作用到底是什么,以及关联对象的底层是如何存储和获取该属性的呢,本文就针对关联对象进行底层探索

主要内容:

  1. 关联对象的使用
  2. 关联对象的底层分析

1、关联对象的使用

1.1 关联对象的简单介绍

通常我们会在分类中使用关联对象。因为分类中无法直接使用属性,创建属性后不会自动生成setter和getter方法的实现和成员变量,即时我们手动添加方法的实现也无法使用成员变量。通过关联对象可以在不同的函数中实现成员变量的关联,以此让我们可以在分类中使用属性,也就是将存储和获取的成员变量进行关联作为同一个值。

1.2 为什么要使用关联对象

  • 因为分类的属性中是没有成员变量和setter、getter方法的实现的
  • 虽然setter、getter方法的实现是可以自定义写的,然而成员变量是不能自己写的,因为反正也存不到类的ro中,ro是只读的,只能读取,不能写入
  • 没有成员变量的话,setter/getter方法就没有操作的对象
  • 所以为了将两个方法所操作的变量关联起来,也就是确保存的值和取的值是同一个,所以就需要关联对象。
  • 所以如果只有getter方法,没有setter方法,就不需要关联了。
  • 当然只要不同的函数操作同一个值,就需要关联对象,而不仅仅关注setter和getter方法

1.3 关联对象的简单使用

设置属性

@interface WYPerson (cate)

@property (nonatomic,copy) NSString *cate_name;
@end
复制代码

说明

  • 在分类中创建这样的属性,其实现在只有setter/getter方法的声明,没实现,并且也没有_cate_name的成员变量
  • 我们并不能直接使用这些setter、getter、_cate_name
  • 就算我们手动的去实现这些方法,但并没有成员变量供其使用
  • 所以接下来需要在自定义方法中使用关联对象进行关联

自定义setter实现

- (void)setCate_name:(NSString *)cate_name{

    /*
     1:对象
     2:标识符
     3:value
     4:策略
     */
    objc_setAssociatedObject(self, "cate_name", cate_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
复制代码

说明

  • setter方法中传入的四个值,对象、标识符、value值,策略
  • value值就是我们进行存储和获取的那个属性值
  • 对象和标识符是用来查询value值
  • 标识符只是用来对变量的识别,一般就写和变量名称一样就行了
  • 策略是值关于这个属性的一些attribute

自定义getter实现

- (NSString *)cate_name{
    /*
     1:对象
     2:标识符
     */
    return  objc_getAssociatedObject(self, "cate_name");
}
复制代码

说明

  • 直接通过对象和标识符进行获取其中的值

策略:

/**
 * Policies related to associative references.
 * These are options to objc_setAssociatedObject()
 */
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};
复制代码
  • 可以看到从上到下分别是assign、retain/nonatomic、copy/nonatomic、retain、copy,注释已经很详细了。不再多说。

移除关联对象: 通过设置value值为nil就可以移除关联对象

注:为什么这样可以移除,可以查看下面的底层分析

源码:

- (void)removeAssociation{
    objc_setAssociatedObject(self, "cate_name", nil, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
复制代码

代码验证:

WYPerson *person = [WYPerson alloc];
person.cate_name = @"嘿嘿";
    
NSLog(@"移除关联前%@",person.cate_name);
//移除关联对象
[person removeAssociation];
NSLog(@"移除关联后%@",person.cate_name);
复制代码

输出结果

2021-10-21 10:27:50.070717+0800 关联对象[54602:771902] 移除关联前嘿嘿
2021-10-21 10:27:50.071130+0800 关联对象[54602:771902] 移除关联后(null)
复制代码

2、关联对象的set流程底层分析

主要探索的是value存到了哪里,以怎样的方式存储。

2.1 objc_setAssociatedObject

从上文我们知道上层通过objc_setAssociatedObject来实现的set属性,因此从它开始向底层探索

在objc中查找源码如下

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

这里调用的是一个,而不是一个函数调用,所以需要查看get()返回的是什么

继续查看源码:


static ChainedHookFunction<objc_hook_setAssociatedObject> SetAssocHook{_base_objc_setAssociatedObject};

//这个是带有泛型的类型,带有get()方法,简单了解即可
template <typename Fn>
class ChainedHookFunction {
    std::atomic<Fn> hook{nil};

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

    //返回这个Fn
    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));
    }
};
复制代码

发现返回的是一个函数

因此我们可以写成这样

void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
//    SetAssocHook.get()(object,key,value,policy);
    _base_objc_setAssociatedObject(object, key, value, policy);
}
复制代码

说明:

  • 进入到底层发现其内部调用了SetAssocHook.get()方法
  • 这种设计模式也和setter一样,属于接口模式,对外的接口不变,内部的实现会根据不同的形参而变化,这对于动态的创建有一定规律的方法有很好的作用
  • 也就是说SetAssocHook.get()返回了一个方法,而这个方法在此处进行执行
  • 所以需要进入到SetAssocHook中查看这个get()返回了什么
  • 可以理解为SetAssocHook.get()等价于_base_objc_setAssociatedObject

2.2 _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);
}
复制代码

2.3 _object_set_associative_reference

这个是进行设值最主要的代码,注释写的比较清楚了,我这里对整理的逻辑进行说明

源码:

/*
 1、将对象和value值进行初始化
 2、通过管理类拿到关联对象的哈希map表
 3、通过对象查询桶子,如果存在直接返回,如果不存在,则创建一个空桶子并返回
 4、在桶子中通过key查询桶子是否存在,如果存在就更新key-value的键值对。没有就插入。
 */
//前面传入的四个参数,第一个参数objcet作为第一层map的key值,第二个参数key作为第二个map的key值,第三第四个参数作为第二次的value值。
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));

    /*
     1、将对象和value值进行初始化
     */
    //将objc_object结构体封装成了DisguisedPtr结构体,便于后续使用,作为key值
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    //将policy和value包装到一个结构体ObjcAssociation中,作为最终存储的value值
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    //会根据不同的策略进行一下处理,只有retain和copy进行了特殊处理
    association.acquireValue();

    {
        //这个是关联对象的管理类,此处初始化一个对象出来
        /*
         这里的对象不是唯一的,可以创建多个,构造函数和析构函数加锁只是为了多线程
         也就是说每次的使用是唯一的,不可以同时使用多个,只有把上一个删掉之后,才会在析构函数中解锁,
         但是对象本身不是唯一的,是可以创建多个的,使用上只能是当前使用的清掉之后才可以使用下一个
         */
        AssociationsManager manager;//这样写就相当于直接调用了构造函数来创建
        
        /*
         关联对象的HashMap表记录了工程中所有的关联对象,这张表是唯一的,这样做也便于查找
         这里也是通过管理类得到哈希表
         在整个程序中是共享的,因为是通过静态变量获取出来的
         */
        AssociationsHashMap &associations(manager.get());
        /*
         1、hashmap先根据disguised获取到桶子(空桶子或有值的桶子都可以,反正是要获取桶子的)
         2、如果是空桶子,说明是第一次关联对象,就需要设置到isa中
         3、桶子获取到ObjectAssociationMap(关联对象映射),并将association存入进去,这样就关联到了。
         */
        
        //如果有值,则开始关联,也就是将值保存下来
        if (value) {
            //返回的是一个类对
            /*
             传入的参数
                第一个参数:disguised
                第二个参数:空ObjectAssociationMap
             
             0、他们两个加起来就是一个桶子
             1、如果已经存在则直接返回一个桶子
             2、如果不存在则创建一个空桶子并插入进去,也会返回这个空桶子
             3、获取的值中最主要的是一个bucket,也就是说这里的refs_result还有其他数据,包括了桶子
             */

            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            //将这个结果拿到手,进行解析,发现是一个key-value的键值对
            //返回值就在value中,所以这样获取返回的是一个bool值
            //这个类对,我们需要的是第二个值,所以直接判断second
            //返回值的second就表示是否是空桶子,如果是空的就需要设置isa
            if (refs_result.second) {//判断第二个存不存在,即bool值是否为true
                /* it's the first association we make 如果是第一次建立关联,需要给这个对象设置标记,在isa中*/
                object->setHasAssociatedObjects();//nonpointerIsa ,标记位true
            }

            /* establish or replace the association */
            //创建或替换association
            /*
             所以上面不管获取的是不是空桶子,都不影响这里
             因为如果是空桶子,就创建association,如果不是空桶子,就更新association
             */
            auto &refs = refs_result.first->second;//得到桶子的value值
            //这里进入的应该是第二个方法,因为key的类型是const void
            auto result = refs.try_emplace(key, std::move(association));//查找当前的key是否有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. 初始化
    1. object需要封装到DisguisedPtr,这是进行了进一步的处理,也不用关注,只要知道第一层是通过对象来查询的就可以(第二层是通过key值获取)
    2. ObjcAssociation是最终存储的值,里面包含了value和策略,他们俩是一起存储的。
    3. 通过我们传入的值来查找最终的位置,之后存入我们的value和策略
    4. acquireValue()会根据策略处理一下association,但只有retain和copy进行了特殊处理
  2. 拿到哈希表
    1. 第一要点就是关联对象的管理类不是全局唯一的,而哈希map表是全场唯一的,这个要千万注意(下面会有专门的验证过程,这里先记住)
    2. 管理类的对象是可以创建多个的,虽然析构函数和构造函数加了锁了,只有执行了析构函数才可以解锁,但是这并不妨碍可以创建多个.
    3. 加锁,并不代表唯一,只是为了避免多线程重复创建,其实在外面试一下是可以重复创建的
    4. hashMap表是一张全局表,记录了项目中所有的关联对象,它是通过静态变量获取到的,所以是全场唯一的
    5. hashMap表里面有很多桶子,一个桶子就是一个键值对,键是disguised,值是ObjectAssociationMap,ObjectAssociationMap就是第二层的哈希表
    6. 对于哈希表我们所要做的就是查找桶子并存入数据
  3. 查询桶子
    1. 查询桶子就是通过disguised进行查询
    2. 需要传入一个空的ObjectAssociationMap,因为如果查找不到,需要创建一个空桶子,所以先传入这个参数
    3. 返回值除了桶子还有其他数据,其他的不用关注
    4. 第一个值第二项是ObjectAssociationMap(它就是用来存储value值的)
    5. 第二个值是一个bool,就是判断是否是空桶子,如果是空桶子,就需要在isa中进行标记关联
  4. 插入或更新key-value
    1. 如果获取到了空桶子,就设置新的association
    2. 如果获取到的不是空桶子,也就是以前已经存过数据了,就更新association
    3. 调用的方法和上面查询桶子基本一样,不赘述
  5. 如果传入的是nil,就移除关联
    1. 根据 DisguisedPtr 找到 AssociationsHashMap 中的 iterator 迭代查询器
    2. 查询到相应的值就直接清理掉

示意图: objc_setAssociatedObject执行流程.png

2.4 AssociationsManager

源码:

// class AssociationsManager manages a lock / hash table singleton pair.
// Allocating an instance acquires the lock
/*
 管理类管理了一个锁和一个哈希表单例对
 创建一个关联对象将获取到锁
 */

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对象的创建会加锁,注销对象后会释放锁,所以一个管理类在进行创建哈希表等操作时,不能让其他管理类来插足
  • AssociationsHashMap表的创建是通过_mapStorage,而它是静态变量,所以是全局唯一的

2.4 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.
  /*
   1、通过key判断是否有桶子了。
   2、如果没有,就得到一个空桶子,并将key和空ObjectAssociationMap插入进去,并返回
   3、如果早已存在,就直接返回这个桶子
   */
  template <typename... Ts>
  std::pair<iterator, bool> try_emplace(KeyT &&Key, Ts &&... Args) {
    BucketT *TheBucket;
    
    //如果查询到了,就获取bucket,组装一下,回调出去,返回这个桶子,返回false
    if (LookupBucketFor(Key, TheBucket))
      return std::make_pair(
               makeIterator(TheBucket, getBucketsEnd(), true),
               false); // Already in map.

    // Otherwise, insert the new element.
    //如果没有查询到,就直接插入新的空桶子,并且返回true
    TheBucket =
        InsertIntoBucket(TheBucket, std::move(Key), std::forward<Ts>(Args)...);
    return std::make_pair(
             makeIterator(TheBucket, getBucketsEnd(), true),
             true);
  }

  // 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.
  /*
   如果map中查找不到key,插入key-value对到map中
   如果key不在map中,则会在适当的时候创建value,否则它不会移动
   */
  /*
   1、通过key查找桶子,如果找到返回true,并且得到桶子
   2、如果没有查到桶子,则创建一个桶子并插入
   */
  template <typename... Ts>
  std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
    BucketT *TheBucket;
    if (LookupBucketFor(Key, TheBucket))//找桶子,其中key是关联的对象,返回桶子TheBucket
      return std::make_pair(//如果桶子存在,则返回false,置为false的原因是哈希map中已经存在
               makeIterator(TheBucket, getBucketsEnd(), true),
               false); // Already in map.已经存在,直接返回-false

    // Otherwise, insert the new element.,如果没有找到桶子,则插入
    TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);//添加值
    return std::make_pair(
             makeIterator(TheBucket, getBucketsEnd(), true),
             true);//返回true,插入桶子
  }

复制代码

说明:

  • try_emplace是重载函数,有两个函数,可以通过不同的参数类型来辨别
  • 通过第一个参数的参数类型可以看到,查找第一层的桶子时调用第一个函数(参数类型为KeyT),查找第二层的桶子时调用第二个函数(参数类型为const KeyT)
  • 两个重载函数的实现基本一样。
  • 先调用LookupBucketFor函数查找该关联对象的桶子是否存在,如果存在,返回YES,并且返回这个桶子,如果不存在返回NO,并且返回一个空桶子
  • 如果存在,调用make_pair函数将桶子返回,并返回false,表示没有添加新桶子
  • 如果不存在,调用InsertIntoBucket函数创建一个桶子对象,并且调用make_pair函数插入桶子,返回这个桶子,并且返回true表示添加了新桶子

2.4 LookupBucketFor

源码:

/*
   查找Val相应的桶子,并且通过FoundBucket返回这个桶子
   如果桶子中包含键和值,返回true,否则返回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);//只是一个哈希算法,用以通过Val得到bucket的下标
    unsigned ProbeAmt = 1;
    while (true) {//循环向前查找,这是因为哈希冲突会防止放到前面的位置
      const BucketT *ThisBucket = BucketsPtr + BucketNo;
      // Found Val's bucket?  If so, return it.
      //如果所要的bucket,则直接返回true
      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.
      //如果找到的桶子是空的,说明这个key不再这个set中,插入Val并
      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);
    }
  }

  template <typename LookupKeyT>
  //会得到两个值,一个是FoundBucket,具体的桶子,还有一个布尔值,用以判断是否包含键和值
  //这个是重载函数,BucketT没有用const修饰
  //这里是调用的地方,因为外部传的参数也是BucketT,而不是const BucketT定义的
  bool LookupBucketFor(const LookupKeyT &Val, BucketT *&FoundBucket) {
    //其实只做了一件事,就是在此调用上边的LookupBucketFor函数
    //唯一的区别就是使用新的const修饰的BucketT 来获取
    const BucketT *ConstFoundBucket;
    //外部传进来的值并没有做任何操作,只是通过指针作为参数的一个便利,让函数内部修改后,外部也可以直接修改掉
    bool Result = const_cast<const DenseMapBase *>(this)
      ->LookupBucketFor(Val, ConstFoundBucket);
    FoundBucket = const_cast<BucketT *>(ConstFoundBucket);
    return Result;
  }
复制代码

说明:

  • 可以看到LookupBucketFor函数也是重载函数,而通过参数类型可以判断得知外部调用的是第二个函数
  • 而第二个函数的实现其实就是调用第一个函数,所以我们需要分析第一个函数
  • 经过简单分析可以看到通过传入的val值通过哈希函数查找桶子,并且通过FoundBucket返回这个桶子。如果桶子中包含键和值,返回true,否则返回false

2.5 总结

  • 最外层是一个全局唯一的静态哈希表,里面存储了所有的关联对象,这个哈希表有好多桶子,每个桶子的键值对是对象和关联表
  • 关联表也存储了很多的桶子,每个桶子的键值对是key-value,这里的key就是标识符
  • 所以总共有两层的哈希表来实现的,也相当于一个二维数组,第一维是查找哪个对象,第二维是查找哪个属性
  • 如果将关联对象的value值设置为空,则移除关联

存储结构图: 16-关联对象底层存储结构图.png

3、关联对象的get流程底层分析

上面我们知道是通过objc_getAssociatedObject函数来实现的,因此从这个函数开始分析 getter流程主要是进行两层的一个查询,分别通过object和key对每一层进行循环查询

3.1 objc_getAssociatedObject

底层通过_object_get_associative_reference实现的

id
objc_getAssociatedObject(id object, const void *key)
{
    return _object_get_associative_reference(object, key);
}
复制代码

3.2 _object_get_associative_reference

源码:

/*
 object是对象,用来查找第一层的值
 key是标识符,用来查找第二层的值
 */
id
_object_get_associative_reference(id object, const void *key)
{
    /*
     这样的做法是为什么????这里把具体的操作放在了一个局部作用域去写,把定义变量和返回值放在外面呢
     */
    
    //定义一个关联对象
    ObjcAssociation association{};
    {
        //拿到全局唯一的哈希map
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        //迭代器用来循环遍历,这里的迭代器是拿到第一层的桶子
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            //拿到其中的一个ObjectAssociationMap
            ObjectAssociationMap &refs = i->second;
            //拿到这个关联对象map的所有桶子,也就是第二层了
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                //拿到关联对象
                association = j->second;
                association.retainReturnedValue();//加了个retain
            }
        }
    }

    return association.autoreleaseReturnedValue();//拿到这个值
}
复制代码

说明:

  1. 上面我们知道关联对象存储在两层的哈希表中,所以需要两个循环遍历获取最终的value值
  2. 分别用object查询第一层循环,和用key查询第二层循环
  3. 最后拿到association的value,返回即可

3.3 总结

  • 拿到全局唯一的哈希表,之后迭代器遍历表中每一个桶子
  • 先通过关联对象查找到第一层的value值ObjectAssociationMap
  • 再通过标识符查找到第二层的value值association
  • 第二层的association就包含了变量值和策略。
  • 最后再取出association中的变量值

4、AssociationsHashMap 唯一性验证

很多人认为加锁会导致AssociationsManager唯一,所以这里把锁去掉看看创建出来的AssociationsHashMap是否唯一。

下面可以看到把锁去掉之后AssociationsHashMap仍然是唯一的,说明它的唯一性与锁无关,而是因为通过一个静态变量创建的,锁只是为了在多线程中避免管理类的重复使用

去掉锁:

去掉锁.png

再定义一个manager和map

再定义一个manager和map.png 调试查看两个是否唯一

第一个.png 第二个.png 结论:

  1. 去掉锁之后仍然是唯一的,所以与锁无关。加锁是为了防止多线程多哈希表的处理。manager是可以创建多次的。
  2. 哈希表的唯一是因为这个哈希表是通过_mapStorage获取的,而这个变量是静态变量

5、总结

  • 关联对象用来给两个函数操作的变量关联起来,常用在分类中的setter和getter方法
  • 关联对象在底层中是通过一个全局的哈希表来存储的,共有两层,第一层以对象作为key,一个新哈希表作为value,第二层也就是这个新哈希表中以标识符作为key,变量值和策略作为value。
  • 当我们在设置关联对象时,如果传入的变量值为nil,会在哈希表中删除该关联对象
  • 关联对象不仅仅局限于常用的分类中,只要有需要存储某个对象的变量值以后都可以使用
  • 当给一个对象添加关联后,会在isa中做一个标记,当注销一个对象时,会进行判断如果该对象进行过关联,就需要先到哈希表中删除该关联对象,再注销对象。

猜你喜欢

转载自juejin.im/post/7032181606664110087
今日推荐