一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情。
继续上一篇底层探索 -- 类de加载过程分析(二)从map_iamges开始加载镜像的分析,最后遗留了一个方法没有分析,还有整块Cat附加相关的内容,现在开始。(本篇总字约2800字)
四、关于Category
4.1、Category的数据结构
-
Category在底层本质的数据类型是
category_t
,可以直接在Objc源码中搜索就可以找到category_t
的数据结构,当然也可以使用Clang命令进行转换然后在cpp文件中查找。 -
其中的
name
存储的是类别名称,cls
就是类别要附加的主Cls,然后就是InstanceMethods
、ClsMethods
、protocolsMethods
的各个方法列表、最后一个List则是是记录Property的。- 因为类别中没有MetaCls、isa、继承链那套关系,所以InstanceMethods、ClsMethods 的数据结构就在同一个数据结构中。
- Category中虽然有
PropertyList
,但是并不会为属性生成Set、Get方法,需要使用关联对象为属性添加方法,这点可以通过Clang后的cpp代码验证。
4.2、attachToClass
接着来分析前面的章节 [[二、映射镜像 ]] 中,末尾故意遗留的方法 attachToClass
:
- 入参中的
previously
在 [[2.5、methodizeClass]] 中已经分析过,是一个备用参数一直都是nil,所以最终执行的attachToClass
入参的previously
和Cls
是同一个对象。 - 并且看
attachToClass
的代码逻辑,也是要进行attachCategories
的,但是想要在attachToClass
中完成Attach,除了要在NonLazy-Cls的情况下进入方法,还要满足it != map.end()
这个判断条件。- 如何能够满足这个条件,首先Cls肯定要是NonLazy的,然后Cat还要具备什么样子的情况下, 才会满足
it != map.end()
条件?稍后在AttachCategory总结中再进行展开分析。
- 如何能够满足这个条件,首先Cls肯定要是NonLazy的,然后Cat还要具备什么样子的情况下, 才会满足
- auto it :实际的数据类型是
DenseMapIterator
类型,数据结构中有Ptr
和End
两个pointer
类型变量,其中End
存储map.end()
的指针地址。 - flag:在
methodizeClass
中调用时已经通过isMeta
进行了判断入参,不是ATTACH_METACLASS
就是ATTACH_CLASS
,所以这里只可能进入ELSE分支执行attachCategories(cls, list.array(), list.count(), flags)
。
4.3、load_images
load_images
方法也是起初在_dyld_objc_notify_register()
中向dyld注册的回调方法,位于第二个参数,现来看一下load_images
的代码逻辑:
- didInitialAttachCategories: 前面在分析
_read_images
方法时,中间有一段代码块:[[8、LoadCategory判断]] ,它的进入条件didInitialAttachCategories
默认等于FALSE,而唯一能够使其 BOOL值改变的位置就在这里--第一次load_images
被调用的时候。 - didCallDyldNotifyRegister: 这个判断条件 默认值同样也是FALSE。用途见名知意,当
_objc_init
中的_dyld_objc_notify_register()
执行完成之后,会将其值变为Ture。 - 在当前方法中有关于
+load()
方法的一系列代码、方法的分析另起篇幅,不在这里进行深入展开。那么,接下来需要继续关注的方法,就是与Category有关的loadAllCategories
了。
4.4、AttachCategory
1、loadAllCategories
-
loadAllCategories
方法的代码简单,见名知意,通过一个FOR循环获取到了所有的hi然后去执行AttachCategory。 -
先来看一下循环的条件:
FirstHeader
展开是header_info *FirstHeader = 0
,getNext()
就是用第一个hi不断的获取下一个。 -
在
loadAllCategories
中可以通过getNext
获取到所有header_info
,这得益于在map_images_nolock
方法中进行 [[2、Cls数量计算代码块]] 的时候调用的addHeader
方法。 这个方法不仅Return hi
形成Array以供_read_images
进行使用,同时还将每次获取到的hi通过内部的appendHeader()
形成了链表,来看一下源码。
2、load_categories_nolock
load_categories_nolock
折叠后只有5行代码,简而言之就是一处processCatlist
实现,两处processCatlist
的调用入参不同。processCatlist
的实现中以hi获取到的CatList.Count为条件,循环取出List去向主Cls、主MateCls的MethodList进行Attach。- 为了测试
catlist2
中处理的是什么Cat、有哪些类需要在catlist2
中处理,所以完整Copy了一份processCatlist实现和count,结果运行起来一个断点都没有进入。
3、attachCategories
-
attachCategories
方法整体来说,可大致划分成3部分看待:容器准备、统计各部分List、进行附加。 -
第二部分循环统计各部分List并装入各自的容器, 其中
cats_count
的数量如果按目前分析的路径从load_categories_nolock
进入,那么就是1,如果从attachToClass
进入,那么就是list.count。 那么mlist中的if (mcount == ATTACH_BUFSIZ)
也有可能执行。 -
第三部分代码中最重要的方法应该是
attachLists
, 各部分的方法附加到主Cls都是调用的attachLists
, 所以分析完这里,接下来要继续跟进attachLists
了。 -
最后来说第一部分的准备过程,除了其他几个容器的声明外,最重要的部分是 extAllocIfNeeded 方法的调用,在前面的的流程中有分析过主Cls的MethodList加载,在 [[2.5、methodizeClass]] 中提出并留了一个问题:rwe 是什么时候创建的? 就是在这里创建的,
extAllocIfNeeded
方法的内部逻辑是:如果rwe
存在则返回,不存在则调用extAlloc
进行创建,并且在初次创建时会将主Cls的MehtodList、PropertyList、ProtocolList进行插入。 来看一下这两个方法的源码:
4.5、 *attachLists
这方法是个重点,刚也说过各部分MethodList的附加算法最终都是执行attachLists
方法。attachLists
是class list_array_tt
中的方法,所以每个MethodList都能可以调用这个方法,方法入参是List指针和List中要被添加指针的Count。
-
先说最简单的,**图中标记2的分支,**是在创建
rwe
的时候调用attachLists
会执行的分支,具体的代码在上一个小节中展示 extAlloc 方法源码中。 -
图中标记3的分支是第二次进行Attach时会执行的,作了个简单的动图方便理解。
-
当
Array()
存在以后,之后的Attach操作就都是执行标记为1 的分支了,同样通过一个GIF来表现。
4.6、AttachCategory总结
以前面分析过过程作为基础,现在对Category附加的情况及各情况下方法调用逻辑再进行一下梳理。
如果是 Lazy-Cls,那就什么时候用什么时候再说了。但是当Cls中实现有+load
方法时,Cls就会成为 NonLazy-Cls, 在_read_images
过程中就会进行 Pre-Realized。 这个过程中Category就可能会被附加到主Cls的MethodList中,有可能会延迟到第一次 loadImage
方法调用时进行附加,还有可能在ro中就已经是附加好了的。
那么对于上述的这几种可能性,在当Cls和Cat处于什么样的条件下会发生呢?为此列举所有的可以组合的条件总计6种,下面来详细的分析一下这6种情况的方法执行路径:
- NonLazy-Cls + Cat
map_imnages
* -->*map_images_nolock
-->_read_imnage
-->reailClsassWithoutSwift
(RealizedCls && Meta) -->methodizeClass
-->attchToClass
(只是看看) - NonLazy-Cls + Cats
与
1.NonLazy-Cls + Cat
执行路径相同 - NonLazy-Cls + NonLazy-Cat
map_imnages
-->map_images_nolock
-->_read_imnage
-->reailClsassWithoutSwift
(RealizedCls && Meta) -->methodizeClass
-->attchToClass
(还是看看) --> A few monments later...load_images
-->load_categories_nolock
-->attachCategories
(InstanceCls && MetaCls) - NonLazy-Cls + NonLazy-Cats
map_imnages
-->map_images_nolock
-->_read_imnage
-->reailClsassWithoutSwift
(RealizedCls && Meta) -->methodizeClass
-->attchToClass
(依旧看看) --> A few monments later...load_images
-->load_categories_nolock
--> FOR(Cats Count){attachCategories
(InstanceCls && MetaCls) } - Cls + NonLazy-Cat
与
1.NonLazy-Cls + Cat
方法调用路径相同 ,Cls是被迫Pre-Realized。 - Cls + NonLazy-Cat(s)
map_imnages
-->map_images_nolock
-->_read_imnage
-->load_images
-->load_categories_nolock
--> FOR(Cats Count){addForClass
(InstanceCls && MetaCls) -->prepare_load_methods
-->reailClsassWithoutSwift
(RealizedCls && Meta) -->methodizeClass
-->attchToClass
-->attachCategories
(InstanceCls && MetaCls) - Cls + Cat(s)
当Cls第一次被使用时
-->_objc_msgSend_uncached
-->lookUpImpOrForward
-->realizeClassWithoutSwift
(RealizedCls && Meta)-->methodizeClass
-->attchToClass
(习惯性看看)
以上,就是对6种条件组合的执行路径的详细分析,同时路径标示出了Cat的附加时机,然后对上述的所有执行路径再次进行一下提炼总结:
-
条件1、2、5的执行路径相同,可以合并同类项。
- 无论如何Cls已经是NonLazy-Cls了,要在
read_images
中进行Pre-Realize,但是Cat(s)并不是NonLazy,所以在reailClsassWithoutSwift
之前CatList已经附加到主Cls的MethodList中了,这点可以在Realized之前去读取并打印ro -> baseMethodlist
中的Method来验证。
- 无论如何Cls已经是NonLazy-Cls了,要在
-
条件3、4的执行路径相同,合并同类项。
- Cls和Cat都是NonLay时,AttachCat是在第一次
loadimage
被调用的时候进行的,唯一看起来存在差异的地方就是load_categories_nolock
中的FOR循环,有几个Cat就要循环几次attachCategories 进行Attach。
- Cls和Cat都是NonLay时,AttachCat是在第一次
-
条件6,针对的是前面
attachToClass
方法中的it != map.end()
这个条件。有多个NonLazy-Cat,且Cls是被迫NonLazy的,这时AttachCategory才会在attachToClass
中完成。不过方法的调用顺序与Cls主动NonLazy的时候有很大的区别,要注意留意。 -
条件7, 普普通通的Normal,就是大多数的情况。当用到Cls的时候,才会在慢速查找流程
lookUpImpOrForward
中进行Realized,这个流程已经在之前博客 [[三、慢速查找]] 中分析过。AttachCat的情况与1、2、5一样,在Cls进行Realize之前就已经完成了,同样可以通过读取打印ro进行验证。
五、总结
至此,Cls的加载原理就全部分析完了。 从 _objc_init
方法开始,对Runtime环境中各个部分的初始化、镜像加载、镜像映射的方法注册,然后是Cls中的Sel、Property、Protocol等内容的加载及最后的Cat附加等等一系列的方法、流程通通做了尽量详细的分析,再次呼应一下博客开始时候说的本篇重点:
本篇重点:
_objc_init
方法对于runtime环境的初始化、ReadImage、LoadImage的方法注册。- Readimage 的加载过程、各种记录表的创建、对Cls的实现、Cls中的数据加载。
- Category 的附加过程、附加时机、附加算法。
篇中分析、记录的内容对你如有帮助,欢迎点赞👍、收藏✨、评论✍️。。如果发现错误,欢迎在评论中指正🙆🏻♂️