iOS 二进制重排原理(转载)

225 阅读5分钟

iOS 启动优化之二进制重排

概述

启动优化实践中主要分为两个阶段:

  • 第一阶段,main 函数之前的优化: ① 二进制重拍。 ② 控制 +load 函数的使用次数。 ③ 控制动态库数量,官方建议原则上不超过6个(可以合并动态)。 ④ 减少类的数量(删除冗余的类)。

  • 第二阶段,main函数之后的优化 主要是针对业务层面的优化 ① 在启动函数 application:didFinishLaunchingWithOptions: 中,将不影响首页加载的业务代码放到首页加载完成后,再进行执行。 ② 必须在首页加载前执行完成的代码,可以开启多个子线程进行初始化(会涉及到任务依赖)。 ③ 首页渲染数据做内置和归档。归档数据不存在时,使用内置数据渲染,否则使用归档数据渲染。

1. 二进制重排

1.1 原理

我们的 App包数据并不是在启动的时候一次全部加载到内存中的,而是类似于懒加载的方式,以每页16KB的数据进行分页加载。启动的时刻,也是缺页加载次数最多的时刻。因为启动用到的类和方法,并不是全部集中在某几页数据中,而是根据编译顺序,分散到不确定的分页数据中。我们做二进制重拍,也就是要让启动用到的函数,集中到最前边的几张表中,减少分页加载的次数,也就节约了启动时间。 那么为什么减少分页加载的次数,可以节省启动时间呢? 这是应为,每页数据加载到内存中,还需要进行重绑定的过程,因为ASLR(地址空间布局随机化),每次启动后指针地址值并不是MachO中编译后的地址,还需要加上这个随机偏移地址,也就是rebase(重绑定)的过程。 启动时刻加载的分页越多,重绑定的地址也就越多,拖慢了应用的启动时间。

1.2 查看默认的二进制排列顺序

① 打开编译配置 Build Settings —> Write Link Map File —> 修改成 YES。

image.png

② 打开编译后生产的 .app 文件,在上两层目录找 Intermediates.noindex Intermediates.noindex/xxx(项目根目录名称).build/Debug-iphoneos/xxx(项目根目录名称).build/xxx-LinkMap-normal-arm64.txt

这个txt就是默认的排列顺序,我们配置.order后,这个文件的排列顺序会按照.order给定的顺序编译后排序。

image.png

1.3 配置.order 文件

打开 Build Setting,搜索 Order File 配置生成的.order文件路径。

.order里写点什么,稍后再说(利用 clang 插桩技术,找到启动过程中用到的方法、函数、block等)。

image.png

至此,环境上的配置,其实已经完成了,但是我们留下了一个 .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

image.png

此时编译会报错,因为插入的函数找不到。

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);
}

此时项目已经可以编译通过,运行后拿到的东西并不是我们想要的(如下):

image.png

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函数(如下图):

image.png

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 1. 3.2 swift 工程 / 混编工程问题 无法捕捉到swift函数。 解决方案: 搜索Other Swift Flags , 添加两条配置即可 : -sanitize-coverage=func -sanitize=undefined

结语

但是,有一个问题,这些log信息有重复函数,例如2.3图中所示。另外我们如果能自动生成 .order文件,会更好。后面的实现就不再赘述了,有兴趣可以自行实现。 文末也提供了这样的工具。

order 文件生成工具类

二进制重排 order 文件生成工具