iOS——关联对象

关联对象简介

associatedObject又称关联对象。顾名思义,就是把一个对象关联到另外一个对象身上。使两者能够产生联系。之前写项目的时候在cell中加入了输入框为了避免输入框内容的复用以及获取特定输入框的文本,需要给UITextField添加一个新的属性indexpath和cell关联。众所周知,分类是无法静态添加成员变量的,所以这里就利用到了Associated Object为该分类动态添加属性。

关联对象的使用

下面就为NSObject的分类添加一个属性。

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (testCategory)
@property (nonatomic, strong) id associatedObject;
@end

NS_ASSUME_NONNULL_END

主要在于实现部分:

//
//  NSObject+testCategory.m
//  关联对象
//
//  Created by 差不多先生 on 2022/5/29.
//

#import "NSObject+testCategory.h"
#import <objc/runtime.h> // 关联对象相关api在runtime库中
@implementation NSObject (testCategory)
- (void)setAssociatedObject:(id)object {

    objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

- (id)associatedObject {

    return objc_getAssociatedObject(self, _cmd);

}


@end

可以看到上面主要运用了两个方法,下面具体解析一下这两个方法。

objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)

objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
这个方法有四个参数。

  • @param object 关联源对象,也就是需要关联的对象。
  • @param key 对应的key
  • @param value 对象的key值。传递nil来清除现有关联。
  • @param policy 内存管理策略

内存管理策略:
点进去看可以发现是一个枚举类型。

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. */
   // 指定复制关联对象。关联是原子的。
};

看一下这个方法的源码,这个方法世纪调用的是下面的方法。:

_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
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.
    // 检查传入的是不是nil对象以及key,避免程序崩溃
    // rdar://problem/44094390
    if (!object && !value) return;
    // 判断runtime版本是否支持关联对象
    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));
    // 将 object 封装成 DisguisedPtr 目的是方便底层统一处理
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    // // 将 policy和value 封装成ObjcAssociation,目的是方便底层统一处理
    ObjcAssociation association{policy, value};
    // (如果有新值)保留锁外的新值。
    // retain the new value (if any) outside the lock.
    // 根据传入的缓存策略,创建一个新的value对象
    association.acquireValue();

    bool isFirstAssociation = false;
    {
        AssociationsManager manager; // 创建一个管理对象管理单例,类AssociationsManager管理一个锁/哈希表单例对。分配一个实例将获得锁
        // 并不是全场唯一,构造函数中加锁只是为了避免重复创建,在这里是可以初始化多个AssociationsManager变量的
        
        
        // 全场唯一
        AssociationsHashMap &associations(manager.get());
        
        if (value) {
            //这块的源码点不进去,查找了之前的版本,对比了看一下。
//            AssociationsHashMap::iterator i = associations.find(disguised_object);//首先通过对象的地址获取对象的hashmap
//              if (i != associations.end()) {//判断AssociationsHashMap中是否存在这个对象为key的值,已经存在
//              // secondary table exists
//              ObjectAssociationMap *refs = i->second;//取值,对应的map
//              ObjectAssociationMap::iterator j = refs->find(key);//通过key查找
//              if (j != refs->end()) {//如果已经存在
//               old_association = j->second;//取到原来老的值,以便后边对其释放
//               j->second = ObjcAssociation(policy, new_value);//存储新的值
//              } else {//不存在
//               (*refs)[key] = ObjcAssociation(policy, new_value);
//              }
            // 对比上面的代码发现,这个函数可能实现以下功能
            // 将传入的object,通过DISGUISE转化成disguised_ptrt类型的对象disguised_object
            // 以disguised_object为key去AssociationsHashMap中查找
            // 如果找到了则取出对应的值zObjectAssociationMap
            // 然后以我们传入的key当做key在ObjectAssociationMap中查找
            // 如果找到了取出key对应的值
            // ObjcAssociation将value结合我们传入的缓存策略policy生成一个新的对象暂换原来的值
            // 如果没有找到则以我们传入的key生成一个新的键值对也就是对象关联表
            
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                /* it's the first association we make */
                // 这是我们建立的第一个关联
                isFirstAssociation = true;
            }
            // 建立或替换关联
            /* establish or replace the association */
            // 获取ObjectAssociationMap中存储值的地址
            auto &refs = refs_result.first->second;
            // 移除之前的关联,根据key
            // 将需要存储的值存放在关联表中存储值的地址中
            // 同时会根据key去查找,如果查找到`result.second` = false ,如果找不到就创建`result.second` = true
            // 创建association时,当(association的个数+1)超过3/4,就会进行两倍扩容
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                // 交换association和查询到的`association`
                // 其实可以理解为更新查询到的`association`数据,新值替换旧值
                association.swap(result.first->second);
            }
        } else {
            // 这里相当于传入的nil,移除之前的关联
            // 到AssociationsHashMap找到ObjectAssociationMap,将传入key对应的值变为空。
            // 查找disguised 对应的ObjectAssociationMap
            auto refs_it = associations.find(disguised);
            // 如果找到对应的 ObjectAssociationMap 对象关联表
            if (refs_it != associations.end()) {
                // 获取 refs_it->second 里面存放了association类型数据
                auto &refs = refs_it->second;
                // 根据key查询对应的association
                auto it = refs.find(key);
                if (it != refs.end()) {
                    // 如果找到,更新旧的association里面的值
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        // 如果该对象关联表中所有的关联属性数据被清空,那么该对象关联表会被释放
                        associations.erase(refs_it);

                    }
                }
            }
        }
    }

    // Call setHasAssociatedObjects outside the lock, since this
    // will call the object's _noteAssociatedObjects method if it
    // has one, and this may trigger +initialize which might do
    // arbitrary stuff, including setting more associated objects.
    // 在锁外面调用setHasAssociatedObjects,因为如果对象有一个,这个//将调用对象的noteAssociatedObjects方法,这可能会触发initialize,这可能会做任意的事情,包括设置更多的关联对象。

    if (isFirstAssociation)
        object->setHasAssociatedObjects();

    // release the old value (outside of the lock).
    // 释放旧的值(在锁外部)
    association.releaseHeldValue();
}

