上篇文章讲到 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 就表明类的首地址
其他的一些环境变量及阐明: