上篇文章讲到 dyld 加载动态库时,会调用 notifyObjCInit 函数,去通知 objc 调用 +load 方法,通过 _dyld_objc_notify_register 与 libobjc 中的 _objc_init 进行通信,其中有三个个参数:map_images 、load_images 和 unmap_image, 其中 unmap_image 是在镜像消失时才调用,本篇文章就来探索一下 map_images 和load_images。
1. _objc_init
去 OC 源码里面查看一下该函数:
environ_init(): 环境变量初始化,可以在Edit Scheme -> Arguments添加一些环境变量tls_init(): 创建线程的析构函数,处理线程 key 的绑定static_init(): 运行 c++ 静态构造函数runtime_init(): 初始化两张表:分类的表和类的表exception_init(): 异常处理的初始化didCallDyldNotifyRegister: 标识对_dyld_objc_register的调用已完成
2. map_images
现在来探索一下 map_images:
由于
map_images_nolock 函数代码比较多,我们分段看下:
1. preopt_init 初始化环境
这一段最主要的函数就是当一次调用该函数时,会调用 preopt_init 来准备初始化环境
可以发现
preopt_init 作用主要就是初始化一些共享缓存,包括选择器的缓存、头的缓存、类的缓存、协议的缓存等
2. 获取并添加 image 的 header 指针
大致步骤如下:
- 首先获取
image的header指针,将mach_header的指针转换成headerType类型的指针 - 通过
addHeader获取header_info, 并将header_info插入到链表里 addHeader方法先从共享缓存中拿,如果有,就插入链表并返回addHeader方法如果没有从共享缓存中拿到,就会封装一个header_info,然后再插入链表并返回。
3. sel_init 和 arr_init
可以看到在第一次执行 map_images,会调用 sel_init 和 arr_init:
sel_init 主要是初始化 selector 表,该表定义如下:
namedSelectors 是个全局变量,存储所有的方法名 SEL,内部结构是 hash 表 DenseMap
可以看出 arr_init 主要做了以下几件事:
- 初始化自动释放池
AutoreleasePool SideTablesMap初始化AssociationsManager的初始化,即为全局使用的关联对象表开辟空间,关于关联对象,可以看下篇文章
4. _read_images
发现里面有很多代码,这也是 map_images 的核心所在
1. 初始化类表
通过注释有以下结论:
doneOnce保证了_read_images只执行一次gcd_objc_realized_classes是一个全局的类表,只要class没有在共享缓存中,那么不管其有没有实现都会存在这个类表里,其本质是个hash表
2. rebase
_getObjc2SelectorRefs 就是拿到 Mach-O 的静态段 __obj_selrefs, 后面所有通过 _getObjc2 开头的 Mach-O 静态段获取,都对应不同的 section name, 如下:
这段代码主要的作用就是将所有的 SEL 注册到 namedSelectors 表中,且当 _getObjc2SelectorRefs 中得到的 SEL 和 sel_registerNameNoLock 中的 SEL 不同时,就会把前者的 SEL 修正修复成后者,这一步就是 rebase, 修复镜像内部的资源指针。验证一下:
造成这两个函数地址不同的原因是 ALSR 偏移
3. 读取类
这一步的主要作用就是发现并读取类,readClass 是关键函数,在未调用该方法前,cls 只是一个地址,执行该方法后,cls 存入表中,是一个类的名称
可以看到有很多条件判断,那么我们自己加段代码,进行单步调试:
Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
const char *mangledName = cls->nonlazyMangledName();
const char *userClassName = "User";
auto user_ro = (const class_ro_t *)cls->data();
// 判断是否是元类
auto user_isMeta = user_ro->flags & RO_META;
if (strcmp(mangledName, userClassName) == 0 && !user_isMeta) {
// do something
}
...
}
打上断点,进行调试,发现 readClass 主要做了以下几件事:
addNamedClass, 该函数把name和cls添加到命名为gdb_objc_realized_classes的表中去,在addNamedClass方法中,调用NXMapInsert把cls和name插入到NXMapTable中,NXMapTable为hash表addClassTableEntry将类和元类加入到allocatedClasses表中
4. 修复类、消息、协议
这里的主要作用就是将未映射的 class 和 superclass 进行重映射
_getObjc2ClassRefs是获取Mach-O中的静态段__objc_classrefs, 即类的引用_getObjc2SuperRefs是获取Mach-O中的静态段__objc_superrefs, 即父类的引用fixupMessageRef修复一些消息的调用- 当类里面有协议时,通过
_getObjc2ProtocolList获取到Mach-O中的静态段__objc_protolist协议列表,即从编译器读取并初始化protocol - 修复没有被加载到的协议,通过
_getObjc2ProtocolRefs获取Mach-O的静态段__objc_protorefs, 然后遍历需要修复的协议,通过remapProtocolRef比较当前协议和协议列表中的同一个内存地址的协议是否相同,如果不同则替换
5. 分类的处理
下一篇文章处理
6. 非懒加载类的加载
非懒加载类和懒加载类的区别就是是否实现了 +load 方法
- 实现了
+load方法,就是非懒加载类 - 没实现就是懒加载类,因为
load会被提前加载,load方法会在load_images中调用
- 在
hi->nlclslist(&count)中通过_getObjc2NonlazyClassList获取Mach-O的静态段__objc_nlclslist非懒加载类表 addClassTableEntry将非懒加载类插入类表,存储到内存,如果已经添加就不会再添加,需要确保整个结构都被添加realizeClassWithoutSwift实现当前类,因为之前readClass读取到内存的只有地址和名称,类的data数据并没有加载出来。realizeClassWithoutSwift下一篇文章处理
7. 没有被处理的类
8. read_images 总结
- 加载所有的类到类的
gdb_objc_realized_classes表中 - 对所有类做重映射
- 将所有
SEL都注册到namedSeletors表中 - 修复函数指针
- 将所有
Protocol都添加到protocol_map表中 - 对所有
Protocol做重映射 - 初始化所有非懒加载类,对
rw、ro等进行操作(初始化) - 遍历已标记的懒加载类,并做初始化操作
- 处理所有
Category, 包括Class和Meta Class - 初始化所有未初始化的类
3. load_images
通过源码来看 load_images 主要做了以下几件事:
loadAllCategories加载所有分类,分类的加载下一篇文章处理prepare_load_methods找到load方法call_load_methods调用load方法
1. prepare_load_methods
- 这里的
schedule_class_load会进行一个递归调用,在找load方法时,会优先找父类的方法,然后再把找到的方法添加到loadable_classes表里面去 add_class_to_loadable_list会去找分类的load方法,找到之后同样会被添加到loadable_classes表里面去load方法不是走消息派发机制的,而是通过地址调用
2. call_load_methods
3. load 方法总结
- 子类的
load方法默认实现了父类的父类的load方法,所以不需要写super load load方法执行顺序:父类 > 本类 > 分类load方法内部使用了锁,所以时线程安全的- 有多个类别实现了
load方法时,load方法都会执行,执行顺序与编译顺序有关(在Build Phases -> Compile Sources里面查看编译顺序),后编译的先执行
拓展:环境变量的配置
在 _objc_init 方法中,可以看到会对环境变量进行初始化,以环境变量 OBJC_DISABLE_NONPOINTER_ISA 为例,该环境变量为是否开启指针优化,YES 表示纯指针,NO 就表示使用 nonpointerisa:
- 未设置 OBJC_DISABLE_NONPOINTER_ISA 时,对象的 isa 指针地址末尾为 1,默认开启了指针优化,表示 isa 不仅包含了类对象地址,还包含了类信息、对象的引用计数等
- 设置 OBJC_DISABLE_NONPOINTER_ISA 为 YES 后,isa 地址末尾变成了 0,此时的 isa 就表示类的首地址
其他的一些环境变量及说明: