底层探索 -- 类de加载过程分析(二)从map_iamges开始加载镜像

480 阅读12分钟

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

接着上一篇类de加载过程分析(一)runtime初始化的末尾,由_dyld_objc_notify_register()引出了镜像映射的方法map_images,那么接下来就从map_images方法着手,继续展开后续的分析。(本篇总字约4330字)

二、映射镜像

2.1、map_images_nolock

ggNj0QoVpYEcfLUCAvMfVwAOOMxhIB0q4lnxvjPOadI.png

  1. map_images 方法注册后由dyld调起,主要逻辑就是加锁后,将参数****(count, paths, mhdrs)**** 又传入了map_images_nolock 进行处理
  2. 首先,从整体来看一下map_images_nolock 的大致代码逻辑-分析一下方法大致上做了什么操作,然后再对于重点部分进行展开分析-看看对于入参是如何使用的:

njiIWWsY1_54TPDF9yEkdGkuY8RNLIwlgHk_NwnNpsU.png

1、preopt_init

1wH53UQfYpyGLC2_N6fPw3YvWfdVwGi7_2xiW7Z9vnw.png

  • preopt_init**:预优化,只执行一次,**内部逻辑主要是setSharedCacheRange 以及对 (bool)preoptimized 这个布尔变量的赋值。

2、CalculateTotalCls

acA3nqi4wYZCHGVrwM1qz9EEIqb22dnPyNWP6UyfmHo.png

  1. 如图所示的代码块是map_images_nolock 中最重要的逻辑所在,通过While循环其完成了以下几件事:
    1. totalClasses 的计算
    2. SELRefCount 的计算
    3. hList[hCount++]hi填充,供_read_images 使用
  2. 再来梳理一下循环的大致逻辑:
    1. mhdrs 取出 structmach_header_64 headerType
    2. 传入addHeaderdyld 中查找header_info*
      1. IF:找到hihi->setLoaded(true);
      2. Else:找不到hi:从libobjc.A.dylib中读取对应的__objc_imageinfo,然后创建hi
      3. 顺便统计totalClassesunoptimizedTotalClasses
      4. appendHeader() 将构造好的hi添加到链表中 (这里的准备会在后Category附加时用到)
      5. 最后,添加hiHeadList
    3. 将返回的hi 执行 hList[hCount++] = hi

3、sel_init()

KXWiJCEhTMAAzCIoIRum1uUIuafv2Op-8SqQ8_RPxR8.png

  • 根据计算好的SelfRefCOunt 初始化SelectorTable,并注册构建、析构选择器。

4、arr_init()

31O6C8wEkCuL7vYs3Z8_ElPWWP2HKck7fqDGtCdE6o0.png

  • 自动释放池、引用计数表、关联对象表的初始化。

2.2、*_read_images

在通过map_images_nolock 方法前半部分的一系列准备工作后会将hList[mhCount] 完成装填,其中就是所有的hi ,接着就是调用_read_images 并会将hList 传入。那么接下来的分析后就要在_read_images 中继续展开,方法注释中对_read_images (共360行代码) 的作用描述是:对链接列表中以 headerList 开头的标头执行初始处理

依旧按之前的分析方式--先整体再局部,来看一下_read_images 是如何对标头进行初始化处理的:

BSMujAC-59z1mcWHQbGBFV43jjT9At6OX7MzunYNa8A.png

  • _read_images 进行整体分析后,刚好可以如图大致划分为10个部分,每部分的操作都对应hi 中的某块区域,下面就按编号来逐个分析记录:

    注意一点: 在开始分析之前,要注意看EACH_HEADER 这个宏定义的循环条件,_read_images 中的主要循环用都是这个宏。 宏中的细节:不仅是hIndex++,且同时对于hi进行了新的赋值:(hi = hList[hIndex])

1、doneOnce

