iOS底层分析之Category分类-属性的绑定

950 阅读5分钟

和谐学习!不急不躁!!我是你们的老朋友小青龙~

承接文章iOS底层分析之类的加载(下)做一些补充。

抛出探究的问题

前面文章对分类数据的加载已经做了分析,但是还有一些收尾工作尚未说明。譬如分类里添加了属性,但是具体的实现并没有提供,以至于报了警告:

image.png

以至于,当我直接对分类里的属性赋值的时候,提示我没有实现setName_category

image.png

既然如此,那就按照要求,我们自己实现一下呗:

image.png

如上图所示,按照我们平时的开发习惯,用objc_setAssociatedObjectobjc_getAssociatedObject去实现set和get方法,就可以正常赋值、访问属性。

我们知道category的数据是运行时,动态加载到类列表的,set和get方法系统不会帮我们生成,需要手动去添加。
但是objc_setAssociatedObjectobjc_getAssociatedObject,是如何做到数据绑定的呢?这就是我们要探究的点。

探究 - objc_setAssociatedObject

由于当前工程就是objc源码,所以直接command+单机一步步访问,最终定位到: objc_setAssociatedObject -> _object_set_associative_reference

// object 实例对象,即属性要跟谁关联
// key    属性名
// value  属性值
// policy 关联策略
/// 探究点:属性值如何绑定到类的实例对象
/// 将value通过key绑定到object上,绑定策略是policy
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    ...
    /// 1、伪装object的指针地址,统一数据结构,即把(objc_object *)object包装成DisguisedPtr<objc_object>这样的数据结构
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    
    /// 2、存储policy和value到association
    ObjcAssociation association{policy, value};

    ...

    bool isFirstAssociation = false;
    {
        /// 管理类
        AssociationsManager manager;
        /// 3、找到AssociationsHashMap哈希表
        AssociationsHashMap &associations(manager.get());

        if (value) {
            /// 4、第一次调用try_emplace,就创建空的BucketT
            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 */
            auto &refs = refs_result.first->second;
            /// 5、再调用一次try_emplace,插入association到BucketT
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else {
            /// 进入这里,说明参数value为空,那就进行erase(擦除,相当于情况这条记录)
            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);

                    }
                }
            }
        }
    }
    if (isFirstAssociation)
        object->setHasAssociatedObjects();

    // 释放旧值(锁外部)。
    association.releaseHeldValue();
}
第一次调用try_emplace

01.png

02.png

我们发现,名叫LookupBucketFor的函数有两个,分别是:

  1. 第二个参数带cost
/** 
  LookupBucketFor-查找Val的适当存储桶,并以
  铸造桶。如果bucket包含键和值,则返回
  true,否则它返回一个带有空标记或墓碑的bucket,并且
  返回false。
  */
  template<typename LookupKeyT>
  bool LookupBucketFor(const LookupKeyT &Val,
                       const BucketT *&FoundBucket) const {
    
    printf("LookupBucketFor-------const-----const \n");
    const BucketT *BucketsPtr = getBuckets();
    const unsigned NumBuckets = getNumBuckets();

    if (NumBuckets == 0) {
      FoundBucket = nullptr;
      return false;
    }
    ...
}
  1. 第二个参数不带cost
bool LookupBucketFor(const LookupKeyT &Val, BucketT *&FoundBucket) {
    printf("LookupBucketFor-------const-----non \n");
    const BucketT *ConstFoundBucket;
    // 这里会调用LookupBucketFor,第二个参数带const
    bool Result = const_cast<const DenseMapBase *>(this)
      ->LookupBucketFor(Val, ConstFoundBucket);
    FoundBucket = const_cast<BucketT *>(ConstFoundBucket);
    return Result;
  }

从try_emplace函数的代码来看,是调用第2个LookupBucketFor。

我们来运行一下工程,看下控制台打印的顺序:

image.png

打印结构,跟我们的分析结果一致。

