可执行程序生成过程
-
预编译
:展开宏,头文件,生成.i文件 -
编译
:生成抽象语法树AST,AST 是抽象语法树,结构上比代码更精简,遍历起来更快,所以使用 AST 能够更快速地进行静态检查,同时还能更快地生成 IR(中间表示) -
汇编
:通过 IR 可以生成多份适合不同平台的机器码。对于 iOS 系统,IR 生成的可执行文件就是 Mach-O。 -
链接(dyld)
:执行可执行文件后,符号绑定到地址上
即App启动
->_dyld_start
->_dyld_main
,接下来是链接流程里的 环境变量设置、共享缓存、主程序初始化、链接主程序、动态库、弱符合绑定、执行初始化方法
、进入主程序入口main函数。
我们重点分析的是执行初始化方法
这一步。objc_init
方法中的_dyld_objc_notify_register
,作用是dyld注册
,仅在objc运行时使用。其中两个重要的流程是:
-
map_images
:dyld将image(镜像文件)加载进内存时,会触发该函数,主要是管理文件中和动态库中的所有符号
,其中的_read_images
读取加载类信息,即类
、分类
、协议
、方法编号SEL
等,将Mach-O中的类信息加载到内存。_read_images
一共分为10
个大步骤,每一步都在源码的for(EACH_HEADER)
循环里,并有相应的英文注释,感兴趣的一定要结合源码去看,而不是看文章纸上谈兵
。 -
load_image
:dyld初始化image会触发该函数,此方法在map_images
之后执行,加载执行load
方法。
_read_images中第三步的readClass
readClass
主要是读取类,在未调用该方法前,cls
只是一个地址
,执行该方法后,cls
是类的名称
。如果已经实例化
,则从ro
中获取name
,否则从mach-O
的数据data中获取name
,这里分析得一般是点击App后第一次来到这里的情况,所以一般是后者。这里还会将cls
插入到两个哈希表中,一个是gdb_objc_realized_classes总表
,一个是allocatedClasses已开辟内存空间的哈希表类的
。
_read_images中第九步的realizeClassWithoutSwift
realizeClassWithoutSwift
方法主要作用是进行非懒加载类的第一次初始化操作,将类加载进内存。将non-lazy 类加载进内存。
==这里注意==:realizeClassWithoutSwift
方法的作用是实现类,任何类都会调用这个方法,只是时机不同
。非懒加载类
会在map_images的read_images
阶段调用,而懒加载类
会在消息发送
的时候调用,即在消息慢速查找
流程中lookUpImpOrForward
中如果类未实现,也会调用realizeClassWithoutSwift
,快速查找
流程是查找缓存
,有缓存就已经说明类已经实现
了,已经调用过这个方法了。分类如果是非懒加载类会迫使主类在load_images阶段加载。
- 读取
macho
中的data
数据,并设置ro、rw
,ro
拷贝一份到rw
中的ro
, - 递归调用
realizeClassWithoutSwift
完善继承链
,以保证类的完整性
。设置cxx函数
。 - 调用
methodizeClass
方法化类:从ro
中读取方法列表
、属性列表
、协议
列表赋值给rw
,并返回cls
。这里还会对rwe不为空的条件下赋值,但第一次rwe都为空,rwe在分类添加到主类时
或者调用class_addXXXX即runtime添加方法、协议、属性时
才会初始化
创建rwe = cls->data()->extAllocIfNeeded()
。下面分析此方法。
methodizeClass方法化类
//拿方法列表源码来举例
method_list_t *list = ro->baseMethods();
if (list) {
prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls), nullptr);
if (rwe) rwe->methods.attachLists(&list, 1);
}
-
排序
:读取ro->baseMthods
,并调用prepareMethodLists
中的fixupMethodList
方法,根据sel address
排序方法,如果方法主类和分类重名
,其底层是比较sel(即name),否则比较imp的address地址。这也是我们在进行方法查找流程中对方法列表进行lookUpImpOrForward
二分查找的前提:保证方法列表中的方法是有序的
。
- ==准备== 分类中的
方法、属性、协议
。
methodizeClass
方法中最后的attachToClass
,attachToClass
中的attachCategories
方法中准备``分类
的数据,rwe也在这里初始化创建,rwe = cls->data()->extAllocIfNeeded();
将属性列表、方法列表、协议列表
等贴到rwe
中。贴的动作函数是attachLists
,分为一维、二维数组等情形,大体流程是将分类的信息内存平移
贴在原本数组的最前面
,即LRU算法思维,分类的方法优先执行,这也是分类的意义。
时机
到此在attachCategories
中分类数据已经准备好了,那最后是什么时机把分类数据贴到主类中去的呢?在attachCategories
打住断点会发现不同情况,调用栈也不同。这里分为四种情况。
【情况1】非懒加载类 + 非懒加载分类
,其分类数据的加载在load_images
方法中的loadAllCategories
,首先对类进行加载,然后把分类的信息贴到类中
【情况2】非懒加载类 + 懒加载分类
,其数据加载在read_image就加载数据,数据来自macho的data,data在编译时期就已经完成,即data中除了类的数据,还有分类的数据,与类绑定在一起
【情况3】懒加载类 + 懒加载分类
,其数据加载推迟到 第一次消息时,数据同样来自macho的data,data在编译时期就已经完成
【情况4】懒加载类 + 非懒加载分类
,只要分类实现了load,会迫使主类提前加载,即在_read_images中不会对类做实现操作,需要在load_images方法中触发类的数据加载,即rwe初始化,同时加载分类数据。注意此时类还是懒加载类,但是就是会像
非懒加载类似的提前加载。所以说懒加载类并不一定只会在第一次消息发送的时候加载,还要取决于有没有非懒加载的分类,如果有非懒加载的分类,那么就走的是 load_images 里面的 prepare_load_methods 的 realizeClassWithoutSwift 。
如果主类本身是非懒加载类,会在read_images阶段加载数据,如果是被迫加载,是在load_images阶段,此阶段是运行时机制,所以分类是运行时期决议的。只要有一个非懒加载分类,那么其他分类也会变成非懒加载分类,即使没实现+load。即要加载就全部加载,否则就不加载。
ro rw rwe
因为苹果系统中只有约10%的类修改过rw,所以为了节省内存,又引出了类的额外信息rwe
。对分类处理时才会进行初始化rwe
。
我们已知:类在编译期
时,类的一些数据信息保存在 ro
中,包含了类的名称,方法,协议,实例变量等 编译期确定
的信息。当类被Runtime
加载之后,runtime会为它分配额外的用于 读取/写入 的 rw
。
ro
是只读的,存放的是 编译期间就确定
的字段信息;而 rw
是在 runtime
时才创建的,它会先将 ro
的内容拷贝一份,再将类的分类、属性、方法、协议等信息添加进去,rw
的存在是因为iOS的运行时机制
,可以动态读写
。
而对于存在动态更改行为
的类,会将这部分动态的内容提取到rwe
中。
rwe
rwe是在methodizeClass
的attachToClass
中的attachCategories
中创建的,即auto rwe = cls->data()->extAllocIfNeeded()
,为什么在这个时候创建rwe
呢?rwe在分类添加到主类时或者调用class_addXXXX即runtime添加方法、协议、属性时才会初始化创建。因为这时候是要往本类中添加属性、方法、协议等,即对原来的 clean memory
要进行处理了,就是要修改本类
了。存在需要对原始类修改或处理时会初始化,一般在加载分类或通过runtime API添加的时候会初始化rwe,因为分类就是向原类添加方法、协议等。此时会对ro的methodList排序,并把分类的方法加载methodList的最前面。
- 以方法为例,首先往
rwe
中添加本类
的方法,即取ro->baseMethods()
添加至list
. - 以方法为例,往
rwe
中添加分类
的方法,会走到prepareMethodLists方法排序,然后mlists + ATTACH_BUFSIZ - mcount
为内存平移
拿到方法列表添加至list
。 - 最后将
list
赋值给rwe->methods
.注意:这里只有非懒加载类
的rwe
才会创建
。我们知道load
方法是在load_images
才会调用。实现了load方法就会将类由懒加载变为非懒加载
。
+load方法的意义就是在read_images阶段提前实现类,并在load_images阶段的时候能够顺利调用到+load方法,不然类都没实现,是调用不到的。
分类实现+load 时,在prepare_load_methods 时,会先调用realizeClassWithoutSwift ,确保本类先实现。
attachList方法插入
新
的方法list就是指分类
的方法,添加在数组的最前面
,旧
的方法排在后面
。这里就是运用了LRU算法思维
,常用的分类方法放在前面,优先调用。
load调用
call_load_methods
无论项目中是否用到
了这个类,调用类和类别中所有未决的 +load 方法,类里面 +load 方法是父类->子类->分类
,彼此不会覆盖。
1.通过 objc_autoreleasePoolPush
压栈一个自动释放池。
2.do-while 循环开始,循环调用类的 +load 方法直到找不到为止
3.调用一次分类中的 +load 方法。
4.通过 objc_autoreleasePoolPop
出栈一个自动释放池。
+ load
在runtime源码中,我们可以看到,+load方法是在load_images
中通过call_load_methods
调用的。
更具体的来说是在运行时加载镜像时,通过prepare_load_methods
方法将+load方法准备就绪
,而后执行call_load_methods
,调用+load方法。
+load方法是系统根据方法地址直接调用
,并不是objc_msgSend函数调用(isa,superClass);这就决定了如果子类没有实现
+load方法,那么当它被加载时runtime是不会调用父类的+load方法的。
每个类的load函数只会自动调用一次.由于load函数是系统自动加载的,因此不需要再调用[super load],否则父类的load函数会多次执行。如果不可避免的执行多次,需要
dispatch_once
来控制保证只执行一次放在load里或initialize里的代码。
调试小技巧
:
- 在调试底层代码时,会有很多系统的类呼啸而过,若想调试自定义的类,可以加个类名判断,即当前类是否是自定义类。
- 断点要研究的函数,然后
bt
打印调用栈反推逻辑。 - 源码调试时可以把一些系统的
private
的方法改为public
。 - 在 Diagnostics页面, 选中Thread Sanitizer,可以检测到多线程数据竞争问题
网友评论