虚拟内存
在了解二进制重排之前,我们先了解虚拟内存,详细的可以查看iOS 系统是怎么管理内存的。
电脑中所运行的程序均需经由内存执行,若执行的程序占用内存很大或很多,则会导致内存消耗殆尽。为解决该问题,Windows 中运用了虚拟内存技术,即匀出一部分硬盘空间来充当内存使用。主要用于解决当多个进程同时存在时,对物理内存的管理。提高了CPU的利用率,使多个进程可以同时、按需加载。所以,虚拟内存其本质就是一张虚拟地址和物理地址对应关系的映射表。
Page Fault
在虚拟内存部分,当进程访问一个虚拟内存page,而对应的物理内存不存在时,会触发缺页中断(Page Fault),因此阻塞进程。此时就需要先加载数据到物理内存,然后再继续访问。这个对性能是有一定影响的。基于Page Fault,App在冷启动过程中,会有大量的类、分类、三方等需要加载和执行,此时的产生的 Page Fault 所带来的的耗时是很大的。
二进制重排原理
编译器在生成二进制代码的时候,默认按照链接的 Object File(.o)
顺序(也就是 Targets->Build Phases->Compile Sources 中的文件顺序)写文件,按照 Object File
内部的函数顺序写函数。
静态库文件.a就是一组.o文件的ar包,可以用ar -t查看.a包含的所有.o。
如下图,page1 和 page2,其中 method1
和 method3
启动时候需要调用,为了执行对应的代码,系统必须进行两次Page Fault。
但如果把
method1
和method3
排布到一起,那么只需要一个 Page Fault 即可,这就是二进制文件重排的核心原理: 将所有启动时刻需要调用的方法排列在一起获取启动阶段的 page fault 次数
通过 System Trace 拿到某个时间段的 page fault 次数
日常开发中性能分析是用最多的工具无疑是Time Profiler,但Time Profiler是基于采样的,并且只能统计线程实际在运行的时间,而发生Page Fault的时候线程是被blocked,所以我们需要用一个不常用但功能却很强大的工具:System Trace。
选中主线程,在 VM Activity 中的 File Backed Page In 次数就是 Page Fault 次数,并且双击还能按时序看到引起 Page Fault 的堆栈:
通过 LinkMap 拿到当前二进制的函数布局
LinkMap 是 iOS 编译过程的中间产物,记录了二进制文件的布局,需要将 Xcode 的 Build Settings -> Write Link Map File 设置为 YES
linkmap主要包括三大部分:
Object Files 生成二进制用到的 link 单元的路径和文件编号
Sections 记录 Mach-O 每个 Segment/section 的地址范围
Symbols 按顺序记录每个符号的地址范围
通过 Link map 文件的查看,我们可以看到 在 Symbols
中有着二进制的函数布局。
通过 Order File 让链接器按照指定顺序生成 Mach-O
Xcode 使用的链接器件是 ld ,ld 有一个不常用的参数 -order_file
,我们可以通过在 Build Settings -> Order File 配置一个后缀为 .order
的文件路径。 通过 Order File 我们可以更改函数和数据布局的顺序。当然,我们最好不要在调试或开发配置中指定 Order File,因为这会使链接的二进制文件对调试器的可读性降低。仅在发布时使用 Order File 。
不需要担心 Order File 中的符号是不存在的,因为 ld 会忽略这些符号
那么,如何编写自己的 .order
文件呢?可以参考下面的示例:
test //函数
[ViewController orderTest]//方法
当我们编写完 Order File 文件后,重新编译工程就可以在 LinkMap 文件中查看到已经调整后的二进制函数布局
Clang 插桩
还剩下最后一个,也是最核心的一个问题,获取启动时候用到的函数符号。LLVM 内置了一个简单的代码覆盖率检测 SanitizerCoverage。它在函数级、基本块级和边缘级插入对用户定义函数的调用。提供了这些回调的默认实现,并实现了简单的覆盖率报告和可视化。
配置 SanitizerCoverage
-
工程 Target 配置
在 Targets->Build Settings -> Other C Flags 中添加-fsanitize-coverage=func,trace-pc-guard
-fsanitize-coverage=func,trace-pc-guard -
Podfile 配置
post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['OTHER_CFLAGS'] = '-fsanitize-coverage=func,trace-pc-guard' end end end
实现 SanitizerCoverage 的方法
实现 __sanitizer_cov_trace_pc_guard_init
和 __sanitizer_cov_trace_pc_guard
方法
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return; // Duplicate the guard check.
void *PC = __builtin_return_address(0);
printf("guard: %p %x PC\n", guard, *guard);
}
获取函数符号
使用-fsanitize-coverage=func,trace-pc-guard
编译器将会在每个函数的边缘插入 __sanitizer_cov_trace_pc_guard
函数。 __builtin_return_address(0)
返回当前函数返回地址也就是当前函数的调用者。我们通过 dladdr()函数根据 PC 指针可以获取到其相关信息。
#import <dlfcn.h>
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
void *PC = __builtin_return_address(0);
Dl_info info;
dladdr(PC, &info);
printf("sname:%s \nsaddr:%p \n",info.dli_sname,info.dli_saddr);
}
可以查看到
info.dli_sname
是我们想要的函数符号信息,我们可以将 App 启动过程中将这些信息去重存储到数组中,启动完成后在沙盒中新建 .order 文件,并将数据写入。
最后,将 .order
文件从沙盒中取出,配置到工程 Order File 。