一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
接着上一篇类de加载过程分析(一)runtime初始化的末尾,由
_dyld_objc_notify_register()
引出了镜像映射的方法map_images
,那么接下来就从map_images
方法着手,继续展开后续的分析。(本篇总字约4330字)
二、映射镜像
2.1、map_images_nolock
map_images
方法注册后由dyld调起,主要逻辑就是加锁后,将参数****(count, paths, mhdrs)**** 又传入了map_images_nolock 进行处理。- 首先,从整体来看一下
map_images_nolock
的大致代码逻辑-分析一下方法大致上做了什么操作,然后再对于重点部分进行展开分析-看看对于入参是如何使用的:
1、preopt_init
preopt_init
**:预优化,只执行一次,**内部逻辑主要是setSharedCacheRange
以及对(bool)preoptimized
这个布尔变量的赋值。
2、CalculateTotalCls
- 如图所示的代码块是
map_images_nolock
中最重要的逻辑所在,通过While循环其完成了以下几件事:totalClasses
的计算SELRefCount
的计算hList[hCount++]
的hi
填充,供_read_images
使用
- 再来梳理一下循环的大致逻辑:
- 从
mhdrs
取出 structmach_header_64 headerType
- 传入
addHeader
从dyld
中查找header_info*
- IF:找到
hi
:hi->setLoaded(
true);
- Else:找不到
hi
:从libobjc.A.dylib
中读取对应的__objc_imageinfo
,然后创建hi
- 顺便统计
totalClasses
、unoptimizedTotalClasses
appendHeader()
将构造好的hi添加到链表中 (这里的准备会在后Category附加时用到)- 最后,添加hi到HeadList中。
- IF:找到
- 将返回的
hi
执行hList[hCount++] = hi
- 从
3、sel_init()
- 根据计算好的
SelfRefCOunt
初始化SelectorTable,并注册构建、析构选择器。
4、arr_init()
- 自动释放池、引用计数表、关联对象表的初始化。
2.2、*_read_images
在通过map_images_nolock
方法前半部分的一系列准备工作后会将hList[mhCount]
完成装填,其中就是所有的hi
,接着就是调用_read_images
并会将hList
传入。那么接下来的分析后就要在_read_images
中继续展开,方法注释中对_read_images
(共360行代码) 的作用描述是:对链接列表中以 headerList 开头的标头执行初始处理。
依旧按之前的分析方式--先整体再局部,来看一下_read_images
是如何对标头进行初始化处理的:
- 对
_read_images
进行整体分析后,刚好可以如图大致划分为10个部分,每部分的操作都对应hi
中的某块区域,下面就按编号来逐个分析记录:注意一点: 在开始分析之前,要注意看
EACH_HEADER
这个宏定义的循环条件,_read_images
中的主要循环用都是这个宏。 宏中的细节:不仅是hIndex++,且同时对于hi进行了新的赋值:(hi = hList[hIndex])
1、doneOnce
- 如图对整个
doneOnce
的逻辑进行了拆分,跳过OS、IWatch不谈,从上至下对doneOnce
中的逻辑进行一下总结:- 改变条件变量:
doneOnce
、launchTime
- 初始化TaggedPointers混淆器
- *以TotalCls的1.3倍大小创建了gdb_objc_realized_classes 表。
- 改变条件变量:
- 对于
gdb_objc_realized_classes
的说明:gdb_objc_realized_classes
并不是其名描述存储已实现的Cls,而是所有使用中的Cls无论是否realized。gdb_objc_realized_classes
中是将ClsName与Cls进行了映射- Set方法:
NXMapInsert(gdb_objc_realized_classes, name, cls)
- Get方法:
NXMapGet(gdb_objc_realized_classes, name)
- 在ReadCls方法中会用到,主要联系记忆。
2、FixupSelectorRef
- 如图,将
FixupSelectorRef
的逻辑以两层循环作为分界分成了2块:- 第一层循环,在
_getObjc2SelectorRefs
中会通过hi->mhdr()
计算后的偏移量找到Data.__objc_selrefs
去获取Sles
。(注意此时hi、count的入参都是指针传递) - 第二层循环,则是对取出的
Sles
进行循环,并在sel_registerNameNoLock
中根据SleName到dyld
中进行查找匹配sel
:找到return result
,没找到则在DenseSet 中插入后返回。 - 最后判断
if (sels[i] != sel)
比较的不单单是String
,sel
是有地址的:(SEL) $6 = 0x00007fff7c635d1d "class"
- 第一层循环,在
- 方法
sel_cname
就是返回SelName:return (const char *)(void *)sel
- 关于
sel_registerNameNoLock
更多细节可以到objc-sel.mm
中查看
3、DiscoverCls
- 这部分的代码逻辑同刚才的SelRef基本一致,2层循环先取出ClsLIst,在处理其中的Cls,只是多了些判断而已:
- 第一层循环,同理,在
_getObjc2ClassList
中查找的位置是Data.__objc_selrefs
。 - 第二层循环,取出每个Cls,进入
readclss()
中读取编译器编写的类及元类。 - 对于FutureCls的处理:如果
newCls != cls
条件成立,则不断创建Cls+1大小的resolvedFutureClasses
进行存储newCls,其中的Cls会在_read_images
的最后进行统一实现。
- 第一层循环,同理,在
readclss()
这个环节是 类加载过程中的重要环节,同时内容也比多,所以拆分成一个小节单独来分析。- 最后补充一下
mustReadClasses
方法的内部细节:判断当前的hi
是否已经被优化。当条件成立时,说明Cls的各方面都已经准备完成:
4、FixupClsRef
- 首先是
noClassesRemapped
的判断:内部逻辑是判断(remapped_class_map == nil)
,这里多数情况会返回YES。_getObjc2ClassRefs
查找的是__objc_classrefs
区_getObjc2SuperRefs
查找的是__objc_superrefs
区
remapped_class_map
是一张LazyInitDenseMap
类型的表,存储需要重新映射的Cls,一般情况下是不需要创建的。&classrefs[i]
:这样是以指针传递的方式入参,传入的是i位置Cls的指针。
8、LoadCategory判断
- didInitialAttachCategories: 默认==FALSE,经过测试、对改变变量BOOL值的代码段分析、还有注释中的描述,这里的
load_category
是不会执行的。 - 对于启动时存在的Category,将推迟到调用
_dyld_objc_notify_register
完成后的第一个load_images
调用。
9、RealizedCls
- 同样,第一层循环遍历了
hi
读了其中的非懒加载类列表, 虽然调用了nlclslist
方法,但是nlclslist
内部是调用了_getObjc2NonlazyClassList
读取__objc_nlclslist
区。 - 第二层循环,获取每个Cls的真实Address,并进去
realizeClassWithoutSwift
对Cls执行首次初始化,包括分配其读写数据。 - 同样,
realizeClassWithoutSwift
是类的加载过程中的核心方法,所以也拆分成一个小节单独来分析。 - 关于这部分循环,涉及到了对于Cls的2个概念:懒加载类、非懒加载类
- 非懒加载类:即在Cls中实现了
+load
方法,或者 静态实例。 - 懒加载类:即什么时候用什么时候Realize的类。
- 所以,这里处理的都是非懒加载类,而一般的懒加载类则是在之前博客中分析的 OC的消息机制中的慢速查找中进行Realized。
- 非懒加载类:即在Cls中实现了
10、RealizedFutureCls
- 对于在 DiscoverCls 中统计的FutureCls进行统一的实现,实现的方法同NormalCls相同,
realizeClassWithoutSwift
不过在方法内部做了区分。
2.3、readClass
现在来对readClass
方法进行分析,将readClass
的代码折叠后,并且按不同的操作划分成了5个部分,先对方法整体处理的现在来从上至下逐个分析一下局部的细节:
1、nonlazyMangledName
- nonlazyMangledName :就是从ro中获取当前Cls的name,方法内就是通过
bits.safe_ro()->getName()
获取Nmae。
2、missingWeakSuperclass
missingWeakSuperclass
:递归判断当前Cls是否缺少SuperCls-
当NSObject.Cls 或者 一直递归 到 NSObject.Cls 的时候,方法都会进入IF,
return(!(2 & 2))
-
除非MissSuper时,在ELSE中会返回YES。但是正常
missingWeakSuperclass
返回的都是NO,条件不成立。 -
如果返回
True
,当前Cls会被加入remappedClasses
表中并且设置SuperCls=nil
,remappedClasses
中的Cls会在完成readClass
完成后进行处理 (read_image第4块内容)
-
3、Memcpy Future RW
- 这段代码的主要作用是 :存在FCls的情况下,保留Cls的关系链以及FCls的rw。
- 乍一看是对FCls的处理,并且有getRo、setRw等操作像是在Realized。其实并不是,正在的Realizing还在后面。
- 接着再来看一下进入代码块的判断条件
popFutureNamedClass
,它的实现也很简单: 根据mangledName
在future_named_class_map
中查找Cls- 表存在且命中:说明当前的mangleName已经被FutureCls占用了,那就将Cls从
future_named_class_map
移除并返回进入代码块处理。 - 表不存在,或没找到返回nil:那就跳过当前代码段。
- 表存在且命中:说明当前的mangleName已经被FutureCls占用了,那就将Cls从
4、Add Cls to AllocatedClasses
-
这段代码的主要内容就是这2个方法:
addNamedClass
和addClassTableEntry
addNamedClass
:将name与Cls对应起来,然后NXMapInsert(gdb_objc_realized_classes, name, cls)
,这个gdb_objc_realized_classes
表就是在前面 [[1、doneOnce ]] 的时候创建的,前面也提到过。addClassTableEntry
:将Cls、Cls->Super递归插入allocatedClasses 中,这个allocatedClasses
在 [[1.3、runtime_init]] 时创建的,这个表的作用也在当时做了分析记录。
5、总结ReadCls
通过对于细节的分析可以看出,readClass
的作用也就是将Cls-ClasNAme这两信息关联起来,并将Cls插入AllocatedClasses
表中,总结一下它的整个流程:
mangleName --> checkSuper --> checkFuture --> addNamedClass --> addClassTableEntry --> return Cls
注意一点:
在这里总结一下有关FutureCls的处理有两处:
read_image
中DiscoverCls代码段中对于resolvedFutureClasses
的统计,及在方法末尾的循环对resolvedFutureClasses
的处理。readClass
中对于占用mangledName
的FutureCls.RW的保留。
2.4、realizeClassWithoutSwift
这个方法同样是类加载过程中的核心方法,在readClass
中将ClsAddress与ClasName进行了关联,realizeClassWithoutSwift
则是对Cls执行首次初始化,包括分配其读写数据。先看将整个方法折叠来看一下大概逻辑,然后再进行细节分析:
- 对于
realizeClassWithoutSwift
方法,按其功能大致可划分为如图所示的2个部分:上半部分是Cls的读写数据的创建、下半部分是对其读写数据的分配。
1、创建读写数据
- 上半部分的方法就是在做一件事:递归对Cls的继承链、Cls元类、及元类的继承链上所有的类进行读写数据的创建,如图标注其中的1、2、3,形成一个循环,将当前Cls及与之相关的一些系列Cls进行统一的Realize并且标记Realized。
- 图中所示的步骤2是整个递归的终止条件,其中判断方法
isRealized()
内部判断的就是Cls.data()->flag<<31
,也就是步骤3指向的标记位。 - 如此,上半部的代码中最核心的也就是对
Cls->rw
的创建(图中标记星号部分),根据条件判读对Cls、FCls进行Realize,对应_read_images
中9、10两个部分的Cls。
2、分配读写数据
-
IF分支中对于
MetaCls.Isa
的处理,正好是之前博客 [[2)通过API理解isa]] 中去分析探索isa 时碰到的情况及猜想的证实:objc_opt_class
中通过getisa
得到cls后做的判断,如果结果是MetaClass,返回的还是当前的调用者obj。 -
ELSE分支中则是对
Instance.isa
的处理:如箭头所示两个bool变量最终的使用位置,通过中间的判断对其值进行改变。- 如果改为Ture,则Instance.isa也是使用Rawisa,也就是纯指针。(其中还有一个DisableNonpointerIsa,就是在environ_init中初始化过得环境变量)
-
数据的写入:如
SuperClsass
、initIsa()
、Cache.SetInsataceSize()
、rw->flag
等等。 -
形成继承链
- Supercls不存在,那当前Cls无疑就是NSObject,将其标记为RootCls。
- 其他的Cls,则是通过
nextSiblingClass
、firstSubclass
将父类、子类串成一个链表。
objc_debug_realized_class_generation_count++; subcls->data()->nextSiblingClass = supercls->data()->firstSubclass; supercls->data()->firstSubclass = subcls;
-
methodizeClass:方法注释中的描述是修复 cls 的方法列表、协议列表和属性列表吗,附加任何未完成的类别。具体是什么样的代码逻辑,现在来另起一小节来进行分析。
2.5、methodizeClass
- 同样,首先对
methodizeClass
的整块代码进行了大致划分:- 第一部分是变量的准备。
- 第二部分是方法属性协议的方法列表的附加,但是此时
rwe
还并未被创建 (何时创建呢?在后面的 [[3、attachCategories]] 小节中进行分析 )。 - 第三部分则是附加Category,但是previously一直都nil,所以IF的分支跳过。
- 综上所述,
methodizeClass
需要重点分析的就只有prepareMethodLists
、attachToClass
两个部分了。
1、prepareMethodLists
此时的rwe还没有被创建,所以只需要关注prepareMethodLists
即可,现在进入方法看一下:
- 需要关注的细节:入参时的
addedLists
是个二级指针,所以在methosizeClass
调用时传入的是&list
。 - 如图所示,整个方法的最核心的部分已标注,从
addedLists
获取到了List指针后进入了fixupMethodList
2、fixupMethodList
-
fixupMethodList
整体很清晰无需刻意划分,3个IF分支处理了3件事:- registerName 向运行时注册方法。
- 对MethodList按Address进行排序。
- 将处理过的MethodList标记FixedUp。
-
第一个IF分支:
meth.name()
、meth.setNmae()
方法内部分别对应SEL的big().name
的Get和Set,sel_cname()
是将值转为Char*
类型。- 重点是
sel_registerNameNoLock
:向 OC运行时注册一个方法,将MethodName映射到SEL,并返回SEL。
-
第二个IF分支:
- 排序,根据SEL的地址进行排序,而不是方法名称。在这里使MethodLIst变的有序,就是之前博客 [[4. getMethodNoSuper_nolock ]] 中查找Method时对List使用二分法的前提。(二分法的使用前提必须是已排序,否则没有意义)
-
第三个IF分支:
-
标记FixUp前,判断List不是SmallList,因为它是immutable-不可变的,同样排序也不会对SmallList排序。
-
3、mList的isUniqued、isFixedUp、setFixedUp方法
这几个方法的核心判断再于这几个参数上:fixed_up_method_list = 3
、uniqued_method_list = 1
、flags()
、entsize()
,其中FlagMask
(好像= 0x3,Google百度都没查到,好几个Objc版本也都没找到具体定义的地方) 用于在 entsize
字段中隐藏多余的位,例如方法列表修正标记。
- isUniqued
uniqued_method_list
比较entsizeAndFlags
的第0位,当第0位为0时,说明当前mList需要修复。
- isFixedUp
fixed_up_method_list
比较的entsizeAndFlags
的0、1位。
- setFixedUp
entsize()
方法&~后,作用要么是比较高位,要么就是清空低位,这里就是清空0、1位后又在位域中或入了fixed_up_method_list
。
三、小结一、二章
分析到目前为止,从方法调用的逻辑来讲类的加载过程基本已经分析的差不过了。从Runtime的初始化开始到mList的读取、Sel关联、排序等等都尽量详细的分析过了,总的来说分内容也不少,所以在进行最后一块内容分析之前,将之前分析的所有方法调用顺序使用流程图的方式进行一下梳理。
还有一块什么内容没分析呢?Category的附加,什么时候附加?如何附加?并且刚才的methodizeClass
方法中最后的方法attachToClass
也没有分析,因为这个方法是与Category附加有关的,所以合并到稍后的Category附加过程分析中进行统计的记录。
篇中分析、记录的内容对你如有帮助,欢迎点赞👍、收藏✨、评论✍️。。如果发现错误,欢迎在评论中指正🙆🏻♂️