这里解释一下上面的几个点。

  1. AssociationsHashMap为什么是全局唯一?
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();
    }
};

这里可以看到hashMap每次都是从静态变量取出,所以全局只会获取他一个。而AssociationsManager没有修饰,可以多次创建。
2.AssociationsHashMap,ObjectAssociationMap是什么?

typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;

通过定义可以看出来,前者根据DisguisedPtr也就是object转化的类型对应了每个对象的后者,里面存的则是key对应的ObjcAssociation。
3.ObjcAssociation是什么?

class ObjcAssociation {
    uintptr_t _policy;
    id _value;
public:
    ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}
    ObjcAssociation() : _policy(0), _value(nil) {}
    ObjcAssociation(const ObjcAssociation &other) = default;
    ObjcAssociation &operator=(const ObjcAssociation &other) = default;
    ObjcAssociation(ObjcAssociation &&other) : ObjcAssociation() {
        swap(other);
    }

    inline void swap(ObjcAssociation &other) {
        std::swap(_policy, other._policy);
        std::swap(_value, other._value);
    }

    inline uintptr_t policy() const { return _policy; }
    inline id value() const { return _value; }

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

    inline void releaseHeldValue() {
        if (_value && (_policy & OBJC_ASSOCIATION_SETTER_RETAIN)) {
            objc_release(_value);
        }
    }

    inline void retainReturnedValue() {
        if (_value && (_policy & OBJC_ASSOCIATION_GETTER_RETAIN)) {
            objc_retain(_value);
        }
    }

    inline id autoreleaseReturnedValue() {
        if (slowpath(_value && (_policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE))) {
            return objc_autorelease(_value);
        }
        return _value;
    }
};

可以看到这个类里面存的成员变量就是内存策略和关联的值,也就是我们添加的属性是存在这里的。

_object_get_associative_reference方法

这个源码就比较简单了

