> For the complete documentation index, see [llms.txt](https://ryukiedev.gitbook.io/wiki/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ryukiedev.gitbook.io/wiki/ios/you-hua/03.-er-jin-zhi-zhong-pai-shi-jian.md).

# 03.二进制重排实践

## 前言

近两年二进制重排在启动优化上还是经常被提到的，但自己没有尝试过。

继上一次[「iOS官方瘦身方案ODR（二）：换肤系统改造｜践行 On-Demand Resources」](https://juejin.cn/post/6992183244267454494)后，再次拿自己个人项目小白鼠「梦见账本」来实践一下。

## 一、 为什么要二进制重排呢？

虽然听着很厉害的样子，但其实是个老概念了。

### 1.1 虚拟内存和分页

我们知道，现代操作系统一般都采用虚拟内存管理机制，用`分段（segment）`和`分页（page）`管理虚拟内存。

分段即是区分`数据段`、`代码段`、`堆内存`、`栈内存`等，不同的段数据的读写权限不一样。以 `iOS` 为例，`代码段（_TEXT）`是可读可执行但不能写的。

分页则是为了方便高效的进行内存管理。由于采用了虚拟内存管理机制，就要建立`虚拟内存`到`物理内存`的`映射表`，称为`页表`。如果在设计上将每一个字节的虚拟内存和物理内存一一对应，这样粒度足够细，虽然不会产生内存浪费（内存碎片），但需要维护巨大的页表；但如果一页数据过大，比如5M，那么存储1个字节就要分配一个5M的页面，是非常大的浪费。内存页过大或过小都有弊端，目前大多数系统的页大小都设置在了`4096字节`，通过页号和页内偏移进行寻址。可以使用`pagesize`命令查看当前系统的页大小。

### 1.2 Page Fault

使用虚拟内存的目的之一是解决物理内存资源紧张的问题。`dyld` 在加载二进制时，会使用 `mmap` 将 `Mach-O` 文件映射到`虚拟内存`地址空间中，此时并不会占用过多的`物理内存`。当读取一个`虚拟内存地址`时，如果该地址在`物理内存`中并不存在，会触发一次`缺页中断（Page Fault）`，这个时候才将文件内容读取至物理内存中。

缺页中断发生时会执行下面的操作：

***分配内存*** 由`内存管理单元`找到空闲内存并分配。

***IO操作*** 从磁盘中读文件并写入内存中。

***解密验签*** 如果是从 `AppStore` 上下载的 `APP`，`iOS` 系统还有对每一页（仅针对 `_TEXT` 段的数据，`_DATA` 段数据不需要）进行解密和签名验证。

以上操作在每一次 `Page Fault` 时都会发生，如果在启动 `APP` 时，存在大量的 `Page Fault` 情况，势必影响启动速度。

## 二、 什么是二进制重排

频繁的发生 `Page Fault` 会影响启动速度，那么，是否可以干预 `Mach-O` 的 `_TEXT` 段函数的映射顺序，将 `APP` 启动时需要用到的方法集中在一页或几页呢？答案是肯定的，二进制重排的原理就是字面上的理解，通过减少 `Page Fault` 发生次数，减少启动耗时。

理论上 `Page Fault` 确实会影响启动速度，但影响的大小要区分看待。一般来说，是要在常规的优化手段都做完之后，再考虑进行二进制重排。且对于小型APP来说，如果本身启动时执行的方法并不算多，那么二进制重排的意义就不是很大。

对于 `iOS 13` 系统来说，由于启用了 `dyld3`，`Page Fault`发生时已经不需要执行解密验签（提前生成了 `lauch closure` 文件），对性能的影响就更小了。

## 三、 System Trace 查看耗时

> 建议重装应用

* 选中指定的设备，选中安装的 App 点击 `[*]` 按钮，应用第一个页面（非启动页）显示后停止。
* 找到自己的项目
* 选中 `Main Thread`
* 选中 `Virtual Memory`

![01](/files/-Mgi89eFjqSm5Hv2Hl-5)

![02](/files/-MhGyFsZTdMJHu6NvZ9B)

`File Backed Page In` 次数就是 `Page Fault` 的次数。「梦见账本」耗时 `341ms`。

![3](/files/-Mgi89eHf1gZsNMMotpz)

点击这里的小箭头，可以看到调用堆栈

![4](/files/-Mgi89eIK-zvozu8lqjL)

当然，我们不可能人工的来整理这些。那么有什么办法可以获取到所有调用呢？

## 四、 获取启动时调用的所有方法

***现有方案对比***

* hook `objc_msgSend`
  * 只能捕获基于 `objc` 的方法调用
* 静态扫描 MachO 文件里的符号和函数数据 + 解析 Trace 文件
  * 容易获取 `+load`、`C++构造函数`
  * initialize hook不到
  * 部分block hook不到
  * C++通过寄存器的间接函数调用静态扫描不出来
* 编译器插桩 `Clang`
  * 可以拿到 `OC`、 `Swift`、 `C`、 `block` 全部调用

## 五、 Clang 插桩

### 5.1 基于 Clang SanitizerCoverage 的方案

`SanitizerCoverage` 是 `Clang` 内置的一个代码覆盖工具。它把一系列以 `__sanitizer_cov_trace_pc_` 为前缀的函数调用插入到用户定义的函数里，借此实现了全局 `AOP`。其覆盖了/ `Swift/Objective-C/C/C++` 等语言，`Method/Function/Block` 全支持。

开启 `SanitizerCoverage` 的方法是：

* 在 `build settings` 里的 `Other C Flags` 中添加 `-fsanitize-coverage=func,trace-pc-guard`
* 如果含有 `Swift` 代码的话
  * 需要在 `Other Swift Flags` 中加入 `-sanitize-coverage=func` 和 `-sanitize=undefined`
* 所有链接到 `App` 中的二进制都需要开启 `SanitizerCoverage`，这样才能完全覆盖到所有调用
  * 例如Pod库，就要在Target里设置

> 在[SanitizerCoverage](https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs)中可以看到 `LLVM` 官方对 `SanitizerCoverage` 的详细介绍，包含了示例代码。

### 5.2 获取 order 文件

这里直接使用了[AppOrderFiles](https://github.com/yulingtianxia/AppOrderFiles)来进行获取。就不贴代码了，源码也不多，有兴趣可以自行查看。

在 `AppDelegate` 中调用：

```swift
// 我是放在 `func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool` 最后调用的
AppOrderFiles { path in
    if let path = path { print("AppOrderFiles: \(path)") }
}
```

安装运行一次后，从 `Xcode` 获取应用的设备的 `.xcappdata` 文件中按照路径，取到 `app.order` 文件。

![05](/files/-Mgi89eL-6bMp40RH87l)

### 5.3 设置 order 文件路径

![06](/files/-Mgi89eNF6R7P6iGR8Kj)

> 没必要加入 `Bundle`

### 5.4 验证顺序 LinkMap

***开启\*\*\*\* ****`LinkMap`**** \*\*\*\*文件输出***

![07](/files/-Mgi89eOkEPOOH31xEo8)

***编译获取\*\*\*\* \*\*\*\*`LinkMap`***

先在项目的 Product 文件夹中找到 `.app` 的目录

![08](/files/-Mgi89ePfXbRGjGtKqaj)

再按如图所示路径找到 `linkmap` 文件

![09](/files/-Mgi89eSnVTvM7G2lXlG)

***对比\*\*\*\* ****`linkmap`**** ****和**** ****`order`**** \*\*\*\*文件***

搜索 `Address Size File Name`

![10](/files/-Mgi89eaxL8s3xpYItha)

发现顺序是一样的了

### 5.5 System Trace 检查效果

![11](/files/-Mgi89ebTDMC-ufrERCq)

只有 `141ms` 了，优化了一大半。具体效果根据不同项目会有所不同。

## 参考

* [App 二进制文件重排已经被玩坏了](http://yulingtianxia.com/blog/2019/09/01/App-Order-Files/)
* [SanitizerCoverage](https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs)
* [AppOrderFiles](https://github.com/yulingtianxia/AppOrderFiles)
* [Improving App Performance with Order Files](https://medium.com/@michael.eisel/improving-app-performance-with-order-files-c7fff549907f)
* [抖音研发实践：基于二进制文件重排的解决方案 APP启动速度提升超15%](https://mp.weixin.qq.com/s?__biz=MzI1MzYzMjE0MQ==\&mid=2247485101\&idx=1\&sn=abbbb6da1aba37a04047fc210363bcc9\&chksm=e9d0cd4fdea7445989cf26623a16fc8ce2876bf3bda95a5532bb0e5e5b1420765653df0b94d1\&mpshare=1\&scene=1\&srcid=0316fLf0VRLHLhRPFDH6LuQo\&sharer_sharetime=1595570815854\&sharer_shareid=ff29c649ff1b5cea91081f325b5ada59#rd)
* [iOS启动优化之二进制重排](https://easeapi.com/blog/blog/143-ios-startup.html)
