iOS 二进制重排

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文件,会更好。后面的实现就不再赘述了,有兴趣可以自行实现。
文末也提供了这样的工具。

order 文件生成工具类

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值