在文章开始之前,先搞明白一个问题,在iOS为什么二进制重排能优化启动时间?
一句话来说,就是减少 Page Fault 带来的性能损失
那么什么是PageFault
,我们先从虚拟内存讲起。
为什么会引入虚拟内存
- 地址空间不隔离,所有程序都可以直接访问物理地址,恶意程序就可以随便修改内存的的值。
- 内存使用效率低
- 当A和B读进内存中,忽然要执行c,这时候内存不够,而C差不多要占据整个内存,就要把A和B存回磁盘中。因为这样子大量的数据换出换入,效率低下。
- 程序运行的地址不确定
因为在程序编写中,指令和数据的地址是固定的,这样子就涉及了程序的重定位问题。
正是因为上面的3大问题,所以在计算机中引入了虚拟地址的概念。
分段
一开始的方案是使用分段,即把每个程序的对应的虚拟内存地址映身到物理内存中
像这种,虽然能解决地址隔离和重定位的问题,但是效率还是不高,因为每次都要把整块地址交换到磁盘中。
分页
分页的基本方法是地址空间人为地等分成固定大小的页,每一页的大小由硬件决定,由操作系统选择决定页的大小。
如上图所谓,内存映射表会把每一页虚拟内存映射到 物理内存 中。
如果进程1要访问 Virtual Page 3,这时 page3 不在内存中,就会发生缺页中断,就是 页错误(Page Fault,缺页异常)。然后操作系统接管进程,负现把 Page 3从磁盘中读出来装到内存中。然后内存映射表建立映射关系。
虚拟内存的实现要依靠硬件的实现,一般来说CPU内置着一个叫 MMU (Memory Management Unit)的部件进行页映射。
怎么做
- 生成Order文件
- 在
Xcode
的BuildSetting
里配置Order文件 - 生成LinkMap文件,查看是否重排成功
- 使用System Trace查看
page fault
的次数 - 最终要看启动时间是否减少
其中的难点就在于怎么生成 Oder
文件,把 App 启动时的调用到的符号尽可能放在一个page里
生成Order的三种方法
clang 静态插桩
静态插桩就是利用clang提供的回调方法,在回调方法里面获取符号。
官方文档:https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs
大佬提供的库: https://github.com/yulingtianxia/AppOrderFiles
使用fishhook objc_msgSend
方法
Hook objc_msgSend
方法要保证栈平衡。
参考戴老师的方法
1 | // 寄的value放到x12寄存器中,通过寄存器寻址跳转到地址 |
修改静态库的符号表
这个方案来自:静态拦截iOS对象方法调用的简易实现
原理:修改静态库的符号名,在静态链接时就能链接到修改后的方法
定义Person.m 文件
1 | @implementation Person |
用clang
把.m文件生成.o目标文件,
clang -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk -c Person.m -o Person.o
现在的文件还未链接,跳转NSLog
符号的地址还没定,一旦链接到bl
跳转的地址就是目标地址
这样子就能找到符号了,如果我们把0x0928
位置的N
字符改成O
。就会变成下面OSLog
这个方法
我们在主工程定义了OSLog
方法,在静态链接就会链接到这个方法。