WJyVPfGvqN21hKZlMPeeKzWv8Y5I05V6tLGCaITjGjc.png

  1. 如图对整个doneOnce 的逻辑进行了拆分,跳过OS、IWatch不谈,从上至下对doneOnce 中的逻辑进行一下总结:
    1. 改变条件变量:doneOncelaunchTime
    2. 初始化TaggedPointers混淆器
    3. *以TotalCls的1.3倍大小创建了gdb_objc_realized_classes 表。
  2. 对于gdb_objc_realized_classes 的说明:
    1. gdb_objc_realized_classes 并不是其名描述存储已实现的Cls,而是所有使用中的Cls无论是否realized。
    2. gdb_objc_realized_classes 中是将ClsName与Cls进行了映射
    3. Set方法:NXMapInsert(gdb_objc_realized_classes, name, cls)
    4. Get方法:NXMapGet(gdb_objc_realized_classes, name)
    5. ReadCls方法中会用到,主要联系记忆。

2、FixupSelectorRef

M5l6DO4MUBAahlrChVsMemE9N8kv5sWXodPpSfHEny0.png

  1. 如图,将FixupSelectorRef 的逻辑以两层循环作为分界分成了2块:
    1. 第一层循环,在_getObjc2SelectorRefs 中会通过hi->mhdr() 计算后的偏移量找到Data.__objc_selrefs 去获取Sles(注意此时hi、count的入参都是指针传递)
    2. 第二层循环,则是对取出的Sles 进行循环,并在sel_registerNameNoLock 中根据SleName到dyld中进行查找匹配sel找到return result,没找到则在DenseSet 中插入后返回。
    3. 最后判断if (sels[i] != sel) 比较的不单单是Stringsel 是有地址的:(SEL) $6 = 0x00007fff7c635d1d "class"
  2. 方法sel_cname 就是返回SelName: return (const char *)(void *)sel
  3. 关于sel_registerNameNoLock 更多细节可以到objc-sel.mm 中查看

3、DiscoverCls

5BTe7sjKssBnmXTzZPK0pOrFAXTUkA_OmJ2-satIKVk.png

  1. 这部分的代码逻辑同刚才的SelRef基本一致,2层循环先取出ClsLIst,在处理其中的Cls,只是多了些判断而已:
    1. 第一层循环,同理,在_getObjc2ClassList 中查找的位置是Data.__objc_selrefs
    2. 第二层循环,取出每个Cls,进入readclss() 中读取编译器编写的类及元类。
    3. 对于FutureCls的处理:如果newCls != cls条件成立,则不断创建Cls+1大小的resolvedFutureClasses 进行存储newCls,其中的Cls会在_read_images 的最后进行统一实现。
  2. readclss() 这个环节是 类加载过程中的重要环节,同时内容也比多,所以拆分成一个小节单独来分析。
  3. 最后补充一下mustReadClasses 方法的内部细节:判断当前的hi是否已经被优化。当条件成立时,说明Cls的各方面都已经准备完成:

YWwcP22RpikB5oVRRygvD9F_Cs-0RbZRMg938exgsSw.png

4、FixupClsRef

zeXQ8Y-m3QpoOmGNYO5ZHhNlDLtq0txvB6EqyReSK-M.png

  1. 首先是noClassesRemapped 的判断:内部逻辑是判断(remapped_class_map == nil) ,这里多数情况会返回YES。
    1. _getObjc2ClassRefs查找的是__objc_classrefs
    2. _getObjc2SuperRefs 查找的是 __objc_superrefs
  2. remapped_class_map 是一张LazyInitDenseMap类型的表,存储需要重新映射的Cls,一般情况下是不需要创建的。
  3. &classrefs[i] :这样是以指针传递的方式入参,传入的是i位置Cls的指针

8、LoadCategory判断

uQg--J9wLBZ9kPjLI0hgTqSkCdJ5HR_jEZiHeU22j1E.png

  1. didInitialAttachCategories: 默认==FALSE,经过测试、对改变变量BOOL值的代码段分析、还有注释中的描述,这里的load_category 是不会执行的。
  2. 对于启动时存在的Category,将推迟到调用 _dyld_objc_notify_register 完成后的第一个load_images 调用。

