底层探索 -- 类de加载过程分析(四)类扩展(Extension)与关联对象(Associate)

749 阅读9分钟

已经通过之前的几篇博客。将有关于Cls数据结构相关的内容基本分析完了,包括Cls在底层的本质、Cls中的数据结构、Cls及Category的各种数据结构的内容加载等等一系列的底层源码。本篇衔接着前一篇分析完的Category,再分析2个与Cat相关的内容:类扩展和关联对象。 ((本篇总字约2800字))

一、 类扩展(Extension)

类扩展,这个叫法听起来可能会比较陌生,但是只要是写代码类扩展肯定会被用到,不过是在用的时候不知道那个结构就叫做类扩展罢了,现在来看看到底什么是类扩展:

AEaLj5xKIBx4zMHPY0l04mwJfUihhFOTBWuO1efJKe0.png

  1. 如图中标注的就是类扩展,也可以说是特殊的Cat或者将其理解为匿名Cat,从Method、Property、Ivars三个方面将Ext与Cat做一下简单比较:

    Snipaste_2022.05.17_09.23.43_batch.png

  2. Extension添加的Mehtod、Property跟随着Cls的加载就完成了,不再像Cat一样根据不同的情况Attach的顺序会有不同,这一点可以通过之前博客中分析 MethodList 时使用的偏移方法去获取输出 baseMethodList 验证。

二、关联对象(Associate)

开始分析Associate之前,先将Associate的存储结构图放到最开始,便于对 setAssociate 存储逻辑在整体上有初步的了解:

drawio_batch.png

2.1、Assocaiate API

