已经通过之前的几篇博客。将有关于Cls数据结构相关的内容基本分析完了,包括Cls在底层的本质、Cls中的数据结构、Cls及Category的各种数据结构的内容加载等等一系列的底层源码。本篇衔接着前一篇分析完的Category,再分析2个与Cat相关的内容:类扩展和关联对象。 ((本篇总字约2800字))
一、 类扩展(Extension)
类扩展,这个叫法听起来可能会比较陌生,但是只要是写代码类扩展肯定会被用到,不过是在用的时候不知道那个结构就叫做类扩展罢了,现在来看看到底什么是类扩展:
-
如图中标注的就是类扩展,也可以说是特殊的Cat或者将其理解为匿名Cat,从Method、Property、Ivars三个方面将Ext与Cat做一下简单比较:
-
Extension添加的Mehtod、Property跟随着Cls的加载就完成了,不再像Cat一样根据不同的情况Attach的顺序会有不同,这一点可以通过之前博客中分析
MethodList时使用的偏移方法去获取输出baseMethodList验证。
二、关联对象(Associate)
开始分析Associate之前,先将Associate的存储结构图放到最开始,便于对 setAssociate 存储逻辑在整体上有初步的了解:
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源码中的差异比较:
- 比较的源代码版本是781和818这两个版本,在717中通过
SetAssocHook调用的方式在818中进行了简化,通过上图的比较可以看得出:无论如何Set_Associate的最终实现方法都是_object_set_associative_reference(object, key, value, policy)。 - 对于
Associate的分析,先从Set方向去分析底层源码的实现逻辑,将存的方法调用顺序、逻辑都梳理清晰了,取值过程中的方法看起来就会变得很简单了。
2.1、setAssociated
首先来看一下_object_set_associative_reference 方法的整体逻辑,大致可以分这3个部分:参数准备、存储过程、释放对象&更新isa:
-
参数准备: 抛开两个简单的IF判断不说,分析一下
disguised和association。-
disguised:是将传入的Objc对象进行统一的数据结构包装。DisguisedPtr内部的实现逻辑是:将object的地址转换为uintptr后取反,然后存到其内部变量uintptr_t value中,还原Object则按转换的方式逆向操作即可。 -
association:与disguised相同,调用的是ObjcAssociation类中的第一种构造函数,将Policy和Value初始化到association内部变量_policy、_value中。 -
紧接着,调用
association.acquireValue()根据_policy对内部变量_value增加引用计数继续持有 (Retain或Copy)。 -
如图左侧红色箭头所示,标注出了
association的生命周期,以及在存值过程中被使用到的位置,主要是在Manager.lock的作用域内,以及最后的 Release。
-
-
存储过程: 整个存的过程是在大括划分划分的Manager.lock的作用域中完成的。
-
创建
manager:这里是调用了AssociationsManager的C++构造函数,类比于OC中的init。manager可以创建多个,但是每次只能使用一个,所以创建出来的 manager 也并不是单例。 -
获取
HashMap:这里通过manager.get初始化的AssociationsHashMap才是全局唯一,那么HashMap 是如何通过manager.get获取来的,怎么就会是全局唯一呢?看一下其源码中的获取过程: -
IF分支: 根据入参的
Value是否有值以进行不同的 HashMap 操作,Value没有值的情况与有值的的情况对于Bucket的查找原理是一模一样的 、调用的方法名大同小异而已。所以只要对 常规情况,有值的代码分支进行分析即可 ,虽然只有几行代码实现,但是个别方法内部实现又有其他方法调用,所以 另起2.2小节 进行分析。
-
-
释放对象&更新isa:
-
IF内方法是否执行,根据存储开始之前的BOOL变量
isFirstAssociation进行判断,方法的作用是:第一次设置Associate的时候对isa中的has_assoc进行更新,以便将来在释放对象时,其中如果存在Associate则要进行清除关联。 -
在对
Value操作完成之后,要对临时变量association进行释放,同时也是对其内部_value持有的引用计数--,association在整个方法中的生命周期刚才也提到了。
-
2.2、try_emplace
首先一句话概括一下 try_emplace 的作用:插入键值对到HashMap中。 其次,try_emplace 在前一篇博客对Category的附加分析中就曾经出现过,在 addForClass() 中。现在来看一下它的源码实现:
-
整体上方法逻辑如图,就是IF的两个分支:找到Bucket、没找到Bucket,接下来分析其中的逻辑:
-
准备一个空的 BucketT指针:
TheBucket,这个指针会在LookupBucketFor被赋值,也就是通过指针传递参数保持内外同步。 -
调用
LookupBucketFor进行查找,在LookupBucketFor中会Get当前 (try_emplace 的调用者)this.Buckets,查找流程在2.3小节展开。- 如果当前Key已经存在于HashMap中,要注意
std::make_pair的second值设置的是FALSE。 - 如果没有找到,那么就调用
InsertIntoBucket对当前的this.Bucket进行 Key&Value 的插入,并对 Bucket 的容量进行调整。
- 如果当前Key已经存在于HashMap中,要注意
-
完成插入后依然是构建一个
std::pair返回,这时 Pair 的 second 值设置的是TURE ,表明是第一次进行 SetAssociate。- 这里的BOOL变化影响着
objc_getAssociatedObject中的isFirstAssociation、result.second、refs_result.second的判断。
- 这里的BOOL变化影响着
注意一点:
这里的方法入参(const KeyT &Key, Ts &&... Args),单从objc_getAssociatedObject作为入口来说,&Key 第一次是 disguised ,第二次是才是 SetAssociateKey,要注意区别,当然这也对应着两层不同的HashMap。
知识点补充:
std::pair主要作用是将两个数据组合成一个数据,两个数据的类型可同类型或者不同类型。
- 比如
std::pair<int,float>或std::pair<double,double>。std::pair的实质是一个结构体,其主要的两个成员变量是first和second,两个变量可以直接使用。- 初始化一个
std::pair可以使用构造函数,也可以使用std::make_pair函数。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。
-
LookupBucketFor有两个同名函数不过参数不同,如图中箭头所示,标注出了方法最终的执行方向,最终的实现是上面参数中带有const BucketT *&FoundBucket的方法。 -
第一块划分: 获取当前HashMap的Bucket和Bucket容量,在第一次进入
LookupBucketFor进行查询是由于是NewMap,NumBuckets == 0、BucketPrt == nil,所以会直接Rerun,并且在随后的InsertIntoBucket中会有方法为Bucket开辟空间。 -
**第二块划分:**准备接下来要进行比较的参数,以及通过Hash算法计算Bucket 中的空位,然后进入 while 循环。
-
第三快划分: 是整块代码的核心,找到对应Val的Bucket并返回,当然找不到的时候,也会返回 空Bucket 或者 标记删除的Bucket 。
- 最开始的
+BucketNO是 指针偏移,偏移整个 BucketT的长度。 - 进入While循环,大部分情况下前面两个IF就可以应对了,一个返回 对应Bucket,一个返回
EmptyBucket,很少会返回TombBucket。 - 最后的再Hash,关键代码就是 ProbeAmt++
- 最开始的
2.4、 InsertIntoBucket
-
在
InsertIntoBucketImpl会对当前Map.Bucket进开辟空间,详细分析放到 2.5小节进行 ,其余代码也就只剩3行了。 -
使用
std::forward()完美转发函数,为Bucket的First赋值。这里的值如果是第一层Map,就是disguised的地址, 如果是第二层Map,则是setAssociateKey的地址。 -
同样,对 Second 进行赋值。如果是第一层Map,这里的Values是
ObjectAssociationMap,会调用其构造函数进行初始化,开辟对应的Bucket空间,如果是第二层,这里的Values就是ObjcAssociation,同样是调用其构造函数进行初始化,构函数是内部的逻辑是swap(other)。 -
最后,返回 Insert 好的
TheBucket到try_emplace中去执行std::make_pair。
2.5、InsertIntoBucketImpl
-
当第一次进入方法时,此时Map的
NewNumEntries、NumBuckets都是0,所以会在IF的3/4分支中对于Map.Bucket进行开辟空间。 -
开辟空间的关键在于
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 位无符号型整数类型能表示的最大整数。这与二进制位的按位加有关,至于为什么计算机底层要这么设计,可以更深入了解二进制数相关的知识。
- 因为
-
然后调用
LookupBucketFor使用TheBucket对于** Map.Bucket 进行引用**,最后返回TheBucket在InsertIntoBucket中对其First、Second进行赋值,这一些列的方法前面都已经做过分析。
2.6、getAssociate
- 现在看
_object_get_associative_reference的逻辑 是不是像分析Set之前所说的-很简单。 - 而且整个代码基本和Set过程中, Value 没有值的 ELSE 分支差不多一样,这里不能有
erase()操作。
三、总结
以上,就是关于 Extension 的简单分析,以及 Associate 底层实现原理分析的全部内容,其中Associate的分析思路是 从 Set方向进入然后是Get的方向作为加强理解, 所以对于 Set 过程要结合好结构图认真思考、理解,毕竟其中存在多层的存储和查找,不仔细分析理解,容易把自己找丢了。
至此,与 类-Cls 有关的内容就全部分析完了,下一篇博客开始准备分析 KVO&KVC 相关的内容。
篇中分析、记录的内容对你如有帮助,欢迎点赞👍、收藏✨、评论✍️。。如果发现错误,欢迎在评论中指正🙆🏻♂️