启动优化指的是优化启动过程中主线程的耗时任务,达到减少启动时间的目的。任务的耗时优化通常有以下方法:
- 移除
- 删除不必要的任务,如下线无用代码、避免产生静态初始化代码等。
- 预载
- 提前触发任务,使用缓存,如系统 Prewarm 等。
- 瘦身
- 优化任务本身,如二进制重排等。
- 并发
- 多线程并发执行任务,利用多核优势。
- 延后
- 任务延后到启动结束,如动态库懒加载等。
在启动执行过程中 Page In 带来的耗时占总耗时可达 30% 以上。二进制重排主要是为了优化 Page In 的耗时(具体优化方案可参考:『极致』的二进制重排),其原理是减少 Page In 的数量。
Page In 的产生原因是进程访问虚拟内存页,而其对应的物理内存不存在。如果访问虚拟内存页时,物理内存存在,那么会产生 Page Cache,Page Cache 的平均耗时大概是 Page In 的 1%:
如果能够将启动过程中主线程的 Page In 转换成 Page Cache,那么启动耗时将会进一步减少。
方案
首先要确保主线程触发的是 Page Cache,那么子线程就必须在主线程之前触发 Page In。需要在 APP 的最早时机进行子线程 Page In 预载,在 iOS 中开发者可执行逻辑的最早时机是 +load 方法,如何设置第一个 +load 方法可参考:+load 方法那些事儿。
Page In 触发
如何触发 Page In?只需要访问内存页的地址即可:
unsigned char volatile *ptr = (unsigned char *)pc;
unsigned char result = *ptr;
如何获取 Page In 地址?有以下两种方法:
- 按页访问
- 动态下发
按页访问
按页访问依赖二进制重排结果的准确性,在 A7 芯片以后 iPhone 设备的内存页大小为 16kb。如果二进制重排结果足够准确,那么可以通过获取 __TEXT,__text 地址(可适当偏移) ,加上 ASLR 按照每次累加 16kb 大小访问即可。
动态下发
动态下发是提前获取 APP 启动过程中产生的 Page In 地址,之后通过动态下发到客户端,客户端获取到 Page In 地址后存储到本地,下次启动的时候读取并且加上 ASLR 访问即可。
Instruments
如何提前获取 Page In 地址,可以通过 Instruments 工具:
可以发现 Size 大小就是 16kb,此外这里获取的 Page In 地址还包括其它动态库以及系统库。(动态下发对比依赖二进制重排的方式可预载的 Page In 数量会更多)
这里的 Address 是已经拼接了 ASLR 的,所以需要获取本次启动的每个动态库的 ASLR,然后将 Address - ASLR 下发到客户端。
对于下发的地址需要做一层地址合法性校验,避免客户端访问非法地址而产生崩溃。
Pymobiledevice3
因为 Instruments 对 App Store 包使用做了限制,另外 Instruments 不便流程脚本化。可以使用开源库 pymobiledevice3 获取 Page In Address,命令可参考:
pymobiledevice3 developer dvt core-profile-session parse-live --show-tid
pymobiledevice3 developer dvt core-profile-session stackshot
可以获取动态库的信息:
总结
Page In 预载是另外一个角度减少启动耗时的思路,除了启动阶段也可以尝试在页面打开等相对固定且有一定的耗时的场景使用。