id
_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};//创建空的关联对象

    {
        AssociationsManager manager;//创建一个AssociationsManager管理类
        AssociationsHashMap &associations(manager.get());//获取全局唯一的静态哈希map
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);//找到迭代器,即获取buckets
        if (i != associations.end()) {//如果这个迭代查询器不是最后一个 获取
            ObjectAssociationMap &refs = i->second; //找到ObjectAssociationMap的迭代查询器获取一个经过属性修饰符修饰的value
            ObjectAssociationMap::iterator j = refs.find(key);//根据key查找ObjectAssociationMap,即获取bucket
            if (j != refs.end()) {
                association = j->second;//获取ObjcAssociation
                association.retainReturnedValue();
            }
        }
    }

    return association.autoreleaseReturnedValue();//返回value
}


通过源码可知,主要分为以下几部分

1:创建一个 AssociationsManager 管理类

2:获取唯一的全局静态哈希Map:AssociationsHashMap

3:通过find方法根据 DisguisedPtr 找到 AssociationsHashMap 中的 iterator 迭代查询器

4:如果这个迭代查询器不是最后一个 获取 : ObjectAssociationMap (policy和value)

5:通过find方法找到ObjectAssociationMap的迭代查询器获取一个经过属性修饰符修饰的value

6:返回 value

删除关联对象

系统提供了_object_remove_assocations(id object, bool deallocating)方法,你可能会在刚开始接触对象关联时想要尝试去调用objc_removeAssociatedObjects() 来进行删除操作,但如文档中所述,你不应该自己手动调用这个函数。此函数的主要目的是在“初试状态”时方便地返回一个对象。你不应该用这个函数来删除对象的属性,因为可能会导致其他客户对其添加的属性也被移除了。规范的方法是:调用objc_setAssociatedObject方法并传入一个nil 值来清除一个关联。

一些map对应关系

在这里插入图片描述
图片参考:iOS-底层原理 19:类扩展 与 关联对象 底层原理探索

写这个是为了了解怎么存取属性的,目前对整体了解的还是比较浅,没有对对象转化为disguised_object的分析,后续学习了过来补充。

try_emplace方法探究

之前源码中出现了try_emplace方法:try_emplace方法的作用就是去表中查找Key相应的数据,不存在就创建。
下面看一下该方法的具体实现:

template <typename... Ts>
std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
    BucketT *TheBucket;
    // 根据key去查找对应的TheBucket
    if (LookupBucketFor(Key, TheBucket))
      // 通过make_pair生成相应的键值对
      return std::make_pair(
               makeIterator(TheBucket, getBucketsEnd(), true),
               false); // Already in map.表示【表中】已经存在bucket
    
    // Otherwise, insert the new element.
    // 如果没有查询到 将数据(键值)插入TheBucket中
    TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);
    // 通过make_pair生成相应的键值对
    return std::make_pair(
             makeIterator(TheBucket, getBucketsEnd(), true),
             true); // true表示第一次往哈希关联表中添加bucket
}
  • 通过LookupBucketFor方法去表中查找Key对应的TheBucket是否有存在,如果存在对TheBucket进行包装然后返回
  • 如果不存在,通过InsertIntoBucket方法插入新值,扩容的规则和cache方法存储的规则是一样的

LookupBucketFor方法
这个方法就是 根据Key去表中查找Bucket,如果已经缓存过,返回true,否则返回false