9、RealizedCls

ZI6I7mOF3d0r6hVLPpiaOOs91rMq93BkPYiQ0xmETlI.png

  1. 同样,第一层循环遍历了hi 读了其中的非懒加载类列表, 虽然调用了nlclslist 方法,但是nlclslist内部是调用了_getObjc2NonlazyClassList 读取__objc_nlclslist 区。
  2. 第二层循环,获取每个Cls的真实Address,并进去realizeClassWithoutSwift 对Cls执行首次初始化,包括分配其读写数据。
  3. 同样,realizeClassWithoutSwift 是类的加载过程中的核心方法,所以也拆分成一个小节单独来分析。
  4. 关于这部分循环,涉及到了对于Cls的2个概念:懒加载类、非懒加载类
    1. 非懒加载类:即在Cls中实现了+load 方法,或者 静态实例。
    2. 懒加载类:即什么时候用什么时候Realize的类。
    3. 所以,这里处理的都是非懒加载类,而一般的懒加载类则是在之前博客中分析的 OC的消息机制中的慢速查找中进行Realized。

10、RealizedFutureCls

h8nsGZRAUUat9NGroV_ImZq0j64DE2RIx3wx0U7IsPo.png

  1. 对于在 DiscoverCls 中统计的FutureCls进行统一的实现,实现的方法同NormalCls相同,realizeClassWithoutSwift 不过在方法内部做了区分。

2.3、readClass

现在来对readClass 方法进行分析,将readClass 的代码折叠后,并且按不同的操作划分成了5个部分,先对方法整体处理的现在来从上至下逐个分析一下局部的细节:

hr-Qap96OJt5UDyPEUnjFyzT6tfCYynQMPqXibne1n4.png

1、nonlazyMangledName

  • nonlazyMangledName :就是从ro中获取当前Cls的name,方法内就是通过bits.safe_ro()->getName() 获取Nmae。

2、missingWeakSuperclass

VWG2Is9yZ0pt-4VKXZFkegjlvEruDwbmVvDiFflaRuM.png

  • missingWeakSuperclass :递归判断当前Cls是否缺少SuperCls
    1. 当NSObject.Cls 或者 一直递归 到 NSObject.Cls 的时候,方法都会进入IF,return(!(2 & 2))

    2. 除非MissSuper时,在ELSE中会返回YES。但是正常missingWeakSuperclass 返回的都是NO,条件不成立

    3. 如果返回True ,当前Cls会被加入remappedClasses 表中并且设置SuperCls=nilremappedClasses 中的Cls会在完成readClass 完成后进行处理 (read_image第4块内容)

3、Memcpy Future RW

KV_OTtoUDRC0k5kNi5U6r463mTraf1o6PaOFGKuiFEI.png

  1. 这段代码的主要作用是 :存在FCls的情况下,保留Cls的关系链以及FCls的rw。
    1. 乍一看是对FCls的处理,并且有getRosetRw等操作像是在Realized。其实并不是,正在的Realizing还在后面。
  2. 接着再来看一下进入代码块的判断条件popFutureNamedClass ,它的实现也很简单: 根据 mangledNamefuture_named_class_map 中查找Cls
    1. 表存在且命中:说明当前的mangleName已经被FutureCls占用了,那就将Cls从future_named_class_map 移除并返回进入代码块处理。
    2. 表不存在,或没找到返回nil:那就跳过当前代码段。

4、Add Cls to AllocatedClasses

iuGar7nxrdrg8IzyAmXPrl19Wxi5K9_My2SDQcrNejo.png

  1. 这段代码的主要内容就是这2个方法:addNamedClassaddClassTableEntry

    1. addNamedClass将name与Cls对应起来,然后NXMapInsert(gdb_objc_realized_classes, name, cls) ,这个gdb_objc_realized_classes 表就是在前面 [[1、doneOnce ]] 的时候创建的,前面也提到过。
    2. addClassTableEntry将Cls、Cls->Super递归插入allocatedClasses,这个allocatedClasses[[1.3、runtime_init]] 时创建的,这个表的作用也在当时做了分析记录。

    C2TIgg_7sbYJqPOTZUQVcOyujvoWk1kM_KuzLK7QGwE.png

