iOS 启动优化之二进制重排
目前已在多个项目中实践过了启动优化相关技术,今天记录一下,分享给更多的人。
概述
启动优化实践中主要分为两个阶段:
- 第一阶段,main 函数之前的优化:
① 二进制重拍。
② 控制+load
函数的使用次数。
③ 控制动态库数量,官方建议原则上不超过6个(可以合并动态)。
④ 减少类的数量(删除冗余的类)。 - 第二阶段,main函数之后的优化
主要是针对业务层面的优化
① 在启动函数application:didFinishLaunchingWithOptions:
中,将不影响首页加载的业务代码放到首页加载完成后,再进行执行。
② 必须在首页加载前执行完成的代码,可以开启多个子线程进行初始化(会涉及到任务依赖)。
③ 首页渲染数据做内置和归档。归档数据不存在时,使用内置数据渲染,否则使用归档数据渲染。
1. 二进制重拍
1.1 原理
我们的 App包数据并不是在启动的时候一次全部加载到内存中的,而是类似于懒加载的方式,以每页16KB的数据进行分页加载。启动的时刻,也是缺页加载次数最多的时刻。因为启动用到的类和方法,并不是全部集中在某几页数据中,而是根据编译顺序,分散到不确定的分页数据中。我们做二进制重拍,也就是要让启动用到的函数,集中到最前边的几张表中,减少分页加载的次数,也就节约了启动时间。
那么为什么减少分页加载的次数,可以节省启动时间呢?
这是应为,每页数据加载到内存中,还需要进行重绑定的过程,因为ASLR(地址空间布局随机化),每次启动后指针地址值并不是MachO中编译后的地址,还需要加上这个随机偏移地址,也就是rebase(重绑定)的过程。
启动时刻加载的分页越多,重绑定的地址也就越多,拖慢了应用的启动时间。
1.2 查看默认的二进制排列顺序
① 打开编译配置 Build Settings —> Write Link Map File —> 修改成 YES。
② 打开编译后生产的 .app
文件,在上两层目录找 Intermediates.noindex
Intermediates.noindex/xxx(项目根目录名称).build/Debug-iphoneos/xxx(项目根目录名称).build/xxx-LinkMap-normal-arm64.txt
这个txt就是默认的排列顺序,我们配置.order
后,这个文件的排列顺序会按照.order
给定的顺序编译后排序。
1.3 配置.order
文件
打开 Build Setting,搜索 Order File 配置生成的.order
文件路径。
.order
里写点什么,稍后再说(利用 clang 插桩技术,找到启动过程中用到的方法、函数、block等)。
至此,环境上的配置,其实已经完成了,但是我们留下了一个 .order
文件填充符号的问题,这也是核心的问题。
2. 利用插桩获取重排符号
需要说明的是,获取到符号表后,以下配置我们就可以删除掉了。
插桩获取符号可以参考LLVM官网的介绍。
原理就是在每一个OC的方法、函数、block 的汇编代码中,插入一条__sanitizer_cov_trace_pc_guard
汇编指令,读取pc寄存器的指令指针,回调到我们自己添加的C函数__sanitizer_cov_trace_pc_guard
,然后根据指针恢复出OC的方法名、函数名、block。
2.1 配置pc寄存器跟踪配置
Build Settings —> Other C Flags —> 添加 -fsanitize-coverage=trace-pc-guard
此时编译会报错,因为插入的函数找不到。
2.2 引入插桩函数
以下代码来自 LLVM官网
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N;
if (start == stop || *start) return;
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N;
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
char PcDescr[1024];
//__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
此时项目已经可以编译通过,运行后拿到的东西并不是我们想要的(如下):
2.3 恢复函数信息
- 引入头文件
#import <dlfcn.h>
- 使用如下C函数恢复pc存储的函数信息,结果存储在
DL_info
结构体中。
Dl_info info;
dladdr(PC, &info);
printf("%s\n", info.dli_sname);
完整代码如下:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
printf("%s\n", info.dli_sname);
}
到这里我们已经拿到了二进制重拍需要的OC方法、C函数及block函数(如下图):
2.4 填充.order
文件
上一步拿到的log 信息就是我们要在.order
文件中写入的二进制重排数据。
需要注意的是,非OC函数,都需要添加 _
开头。
其实打印出来的block函数已经有下划线了,但是还要再加一个。
完成这些工作后,我们就可以把2.1至2.3的配置删除了。
3. 问题记录
3.1 循环的问题
插桩也会对循环进行拦截插桩。
解决方案是,在步骤2.1中修改编译配置:
Build Settings —> Other C Flags —> 添加 `-fsanitize-coverage=func,trace-pc-guard`
3.2 swift 工程 / 混编工程问题
无法捕捉到swift函数。
解决方案:
搜索Other Swift Flags , 添加两条配置即可 :
-sanitize-coverage=func
-sanitize=undefined
结语
但是,有一个问题,这些log信息有重复函数,例如2.3图中所示。另外我们如果能自动生成 .order
文件,会更好。后面的实现就不再赘述了,有兴趣可以自行实现。
文末也提供了这样的工具。