简单来说,二进制重排是在链接阶段,根据App启动时的真实函数调用顺序,重新排列最终二进制文件中函数布局的一种优化技术。它的核心目标是减少App冷启动过程中的缺页中断次数,从而显著缩短启动耗时。其本质是在利用操作系统虚拟内存的局部性原理,用空间(将相关代码组织到一起)来换取启动时磁盘I/O时间的大幅减少。
🎯 一、为什么要进行二进制重排?
这背后的核心原因是虚拟内存的缺页中断机制。
当App启动时,系统并非一次性加载所有代码到物理内存,而是按需动态加载。iOS会将App的可执行文件(Mach-O)按固定大小(iOS为16KB)分成一页一页(Page)存储。当CPU需要执行某个函数时,如果该函数所在的代码页尚未在物理内存中(这种现象很常见),就会触发一个缺页中断,系统需要暂停当前指令,从磁盘(或闪存)中读取该页内容到内存,然后恢复执行。
一次缺页中断本身耗时约0.6-0.8毫秒。单次看似很短,但在App冷启动时,可能调用上百个函数。这些函数在默认的二进制布局中通常分散在许多不同的内存页里,导致启动过程可能触发数千次Page Fault。这些中断时间累积起来,就会成为启动耗时的主要部分。
“二进制重排”就是为了解决这个痛点。 通过把启动阶段会调用的所有函数(相关代码)都集中排列到最靠前的几个连续内存页中,就能让系统在启动早期只需加载极少数页面,便包含了全部所需的代码,从而把启动时可能发生的上千次Page Fault,大幅降低到几十甚至几次,显著加速App启动。
⚒️ 二、如何实现二进制重排?
实现它主要有三步:采集 -> 加工 -> 配置。
步骤 1:采集启动函数符号
我们需要知道App在冷启动时到底调用了哪些函数及其调用顺序。
- 主流技术方案:业界最通用的策略是使用Clang编译器内置的SanitizerCoverage功能。这是一种安全性极高的编译期插桩技术,通过在每一个函数(无论OC、C++、Swift还是Block)的入口插入一段回调代码来全量捕获。
- 配置插桩与线程安全:在Xcode的
Build Settings中配置-fsanitize-coverage=trace-pc-guard即可启用。收集到的函数调用信息,必须用无锁原子操作安全存入静态数组(例如使用OSAtomicIncrement),以避免启动时可能因死锁拖慢性能甚至导致崩溃。 - 备用方案:除了主流的Clang插桩,还有静态扫描+运行时Trace方案(在大型、组件化项目中常见,但覆盖率可能稍低),和基于
objc_msgSend的Hook方案(方法简单但仅能覆盖OC方法,不推荐用于完整优化)。
步骤 2:生成符号重排文件 (.order)
从插桩数据中,导出所有已调用函数的符号名(symbols),并按首次调用的时间排序、去重,生成一个纯文本文件(即.order文件)。
- 文件格式:每行一个符号名,按照你期望它们在二进制中的排列顺序从上到下排列。
- 示例:注意:
1
2
3
4_main
-[AppDelegate application:didFinishLaunchingWithOptions:]
+[MyManager sharedInstance]
__Z8logHelloP10SomeObject # 一个C++函数的符号.order文件中的符号名必须与Link Map文件中的记录一致,通常C函数或全局变量前需加下划线“_”。
步骤 3:配置链接器应用重排
将前面辛苦生成的.order文件路径添加到Xcode的Build Settings -> Order File项,更新Write Link Map File输出文件再次编译,即可让链接器(ld)据此重排。
- 验证结果:开启
Write Link Map File后,检查工程编译生成的.linkmap文件中的Symbols列表或使用nm -p命令,即可直观验证重排是否生效。
💎 总结
二进制重排是一个“先难后易”的优化过程。它的难点不在于最终的配置,而在于前期能否精准、完整地采集到启动函数调用链。 引入Clang插桩并处理号其线程安全问题后,通常能将启动时间缩短10%-30%。一旦前期工作完成,后续维护只需在启动代码变化较大时重新采集和生成.order文件即可。这一优化在抖音中被证实能让App启动速度提升超15%,是中小型应用从优秀迈向极致的一个关键技术。