iOS 启动优化②之二进制重排

虚拟内存

    在了解二进制重排之前,我们先了解虚拟内存,详细的可以查看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,其中 method1method3 启动时候需要调用,为了执行对应的代码,系统必须进行两次Page Fault

默认

    但如果把 method1method3 排布到一起,那么只需要一个 Page Fault 即可,这就是二进制文件重排的核心原理: 将所有启动时刻需要调用的方法排列在一起
重排

获取启动阶段的 page fault 次数

通过 System Trace 拿到某个时间段的 page fault 次数

    日常开发中性能分析是用最多的工具无疑是Time Profiler,但Time Profiler是基于采样的,并且只能统计线程实际在运行的时间,而发生Page Fault的时候线程是被blocked,所以我们需要用一个不常用但功能却很强大的工具:System Trace

Product-profile-SystemTrace

    选中主线程,在 VM Activity 中的 File Backed Page In 次数就是 Page Fault 次数,并且双击还能按时序看到引起 Page Fault 的堆栈:
查看 Main Thread 中的File Backed Page In

通过 LinkMap 拿到当前二进制的函数布局

    LinkMap 是 iOS 编译过程的中间产物,记录了二进制文件的布局,需要将 Xcode 的 Build Settings -> Write Link Map File 设置为 YES
linkmap主要包括三大部分:

  • Object Files 生成二进制用到的 link 单元的路径和文件编号

  • Sections 记录 Mach-O 每个 Segment/section 的地址范围

  • Symbols 按顺序记录每个符号的地址范围

    通过 Link map 文件的查看,我们可以看到 在 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 会忽略这些符号

Build Settings -> Order File

那么,如何编写自己的 .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 
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);
}

dli_sname

可以查看到 info.dli_sname 是我们想要的函数符号信息,我们可以将 App 启动过程中将这些信息去重存储到数组中,启动完成后在沙盒中新建 .order 文件,并将数据写入。

最后,将 .order 文件从沙盒中取出,配置到工程 Order File

你可能感兴趣的:(iOS 启动优化②之二进制重排)