关联对象的作用就是存储,特别是用在Category 中添加共有属性,为Property实现Set/Get方法 (更多用法可查看Associated Objects 。在OC层面中 Associate 有仅3个API,分别是增、查、删: objc_setAssociatedObject(object, key, value,policy)
objc_getAssociatedObject(object, key)
objc_removeAssociatedObjects ,继续看一下这3个方法的源码实现及在不同版本的Objc源码中的差异比较:

VN-R2mNNekQIxGZB7x3LdfZM-rlkdFe4ZHX6WqA-0oE.png

  1. 比较的源代码版本是781和818这两个版本,在717中通过SetAssocHook 调用的方式在818中进行了简化,通过上图的比较可以看得出:无论如何Set_Associate的最终实现方法都是_object_set_associative_reference(object, key, value, policy)
  2. 对于Associate 的分析,先从Set方向去分析底层源码的实现逻辑,将存的方法调用顺序、逻辑都梳理清晰了,取值过程中的方法看起来就会变得很简单了。

2.1、setAssociated

首先来看一下_object_set_associative_reference 方法的整体逻辑,大致可以分这3个部分:参数准备、存储过程、释放对象&更新isa:

LrCP3NZDjeyWbPmgsRL1CkLXZhr1CvyInAnLyDsZ45k.png

  1. 参数准备: 抛开两个简单的IF判断不说,分析一下disguisedassociation

    1. disguised :是将传入的Objc对象进行统一的数据结构包装。DisguisedPtr 内部的实现逻辑是:将object的地址转换为uintptr后取反,然后存到其内部变量 uintptr_t value,还原 Object 则按转换的方式逆向操作即可。

    2. association :与disguised 相同,调用的是 ObjcAssociation 类中的第一种构造函数,将PolicyValue 初始化到association 内部变量 _policy、_value中。

    3. 紧接着,调用 association.acquireValue() 根据 _policy 对内部变量 _value 增加引用计数继续持有 (Retain或Copy)。

    4. 如图左侧红色箭头所示,标注出了association 的生命周期,以及在存值过程中被使用到的位置,主要是在 Manager.lock 的作用域内,以及最后的 Release。

      aL0iUmGEgp4h70wWc9ZpFtmufMFswPqeJYkAvMF6BS8.png

  2. 存储过程: 整个存的过程是在大括划分划分的Manager.lock的作用域中完成的。

    1. 创建 manager :这里是调用了 AssociationsManager 的C++构造函数,类比于OC中的 init 。manager可以创建多个,但是每次只能使用一个,所以创建出来的 manager 也并不是单例

    2. 获取 HashMap :这里通过 manager.get 初始化的 AssociationsHashMap 才是全局唯一,那么HashMap 是如何通过 manager.get 获取来的,怎么就会是全局唯一呢?看一下其源码中的获取过程:

      qZ4Ykw9sTELJenHofDfoPRXjCVjq2OTOLb0GEd6bLQA.png

    3. IF分支: 根据入参的 Value 是否有值以进行不同的 HashMap 操作,Value没有值的情况与有值的的情况对于Bucket的查找原理是一模一样的 、调用的方法名大同小异而已。所以只要对 常规情况,有值的代码分支进行分析即可 ,虽然只有几行代码实现,但是个别方法内部实现又有其他方法调用,所以 另起2.2小节 进行分析。

  3. 释放对象&更新isa:

    1. IF内方法是否执行,根据存储开始之前的BOOL变量 isFirstAssociation 进行判断,方法的作用是:第一次设置Associate的时候对isa中的has_assoc进行更新,以便将来在释放对象时,其中如果存在Associate则要进行清除关联

    2. 在对 Value 操作完成之后,要对临时变量 association 进行释放,同时也是对其内部 _value 持有的引用计数--association 在整个方法中的生命周期刚才也提到了。

2.2、try_emplace

首先一句话概括一下 try_emplace 的作用:插入键值对到HashMap中。 其次,try_emplace 在前一篇博客对Category的附加分析中就曾经出现过,在 addForClass() 中。现在来看一下它的源码实现:

pjc3_iJOUsAkQydbNP2qAl8Qq8Z-70PqifGFXakclB0.png

  1. 整体上方法逻辑如图,就是IF的两个分支:找到Bucket、没找到Bucket,接下来分析其中的逻辑:

  2. 准备一个空的 BucketT指针:TheBucket ,这个指针会在 LookupBucketFor 被赋值,也就是通过指针传递参数保持内外同步。

  3. 调用 LookupBucketFor 进行查找,在LookupBucketFor 中会Get当前 (try_emplace 的调用者) this.Buckets ,查找流程在2.3小节展开。

    1. 如果当前Key已经存在于HashMap中,要注意 std::make_pairsecond 值设置的是FALSE。
    2. 如果没有找到,那么就调用 InsertIntoBucket 对当前的 this.Bucket 进行 Key&Value 的插入,并对 Bucket 的容量进行调整
  4. 完成插入后依然是构建一个 std::pair 返回,这时 Pairsecond 值设置的是TURE ,表明是第一次进行 SetAssociate。

    1. 这里的BOOL变化影响着 objc_getAssociatedObject 中的 isFirstAssociationresult.secondrefs_result.second 的判断。

注意一点:
这里的方法入参 (const KeyT &Key, Ts &&... Args) ,单从objc_getAssociatedObject 作为入口来说,&Key 第一次是 disguised ,第二次是才是 SetAssociateKey,要注意区别,当然这也对应着两层不同的HashMap。


知识点补充:

  1. std::pair 主要作用是将两个数据组合成一个数据,两个数据的类型可同类型或者不同类型。
    • 比如 std::pair<int,float> std::pair<double,double>
    • std::pair 的实质是一个结构体,其主要的两个成员变量是 firstsecond ,两个变量可以直接使用。
    • 初始化一个 std::pair 可以使用构造函数,也可以使用 std::make_pair 函数。
  2. make_pair 主要作用是 构造一个 Pair 对象,并设置First、Second变量。
    • 一般make_pair 使用在需要 std::pair 做参数的位置。
    • 函数定义:template <class T1, class T2> pair<V1,V2> make_pair (T1&& x, T2&& y);

有关 pair & make_pair 更详细介绍,可以到cplusplus.com中查看

2.3、LookupBucketFor

还是先概括一下LookupBucketFor 方法的作用:查找Val的对应的Bucket,将其返回到 指针引用FoundBucket 中。 如果Bucket中存在被查找的Key&Value,返回TURE。否则,返回 空Bucket 或 标记删除的Bucket 并 返回 FALSE。

VyKHsCla3bDDGuRVfBxU1WgxLLX0_LgWIkX7zcz5eGM.png

  1. LookupBucketFor 有两个同名函数不过参数不同,如图中箭头所示,标注出了方法最终的执行方向,最终的实现是上面参数中带有const BucketT *&FoundBucket 的方法。

  2. 第一块划分: 获取当前HashMap的Bucket和Bucket容量,在第一次进入 LookupBucketFor 进行查询是由于是NewMap,NumBuckets == 0BucketPrt == nil,所以会直接 Rerun,并且在随后的 InsertIntoBucket 中会有方法为Bucket开辟空间。

  3. **第二块划分:**准备接下来要进行比较的参数,以及通过Hash算法计算Bucket 中的空位,然后进入 while 循环。

  4. 第三快划分: 是整块代码的核心,找到对应Val的Bucket并返回,当然找不到的时候,也会返回 空Bucket 或者 标记删除的Bucket 。

    1. 最开始的 +BucketNO指针偏移,偏移整个 BucketT的长度。
    2. 进入While循环,大部分情况下前面两个IF就可以应对了,一个返回 对应Bucket,一个返回 EmptyBucket ,很少会返回 TombBucket
    3. 最后的再Hash,关键代码就是 ProbeAmt++

2.4、 InsertIntoBucket

frpC-0ZP-tToeI6VOVA_GYv1A47QOypExVSAyd5Hp0M.png

  1. InsertIntoBucketImpl 会对当前Map.Bucket进开辟空间,详细分析放到 2.5小节进行 ,其余代码也就只剩3行了。

  2. 使用 std::forward() 完美转发函数,为Bucket的First赋值。这里的值如果是第一层Map,就是 disguised 的地址, 如果是第二层Map,则是 setAssociateKey 的地址

  3. 同样,对 Second 进行赋值。如果是第一层Map,这里的Values是 ObjectAssociationMap ,会调用其构造函数进行初始化,开辟对应的Bucket空间,如果是第二层,这里的Values就是ObjcAssociation ,同样是调用其构造函数进行初始化,构函数是内部的逻辑是 swap(other)

  4. 最后,返回 Insert 好的 TheBuckettry_emplace 中去执行 std::make_pair

2.5、InsertIntoBucketImpl

DdD2iqW3gc_eASRB6VYbtkQxiEbTkvY5Xmz503q0At0.png

  1. 当第一次进入方法时,此时Map的 NewNumEntriesNumBuckets 都是0,所以会在IF的3/4分支中对于Map.Bucket进行开辟空间。

  2. 开辟空间的关键在于this->grow(NumBuckets * 2) ,虽然在第一次开辟时传入的NumBuckets*2 依旧等于0 ,但是 grow() 内部会用NextPowerOf2(AtLeast-1) 方法计算出大于AtLeast 的最小2次幂,用来分配新的 Buckets。

    • 因为AtLeast 前面有 unsigned 修饰,所以这里不会成 0-1 == -1
    • 用32位举例,对于 32 位无符号型整数来说,计算 0 - 1 会得到 4294967295,也就是 32 位无符号型整数类型能表示的最大整数。这与二进制位的按位加有关,至于为什么计算机底层要这么设计,可以更深入了解二进制数相关的知识。
  3. 然后调用 LookupBucketFor 使用 TheBucket 对于** Map.Bucket 进行引用**,最后返回 TheBucketInsertIntoBucket 中对其First、Second进行赋值,这一些列的方法前面都已经做过分析。 A0QrB6LsSqIU-VtRDFkNIC2E5vIw07DlgkHw50tHFHE.png

2.6、getAssociate

aLaEwuZBWc3H-uS3N0ftr8eF83SY-ZuYxB4faGwOgZc.png

  1. 现在看 _object_get_associative_reference 的逻辑 是不是像分析Set之前所说的-很简单。
  2. 而且整个代码基本和Set过程中, Value 没有值的 ELSE 分支差不多一样,这里不能有 erase() 操作。

三、总结

以上,就是关于 Extension 的简单分析,以及 Associate 底层实现原理分析的全部内容,其中Associate的分析思路是 从 Set方向进入然后是Get的方向作为加强理解, 所以对于 Set 过程要结合好结构图认真思考、理解,毕竟其中存在多层的存储和查找,不仔细分析理解,容易把自己找丢了。

至此,与 类-Cls 有关的内容就全部分析完了,下一篇博客开始准备分析 KVO&KVC 相关的内容。


篇中分析、记录的内容对你如有帮助,欢迎点赞👍、收藏✨、评论✍️。。如果发现错误,欢迎在评论中指正🙆🏻‍♂️