底层探索 -- 类de加载过程分析(三)Category的附加

421 阅读8分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

继续上一篇底层探索 -- 类de加载过程分析(二)从map_iamges开始加载镜像的分析,最后遗留了一个方法没有分析,还有整块Cat附加相关的内容,现在开始。(本篇总字约2800字)

四、关于Category

4.1、Category的数据结构

maQad1bhuINXSU7dAsmFrxsU8mtX8-uz-wtD2nfI3ls.png

  1. Category在底层本质的数据类型是category_t ,可以直接在Objc源码中搜索就可以找到category_t 的数据结构,当然也可以使用Clang命令进行转换然后在cpp文件中查找

  2. 其中的name 存储的是类别名称,cls 就是类别要附加的主Cls,然后就是InstanceMethodsClsMethodsprotocolsMethods 的各个方法列表、最后一个List则是是记录Property的。

    1. 因为类别中没有MetaCls、isa、继承链那套关系,所以InstanceMethodsClsMethods 的数据结构就在同一个数据结构中。
    2. Category中虽然有PropertyList ,但是并不会为属性生成Set、Get方法,需要使用关联对象为属性添加方法,这点可以通过Clang后的cpp代码验证。

    dFvnNtH9yMZ3p_UYUkCIVXNKGm--z86I9kfzxAcwoUU.png

4.2、attachToClass

WGvjs1erSfjuIxxEKd1mAM4jXHg9TZfUgMVIpsU41Qg.png

接着来分析前面的章节 [[二、映射镜像 ]] 中,末尾故意遗留的方法 attachToClass

  1. 入参中的previously[[2.5、methodizeClass]] 中已经分析过,是一个备用参数一直都是nil,所以最终执行的attachToClass 入参的previouslyCls 是同一个对象。
  2. 并且看attachToClass 的代码逻辑,也是要进行attachCategories的,但是想要在attachToClass 中完成Attach,除了要在NonLazy-Cls的情况下进入方法,还要满足it != map.end() 这个判断条件。
    1. 如何能够满足这个条件,首先Cls肯定要是NonLazy的,然后Cat还要具备什么样子的情况下, 才会满足it != map.end() 条件?稍后在AttachCategory总结中再进行展开分析
  3. auto it :实际的数据类型是DenseMapIterator 类型,数据结构中有PtrEnd两个pointer类型变量,其中End 存储map.end() 的指针地址。
  4. 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 的代码逻辑:

PlUlHxw2B_5avr8vEOucTz3ho0KB7BdCwAMZo5Gr2pQ.png

  1. didInitialAttachCategories: 前面在分析_read_images 方法时,中间有一段代码块:[[8、LoadCategory判断]] ,它的进入条件didInitialAttachCategories 默认等于FALSE,而唯一能够使其 BOOL值改变的位置就在这里--第一次load_images 被调用的时候。
  2. didCallDyldNotifyRegister: 这个判断条件 默认值同样也是FALSE。用途见名知意,当_objc_init 中的_dyld_objc_notify_register() 执行完成之后,会将其值变为Ture。
  3. 在当前方法中有关于+load() 方法的一系列代码、方法的分析另起篇幅,不在这里进行深入展开。那么,接下来需要继续关注的方法,就是与Category有关的loadAllCategories 了。

4.4、AttachCategory

1、loadAllCategories

WxxNTzmM0Ggb0BUJkZ52yHIhkVdI3fsY1kDNHuoeuQ4.png

  1. loadAllCategories 方法的代码简单,见名知意,通过一个FOR循环获取到了所有的hi然后去执行AttachCategory。

  2. 先来看一下循环的条件:FirstHeader 展开是 header_info *FirstHeader = 0getNext() 就是用第一个hi不断的获取下一个。

  3. loadAllCategories 中可以通过getNext 获取到所有header_info ,这得益于在map_images_nolock 方法中进行 [[2、Cls数量计算代码块]] 的时候调用的addHeader 方法。 这个方法不仅Return hi 形成Array以供_read_images 进行使用,同时还将每次获取到的hi通过内部的appendHeader() 形成了链表,来看一下源码。

    -vRd4OWgWveP9TupDOzEdXLvAL8SSoVVFbrgNw5G_gM.png

2、load_categories_nolock

j1JUBHI2Thxq2s62Z3T_ADhTPMKXmlPAX4r9Lcq0KME.png

  1. load_categories_nolock 折叠后只有5行代码,简而言之就是一处processCatlist 实现,两处processCatlist 的调用入参不同。
  2. processCatlist 的实现中以hi获取到的CatList.Count为条件,循环取出List去向主Cls、主MateCls的MethodList进行Attach。
  3. 为了测试catlist2 中处理的是什么Cat、有哪些类需要在catlist2 中处理,所以完整Copy了一份processCatlist实现和count,结果运行起来一个断点都没有进入。

3、attachCategories

