我们初步探索了dyld和objc的关联,也因此引出将类加载到内存中,最关键的就是两个函数map_images和load_images
map_images:主要是管理文件中和动态库中的所有符号,即class、protocol、selector、category等load_images:加载执行load方法
静态库、动态库以及自己写的代码通过编译会生成mach-o形式的可执行文件,再从Mach-O中读取到内存。
map_images:加载镜像文件到内存
map_images方法的主要作用就是将Mach-O可执行文件中的类信息加载到内存
- 进入
map_images的源码
- 进入
map_images_nolock源码,其关键代码是_read_images
_read_images 源码实现
_read_images主要是加载类信息(即类、分类、协议等)到内存,进入_read_images源码实现,主要分为以下几部分:
- 条件控制进行的一次加载
- 修复预编译阶段的@selector的混乱问题
- 错误混乱的类处理
- 修复重映射一些没有被镜像文件加载进来的类
- 修复一些消息
- 当类里面有协议时:readProtocol 读取协议
- 修复没有被加载的协议
- 分类处理
- 类的加载处理
- 没有被处理的类,优化那些被侵犯的类
1、条件控制进行的一次加载
在doneOnce流程中通过NXCreateMapTable创建表,即创建一张类的哈希表 gdb_objc_realized_classes,目的是为了方便快捷地查找类
查看gdb_objc_realized_classes的注释说明,这张哈希表用于存储不在dyld共享缓存中且已命名的类,无论类是否实现。
2.修复预编译阶段的@selector的混乱问题
主要是通过_getObjc2SelectorRefs拿到Mach_O中的静态段__objc_selrefs,遍历列表调用sel_registerNameNoLock将SEL添加到namedSelectors哈希表中
3、错误混乱的类处理
主要是从Mach-O中取出所有类,在遍历进行处理。当某些类已经被移动了,但未被删除的时候,就会在这里进行处理
- 通过断点调试,在未执行
readClass方法前,cls只是一个地址 - 在执行完
readClass方法之后,newCls是一个类的名称
所以到了这一步,类的信息目前仅仅存储了地址+名称
4、修复重映射一些没有被镜像文件加载进来的类
主要是将未映射的Class 和Super Class进行重映射,其中
- _getObjc2ClassRefs是获取Mach-O中的静态段__objc_classrefs即类的引用
- _getObjc2SuperRefs是获取Mach-O中的静态段__objc_superrefs即父类的引用
- 通过注释可以得知,被remapClassRef的类都是懒加载的类,所以最初经过调试时,这部分代码是没有执行的
5、修复一些消息
主要是通过_getObjc2MessageRefs获取Mach-O的静态段__objc_msgrefs`,并遍历通过fixupMessageRef将函数指针进行注册,并fix为新的函数指针
6、当类里面有协议时:readProtocol 读取协议
7、修复没有被加载的协议
8、分类处理
分类的加载,会在后面的文章中介绍...
9、类的加载处理
主要是实现类的加载处理,实现非懒加载类
- 通过
_getObjc2NonlazyClassList获取Mach-O的静态段__objc_nlclslist非懒加载类表 - 通过
addClassTableEntry将非懒加载类插入类表,存储到内存,如果已经添加就不会载添加,需要确保整个结构都被添加 - 通过
realizeClassWithoutSwift实现当前的类,因为前面3中的readClass读取到内存的仅仅只有地址+名称,类的data数据并没有加载出来
10、没有被处理的类,优化那些被侵犯的类
主要是实现没有被处理的类,优化被侵犯的类
接下来 我们需要重点关注一下readClass以及realizeClassWithoutSwift这两个方法。
readClass:读取类
readClass主要是去读取类,在未执行readClass之前,cls只是一个地址,在执行完readClass之后,返回的newCls已经有了名称。
- 为了让自己定义的LGPerson类进来,我们开readClass源码里面写了一些判断条件
// 如果想自定义的类进入,自己加一个判断
const char *LGPersonName = "LGPerson";
if (strcmp(mangledName, LGPersonName) == 0) {
auto kc_ro = (const class_ro_t *)cls->data();
printf("%s -- 研究重点--%s\n", __func__,mangledName);
}
- readClass源码实现如下,关键代码是addNamedClass和addClassTableEntry
通过源码实现,主要分为以下几步:
- 通过
mangledName获取类的名字,其中mangledName方法的源码实现如下(todo:有变化)
其中nonlazyMangledName实现如下
其中installMangledNameForLazilyNamedClass如下
-
当前类的父类中若有丢失的weak-linked类,则返回nil
-
判断是不是后期需要处理的类,在正常情况下,不会走到popFutureNamedClass,因为这是专门针对未来待处理的类的操作,也可以通过断点调试,可知不会走到if流程里面,因此也不会对ro、rw进行操作
-
通过addNamedClass将当前类添加到已经创建好的gdb_objc_realized_classes哈希表,该表用于存放所有类
通过addClassTableEntry,将初始化的类添加到allocatedClasses表,这个表在dyld与objc的关联文章中提及过,是在_objc_init中的runtime_init就创建了allocatedClasses表
总结:
readClass的主要作用就是将Mach-O中的类读取到内存,即插入表中,但是目前的类仅有两个信息:地址以及名称,而mach-O的其中的data数据还未读取出来
realizeClassWithoutSwift:实现类
realizeClassWithoutSwift方法中有ro、rw的相关操作,这个方法在消息流程的慢速查找中有所提及,方法路径为:慢速查找(lookUpImpOrForward) -->realizeClassMaybeSwiftAndLeaveLocked --> realizeClassMaybeSwiftMaybeRelock --> realizeClassWithoutSwift(实现类)
realizeClassWithoutSwift方法主要作用是实现类,将类的data数据加载到内存中,主要有以下几部分操作:
- 读取
data数据,并设置ro、rw - 递归调用
realizeClassWithoutSwift完善继承链 - 通过
methodizeClass方法化类
【第一步】:读取data数据
读取class的data数据,并将其强转为ro,以及rw初始化和ro拷贝一份到rw中的ro
-
ro 表示 readOnly,即只读,其在编译时就已经确定了内存,包含类名称、方法、协议和实例变量的信息,由于是只读的,所以属于Clean Memory,而Clean Memory是指加载后不会发生更改的内存
-
rw表示readWrite,即可读可写,由于其动态性,可能会往类中添加属性、方法、添加协议,在最新的2020的WWDC的对内存优化的说明Advancements in the Objective-C runtime - WWDC 2020 - Videos - Apple Developer中,提到rw,其实在rw中只有10%的类真正的更改了它们的方法,所以有了rwe,即类的额外信息。对于那些确实需要额外信息的类,可以分配rwe扩展记录中的一个,并将其滑入类中供其使用。其中rw就属于dirty memory,而dirty memory是指在进程运行时会发生更改的内存,类结构一经使用就会变成ditry memory,因为运行时会向它写入新数据,例如 创建一个新的方法缓存,并从类中指向它
【第二步】递归调用 realizeClassWithoutSwift 完善 继承链
递归调用realizeClassWithoutSwift完善继承链,并设置当前类、父类、元类的rw
- 递归调用
realizeClassWithoutSwift设置父类、元类 - 设置父类和元类的isa指向
- 通过
addSubclass和addRootClass设置父子的双向链表指向关系,即父类中可以找到子类,子类中可以找到父类
这里有一个问题,realizeClassWithoutSwift递归调用时,isa找到根元类之后, 但根元类的isa指向自己,不会返回nil,所以有以下的递归终止条件,其目的是保证类只加载一次
- 在realizeClassWithoutSwift中,如果
类不存在,则返回nil,如果类已经实现,则直接返回cls。 所以根元类的isa虽然指向自己,但此时根元类已经实现,直接返回cls,递归终止。
- 在remapClass方法中,如果cls不存在,则直接返回nil
【第三步】通过 methodizeClass 方法化类
通过methodizeClass方法,从ro中读取方法列表(包括分类中的方法)、属性列表、协议列表赋值给rw,并返回cls
断点调试 realizeClassWithoutSwift
- 在LGPerson中重写+load函数
// 变成懒加载类,方便调试
+ (void)load {
}
realizeClassWithoutSwift方法中增加自定义逻辑
运行程序,让LGPerson进到_read_Images里面
跳转到下一个断点,进入realizeClassWithoutSwift, 并且此时的类就是LGPerson
通过x/4gx cls ,查看cls内存情况,此时bits值为0
这里我们需要去查看set_ro的源码实现
通过源码可知ro的获取主要分两种情况:有没有运行时
- 如果有运行时,从rw中读取
- 反之,如果没有运行时,从ro中读取
methodizeClass:方法化类
其中methodizeClass的源码实现如下,主要分为几部分:
- 将属性列表、方法列表、协议列表等贴到rwe中
- 附加分类中的方法(将在后面的文章中进行解释说明)
方法如何排序
- 进入
prepareMethodLists的源码实现,其内部是通过fixupMethodList方法排序
- 通过
fixupMethodList找到SortBySELAddress
- 在SortBySELAddress方法里面
return lhs.name < rhs.name,我们知道方法的排序是根据method_t里面的name的地址进行排序
attachLists
通过rwe->methods.attachLists(&list, 1),从list中添加方法列表到rwe的methods中
attachLists的源码如下:
- attachLists方法主要是将
类和分类的数据加载到rwe中
懒加载类和非懒加载类
区别: 非懒加载类实现了+load方法,而懒加载类没有。
懒加载类和非懒加载类的数据加载时机如下图所示: