近期学习了 QQWallet 团队开源的Cocoa Hot Reload热重载工具,和热重载老工具injection不同的是,这款开源的工具支持了 iOS13的真机热重载。
经过和Cocoa Hot Reload团队成员的交流,和把之前对injection研究的知识点进行串联,梳理成此文。
链接器和加载器
之所以介绍链接器和加载器,是要回忆一下当点击编译时,编译器做了什么,并进一步回忆动态库的概念。
1 | 1.生成汇编文件; |
链接器的定义
汇编器生成源程序的目标代码(能被CPU直接识别的二进制代码),并将其交给链接器。链接器采用此目标代码并生成 可执行代码 程序,并将其移交给加载程序。
高级语言,程序有一些 内置库 和 头文件。源程序可能包含一些库函数,其定义存储在内置库中。链接器将这些功能链接到内置库。如果找不到内置库,它将通知编译器,然后编译器将生成错误。
我们有两种类型的链接器:
- 链接编辑器:这是一个链接器,可生成可重定位的可执行模块。
- 动态链接器:延迟某些外部模块的链接,直到生成加载模块才进行连接。
加载器的定义
由于当前执行的程序必须驻留在计算机的主内存中。加载器会将链接器生成的程序的可执行模块加载到主存储器中进行执行。
有三种加载方法:
- 绝对加载:
此方法将程序的可执行文件加载到相同的主内存位置,它的缺点是必须知道将模块加载到主存储器的分配策略。如果要修改程序就必须更改程序的所有地址。
- 可重载:
编译器或汇编器不会产生实际的主内存地址,而是产生相对地址。
- 动态运行时加载:
这种方法会先生成可执行模块,但不会加进内存,只有在实际执行可执行模块的指令时,才会生成程序的绝对地址。也就是热重载技术的关键一步。
老牌热重载工具injection的原理
injection原理粗暴简单,在编译阶段将injection的Bundle包导入,通过NSBundle load方法注入动态库,当监测到文件进行改动时,会把改动的单个文件编译生成动态库(也就是.dylib文件),然后通过dlopen把动态库文件载入到运行的App中。
疑问:为什么injection不能在真机执行热重载。
热重载有两个关键步骤:(1)生成.dylib动态文件;(2)通过dlopen进行加载。
而NSBundle load方法的内部实现是调用了dlopen,而dlopen内部还会先校验签名。如果库不是事先打包进app,和app使用同一签名,就会报签名错误,从而加载不成功,errMsg为:”file system sandbox blocked mmamp()”。
而在上图的签名过程中,模拟器在公钥A环节是缺失的,也即无法对App进行签名校验。一个可以辅证的例子是:在编译微信工程时,经常会出现签名不正确导致真机编译不过,而对模拟器进行编译时,即使证书是完全不同的两个(debug_wc、debug_test),也是可以正常编译的。
CocoaHotReload可真机热重载的原因
CocoaHotReload对iOS13的机型支持了真机热重载,这里的疑问是,和之前的机型相比,iOS13改动到了什么?
改动点:iOS13后 dlopen 苹果没有做签名检验了。
这里要注意的是:iOS13的沙盒机制依旧是对.app下的目录进行签名的,只是dlopen时不再去做签名的校验。