IzJbOqiwoKjLyNbP-AN93osVNyxFA3a3nAQW-OXf1TA.png

  1. attachCategories 方法整体来说,可大致划分成3部分看待:容器准备、统计各部分List、进行附加

  2. 第二部分循环统计各部分List并装入各自的容器, 其中cats_count 的数量如果按目前分析的路径从 load_categories_nolock 进入,那么就是1,如果从attachToClass 进入,那么就是list.count。 那么mlist中的 if (mcount == ATTACH_BUFSIZ) 也有可能执行。

  3. 第三部分代码中最重要的方法应该是 attachLists各部分的方法附加到主Cls都是调用的 attachLists , 所以分析完这里,接下来要继续跟进attachLists 了。

  4. 最后来说第一部分的准备过程,除了其他几个容器的声明外,最重要的部分是 extAllocIfNeeded 方法的调用,在前面的的流程中有分析过主Cls的MethodList加载,在 [[2.5、methodizeClass]] 中提出并留了一个问题:rwe 是什么时候创建的? 就是在这里创建的,extAllocIfNeeded 方法的内部逻辑是:如果rwe存在则返回,不存在则调用 extAlloc 进行创建,并且在初次创建时会将主Cls的MehtodList、PropertyList、ProtocolList进行插入。 来看一下这两个方法的源码:

    APTo8buIBjBvOoFECW3YSYsS4sXwINgwEKGh1JrEeKM.png

4.5、 *attachLists

这方法是个重点,刚也说过各部分MethodList的附加算法最终都是执行attachLists 方法。attachListsclass list_array_tt 中的方法,所以每个MethodList都能可以调用这个方法,方法入参是List指针和List中要被添加指针的Count。

h6nk3TXJVONt8rlphzC8aHXvIpNd-f5DEgpil45IgOI.png

  1. 先说最简单的,**图中标记2的分支,**是在创建rwe 的时候调用attachLists 会执行的分支,具体的代码在上一个小节中展示 extAlloc 方法源码中。

  2. 图中标记3的分支是第二次进行Attach时会执行的,作了个简单的动图方便理解。

    poeffj7B1RhrOD6Gczm0TXgV6DVt_71ed8LtAuLvrvo.GIF

  3. Array() 存在以后,之后的Attach操作就都是执行标记为1 的分支了,同样通过一个GIF来表现。

    _1wPhTQAhjRXVw6m4HfcM7sld7-W8wjh-3eIIrOK98Q.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种情况的方法执行路径:

  1. NonLazy-Cls + Cat

    map_imnages* -->* map_images_nolock --> _read_imnage --> reailClsassWithoutSwift (RealizedCls && Meta) --> methodizeClass --> attchToClass (只是看看)

  2. NonLazy-Cls + Cats

    1.NonLazy-Cls + Cat 执行路径相同

  3. 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)

  4. 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) }

  5. Cls + NonLazy-Cat

    1.NonLazy-Cls + Cat 方法调用路径相同 ,Cls是被迫Pre-Realized。

  6. 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)

  7. Cls + Cat(s)

    当Cls第一次被使用时 --> _objc_msgSend_uncached --> lookUpImpOrForward --> realizeClassWithoutSwift (RealizedCls && Meta)--> methodizeClass --> attchToClass (习惯性看看)

以上,就是对6种条件组合的执行路径的详细分析,同时路径标示出了Cat的附加时机,然后对上述的所有执行路径再次进行一下提炼总结:

  1. 条件1、2、5的执行路径相同,可以合并同类项。

    1. 无论如何Cls已经是NonLazy-Cls了,要在read_images 中进行Pre-Realize,但是Cat(s)并不是NonLazy,所以在reailClsassWithoutSwift 之前CatList已经附加到主Cls的MethodList中了,这点可以在Realized之前去读取并打印ro -> baseMethodlist 中的Method来验证。
  2. 条件3、4的执行路径相同,合并同类项。

    1. Cls和Cat都是NonLay时,AttachCat是在第一次loadimage 被调用的时候进行的,唯一看起来存在差异的地方就是load_categories_nolock 中的FOR循环,有几个Cat就要循环几次attachCategories 进行Attach
  3. 条件6,针对的是前面attachToClass 方法中的it != map.end() 这个条件。有多个NonLazy-Cat,且Cls是被迫NonLazy的,这时AttachCategory才会在 attachToClass 中完成。不过方法的调用顺序与Cls主动NonLazy的时候有很大的区别,要注意留意。

  4. 条件7, 普普通通的Normal,就是大多数的情况。当用到Cls的时候,才会在慢速查找流程lookUpImpOrForward 中进行Realized,这个流程已经在之前博客 [[三、慢速查找]] 中分析过。AttachCat的情况与1、2、5一样,在Cls进行Realize之前就已经完成了,同样可以通过读取打印ro进行验证。

    drawio2_batch.png

五、总结

至此,Cls的加载原理就全部分析完了。_objc_init 方法开始,对Runtime环境中各个部分的初始化、镜像加载、镜像映射的方法注册,然后是Cls中的Sel、Property、Protocol等内容的加载及最后的Cat附加等等一系列的方法、流程通通做了尽量详细的分析,再次呼应一下博客开始时候说的本篇重点:

本篇重点:

  1. _objc_init 方法对于runtime环境的初始化、ReadImage、LoadImage的方法注册。
  2. Readimage 的加载过程、各种记录表的创建、对Cls的实现、Cls中的数据加载。
  3. Category 的附加过程、附加时机、附加算法。

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