InsertIntoBucket做了什么
template <typename KeyArg, typename... ValueArgs>
  BucketT *InsertIntoBucket(BucketT *TheBucket, KeyArg &&Key,
                            ValueArgs &&... Values) {
    TheBucket = InsertIntoBucketImpl(Key, Key, TheBucket);

    TheBucket->getFirst() = std::forward<KeyArg>(Key);
    ::new (&TheBucket->getSecond()) ValueT(std::forward<ValueArgs>(Values)...);
    return TheBucket;
  }

我们打打印一下,最终的TheBucket是什么样的:

image.png

我们发现InsertIntoBucket函数,会把KeyValues插入到TheBucket里。
Key和Values分别对应着:

  • Key --- try_emplace函数第一个参数
  • Values --- try_emplace函数第二个参数 这里我们可以看到,第一次调用,Values是空的。

继续下一步,结束了第一次try_emplace函数调用,打印一下refs_result:

image.png

image.png

根据前面对InsertIntoBucket函数的分析,second存放的应该是values,但是从打印结果来看,values还是空的,说明并没有插入进去。

第二次调用try_emplace

接下来第二次进入try_emplace,我们发现第二个参数带了association(association是对policy和value的封装)。
我们直接定位InsertIntoBucket函数:

image.png

如图所示,此刻的Values才算是真正的有值。 下一步出来,打印result结果:

image.png

AssociationsHashMap为什么是单例?

定位代码:

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    ...
    AssociationsHashMap &associations(manager.get());
    ...
}

command+单机进入get:

image.png

static修饰,于AssociationsManager来说,_mapStorage是唯一的,所以说AssociationsHashMap是单例

探究 - objc_getAssociatedObject

objc_getAssociatedObject -》_object_get_associative_reference:

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

    {
        AssociationsManager manager;// 管理类
        AssociationsHashMap &associations(manager.get());/// 得到所有HashMap
        /// 根据object找到对应HashMap
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            ObjectAssociationMap &refs = i->second;
            ObjectAssociationMap::iterator j = refs.find(key);//根据key查找,得到TheBucket(j)
            if (j != refs.end()) {
                /// 拿到对应的association(association是对_policy和_value的封装)
                association = j->second;
                /// 取出Value
                association.retainReturnedValue();
            }
        }
    }

    return association.autoreleaseReturnedValue();
}

下断点,打印数据:

image.png

image.png

断点继续往下走:

image.png

至此,完成了objc_setAssociatedObject和objc_getAssociatedObject的探究。

总结

objc_setAssociatedObject流程

  1. 将object封装成「DisguisedPtr」结构

  2. 将policy和value封装成「ObjcAssociation」结构

  3. 找到AssociationsHashMap哈希表

  4. 调用try_emplace

  5. 如果是第一次调用try_emplace,就创建空的BucketT

  6. 然后再调用一次try_emplace,插入association到BucketT

objc_getAssociatedObject流程

  1. 得到管理类

  2. 得到所有的HashMap

  3. 根据object找到对应HashMap

  4. 根据key查找,得到TheBucket

  5. 从TheBucket拿出association

  6. 从association拿出value

补充 - association关联是什么时候释放的呢?

我们知道association是通过object去找到对应的HashMap表,然后再对这张表进行操作,那么association的释放是否也跟object生命周期有关呢?

快速定位,全局搜索“dealloc {”:

image.png

image.png

image.png

image.png

image.png

// deallocating我们知道,传进来是true
void
_object_remove_assocations(id object, bool deallocating)
{
    ObjectAssociationMap refs{};
    {
    // 下面代码跟objc_getAssociatedObject函数源码有点类似,就不做详细解释,
    // 大致流程是:找到关联对象,然后执行erase擦除动作。
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            refs.swap(i->second);

            // If we are not deallocating, then SYSTEM_OBJECT associations are preserved.
            bool didReInsert = false;
            if (!deallocating) {
                // 因为参数deallocating为true,所以不会进入这里
                ...
            }
            if (!didReInsert)
                ///  擦除
                associations.erase(i);
        }
    }
    ...
}

这里放一张关系图

image.png

代码

链接: pan.baidu.com/s/16oTTFgdB…
密码: 5g27
--来自百度网盘超级会员V2的分享

更新

更新时间:2021.08.23
更新内容:association关联是什么时候释放的呢?(详情请看文章)