5、总结ReadCls

通过对于细节的分析可以看出,readClass 的作用也就是将Cls-ClasNAme这两信息关联起来,并将Cls插入AllocatedClasses 表中,总结一下它的整个流程:

mangleName --> checkSuper --> checkFuture --> addNamedClass --> addClassTableEntry --> return Cls

注意一点:
在这里总结一下有关FutureCls的处理有两处:

  1. read_image中DiscoverCls代码段中对于resolvedFutureClasses 的统计,及在方法末尾的循环对resolvedFutureClasses 的处理。
  2. readClass 中对于占用mangledName 的FutureCls.RW的保留。

2.4、realizeClassWithoutSwift

这个方法同样是类加载过程中的核心方法,在readClass将ClsAddress与ClasName进行了关联realizeClassWithoutSwift 则是对Cls执行首次初始化,包括分配其读写数据。先看将整个方法折叠来看一下大概逻辑,然后再进行细节分析:

kmaIX_5-WXfVmmn12TQOOvsYFGArW1WD_yvuoqcnn30.png

  • 对于realizeClassWithoutSwift 方法,按其功能大致可划分为如图所示的2个部分:上半部分是Cls的读写数据的创建、下半部分是对其读写数据的分配

1、创建读写数据

suvbhK4QMoCVxm1HJtxgEUIACYfJM4iSd2M3kQVxHtY.png

  1. 上半部分的方法就是在做一件事:递归对Cls的继承链、Cls元类、及元类的继承链上所有的类进行读写数据的创建,如图标注其中的1、2、3,形成一个循环,将当前Cls及与之相关的一些系列Cls进行统一的Realize并且标记Realized。
  2. 图中所示的步骤2是整个递归的终止条件,其中判断方法isRealized() 内部判断的就是Cls.data()->flag<<31 ,也就是步骤3指向的标记位。
  3. 如此,上半部的代码中最核心的也就是对Cls->rw 的创建(图中标记星号部分),根据条件判读对Cls、FCls进行Realize,对应_read_images 中9、10两个部分的Cls。

2、分配读写数据

3_CrVmQsaJ_76AGG-B21Ilej9p_w24aKiirBLRadIa8.png

  1. IF分支中对于 MetaCls.Isa 的处理,正好是之前博客 [[2)通过API理解isa]] 中去分析探索isa 时碰到的情况及猜想的证实:

    objc_opt_class 中通过getisa得到cls后做的判断,如果结果是MetaClass,返回的还是当前的调用者obj

  2. ELSE分支中则是对Instance.isa 的处理:如箭头所示两个bool变量最终的使用位置,通过中间的判断对其值进行改变。

    1. 如果改为Ture,则Instance.isa也是使用Rawisa,也就是纯指针。(其中还有一个DisableNonpointerIsa,就是在environ_init中初始化过得环境变量)
  3. 数据的写入:如SuperClsassinitIsa()Cache.SetInsataceSize()rw->flag 等等。

  4. 形成继承链

    1. Supercls不存在,那当前Cls无疑就是NSObject,将其标记为RootCls。
    2. 其他的Cls,则是通过nextSiblingClassfirstSubclass 将父类、子类串成一个链表。
    objc_debug_realized_class_generation_count++;        
    subcls->data()->nextSiblingClass = supercls->data()->firstSubclass;
    supercls->data()->firstSubclass = subcls;
    
  5. methodizeClass:方法注释中的描述是修复 cls 的方法列表、协议列表和属性列表吗,附加任何未完成的类别。具体是什么样的代码逻辑,现在来另起一小节来进行分析。

2.5、methodizeClass