// 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.
// 为Val查找相应的桶,在/// FoundBucket中返回。如果桶中包含键和值,则返回/// true,否则返回带有空标记或墓碑的桶,并返回false。
template<typename LookupKeyT>
bool LookupBucketFor(const LookupKeyT &Val,
                     const BucketT *&FoundBucket) const {
  // 获取buckets的首地址
  const BucketT *BucketsPtr = getBuckets();
  // 获取可存储的buckets的总数
  const unsigned NumBuckets = getNumBuckets();

  if (NumBuckets == 0) {
    // 如果NumBuckets = 0 返回 false
    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!");
  // 计算hash下标
  unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);
  unsigned ProbeAmt = 1;
  while (true) {
    // 内存平移:找到hash下标对应的Bucket
    const BucketT *ThisBucket = BucketsPtr + BucketNo;
    // Found Val's bucket?  If so, return it.
    if (LLVM_LIKELY(KeyInfoT::isEqual(Val, ThisBucket->getFirst()))) {
      // 如果查询到`Bucket`的`key`和`Val`相等 返回当前的Bucket说明查询到了
      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.
    // 如果bucket为空,说明当前key还不在表中,返回false,后续进行插入操作
    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);
    }
    // 重新计算hash下标
    BucketNo += ProbeAmt++;
    BucketNo &= (NumBuckets-1);
  }
}

InsertIntoBucket方法:

template <typename KeyArg, typename... ValueArgs>
BucketT *InsertIntoBucket(BucketT *TheBucket, KeyArg &&Key,
                          ValueArgs &&... Values) {
  // 根据Key 找到TheBucket的内存地址
  TheBucket = InsertIntoBucketImpl(Key, Key, TheBucket);
  // 将 Key 和 Values保存到TheBucket中
  TheBucket->getFirst() = std::forward<KeyArg>(Key);
  ::new (&TheBucket->getSecond()) ValueT(std::forward<ValueArgs>(Values)...);
  return TheBucket;
}

主要的工作都是在InsertIntoBucketImpl方法中完成的

  • 计算实际占用buckets的个数,如果超过负载因子(3/4),进行扩容操作this->grow(NumBuckets * 2);
  • 找到TheBucket的内存地址:LookupBucketFor(Lookup, TheBucket);
  • 更新占用的容量个数:incrementNumEntries();
template <typename LookupKeyT>
BucketT *InsertIntoBucketImpl(const KeyT &Key, const LookupKeyT &Lookup,
                              BucketT *TheBucket) {
  // If the load of the hash table is more than 3/4, or if fewer than 1/8 of
  // the buckets are empty (meaning that many are filled with tombstones),
  // grow the table.
  //
  // The later case is tricky.  For example, if we had one empty bucket with
  // tons of tombstones, failing lookups (e.g. for insertion) would have to
  // probe almost the entire table until it found the empty bucket.  If the
  // table completely filled with tombstones, no lookup would ever succeed,
  // causing infinite loops in lookup.
  // 计算实际占用buckets的个数,如果超过负载因子(3/4),进行扩容操作
  unsigned NewNumEntries = getNumEntries() + 1;
  // 获取buckets的总容量
  unsigned NumBuckets = getNumBuckets();
  if (LLVM_UNLIKELY(NewNumEntries * 4 >= NumBuckets * 3)) {
    // 如果哈希表的负载大于等于3/4,进行二倍扩容
    this->grow(NumBuckets * 2); // 首次分配 4 的容量
    LookupBucketFor(Lookup, TheBucket);
    NumBuckets = getNumBuckets();
  } else if (LLVM_UNLIKELY(NumBuckets-(NewNumEntries+getNumTombstones()) <=
                           NumBuckets/8)) {
    this->grow(NumBuckets);
    LookupBucketFor(Lookup, TheBucket);
  }
  ASSERT(TheBucket);

  // Only update the state after we've grown our bucket space appropriately
  // so that when growing buckets we have self-consistent entry count.
  // If we are writing over a tombstone or zero value, remember this.
  if (KeyInfoT::isEqual(TheBucket->getFirst(), getEmptyKey())) {
    // Replacing an empty bucket.
    // 更新占用的容量个数
    incrementNumEntries();
  } else if (KeyInfoT::isEqual(TheBucket->getFirst(), getTombstoneKey())) {
    // Replacing a tombstone.
    incrementNumEntries();
    decrementNumTombstones();
  } else {
    // we should be purging a zero. No accounting changes.
    ASSERT(ValueInfoT::isPurgeable(TheBucket->getSecond()));
    TheBucket->getSecond().~ValueT();
  }

  return TheBucket;
}

关联对象实际关系

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值