F-c2uU3EC3CFJrIineLBJM0Snyv507EvvaROqVDX6sY.png

  1. 同样,首先对methodizeClass 的整块代码进行了大致划分:
    1. 第一部分是变量的准备。
    2. 第二部分是方法属性协议的方法列表的附加,但是此时rwe 还并未被创建 (何时创建呢?在后面的 [[3、attachCategories]] 小节中进行分析 )
    3. 第三部分则是附加Category,但是previously一直都nil,所以IF的分支跳过。
  2. 综上所述,methodizeClass 需要重点分析的就只有 prepareMethodListsattachToClass 两个部分了。

1、prepareMethodLists

此时的rwe还没有被创建,所以只需要关注prepareMethodLists 即可,现在进入方法看一下:

ImsyvCfrn2KVFsBVjhChw76XDXrAy9Xw1bDXP2w7uH4.png

  1. 需要关注的细节:入参时的addedLists 是个二级指针,所以在methosizeClass 调用时传入的是 &list
  2. 如图所示,整个方法的最核心的部分已标注,从addedLists 获取到了List指针后进入了fixupMethodList

2、fixupMethodList

_IJ7HNxqKXUwXmiKkKw-zn3KUwWXU_bLLTcyBZW02jY.png

  1. fixupMethodList 整体很清晰无需刻意划分,3个IF分支处理了3件事:

    1. registerName 向运行时注册方法。
    2. 对MethodList按Address进行排序。
    3. 将处理过的MethodList标记FixedUp。
  2. 第一个IF分支:

    1. meth.name()meth.setNmae() 方法内部分别对应SEL的big().name 的Get和Set,sel_cname() 是将值转为 Char* 类型。
    2. 重点是sel_registerNameNoLock :向 OC运行时注册一个方法,将MethodName映射到SEL,并返回SEL。

    JdUwpNwMKilJSv6NHZk78PwLdEU-DOWa9ONqpU5n4OE.png

  3. 第二个IF分支:

    1. 排序,根据SEL的地址进行排序,而不是方法名称。在这里使MethodLIst变的有序,就是之前博客 [[4. getMethodNoSuper_nolock ]] 中查找Method时对List使用二分法的前提。(二分法的使用前提必须是已排序,否则没有意义)
  4. 第三个IF分支:

    1. 标记FixUp前,判断List不是SmallList,因为它是immutable-不可变的,同样排序也不会对SmallList排序。

3、mList的isUniqued、isFixedUp、setFixedUp方法

M9lLU5wPeXI-VXbvMQdPx4E6i_AcKJOJMmrCAht8wdA.png

这几个方法的核心判断再于这几个参数上:fixed_up_method_list = 3uniqued_method_list = 1flags()entsize() ,其中FlagMask (好像= 0x3,Google百度都没查到,好几个Objc版本也都没找到具体定义的地方) 用于在 entsize 字段中隐藏多余的位,例如方法列表修正标记。

  1. isUniqued
    1. uniqued_method_list 比较entsizeAndFlags 的第0位,当第0位为0时,说明当前mList需要修复。
  2. isFixedUp
    1. fixed_up_method_list 比较的entsizeAndFlags 的0、1位。
  3. setFixedUp
    1. entsize() 方法&~后,作用要么是比较高位,要么就是清空低位,这里就是清空0、1位后又在位域中或入了fixed_up_method_list

三、小结一、二章

分析到目前为止,从方法调用的逻辑来讲类的加载过程基本已经分析的差不过了。从Runtime的初始化开始到mList的读取、Sel关联、排序等等都尽量详细的分析过了,总的来说分内容也不少,所以在进行最后一块内容分析之前,将之前分析的所有方法调用顺序使用流程图的方式进行一下梳理。

drawio_batch.png

还有一块什么内容没分析呢?Category的附加,什么时候附加?如何附加?并且刚才的methodizeClass 方法中最后的方法attachToClass 也没有分析,因为这个方法是与Category附加有关的,所以合并到稍后的Category附加过程分析中进行统计的记录。


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