通过异步生成 dSYM 实现极速打包

背景

对于头条这种百万行级别的大型应用来说,即使使用 Mac Pro 进行编译打包,耗时也接近一小时。公司搭建了组件化平台后,组件得以提前编译为二进制,大大降低的应用的 CI 编译时间,目前耗时大约为八分钟左右。

通过对编译时间的进一步分析发现,大约有两分钟的时间用于生成 dSYM 文件,这个文件是 Release 模式下应用的符号表,由于 CI 打出的包不管是用于灰度测试还是内部研发人员测试,均有发生 crash 的风险和追查 crash 的需求,因此生成 dSYM 文件的步骤是不可省略的。

既然这一步不可省略,直觉告诉我们可以通过异步的方式去生成,从而避免阻塞编译构建。具体的做法为:

  1. 将一次编译构建拆分为两次
  2. 第一次编译不生成 dSYM 文件
  3. 第二次编译再生成 dSYM 文件,由于使用了相同的代码和缓存,因此速度非常快。

符号化流程

UUID 关联

经过实际测试后发现,这种做法并不可行。这里首先介绍一下 crash 日志的解析流程。

当安装在手机上的 App 发生崩溃后,系统会生成一份崩溃日志,其中记录了每个线程的调用堆栈,但是只有进程地址,没有函数名称。将进程地址转换为函数名称依赖于 dSYM 文件,这个过程也称为符号化。

一份崩溃日志必须要有对应的 dSYM 才能解析,它们通过一个叫做 UUID 的标志关联。具体流程如下:

  1. 通过 xcodebuild 命令编译产物时,会生成一个 .app 文件和对应的 dSYM 文件,它们都是 Mach-O 可执行文件,都有自己的唯一标示,即 UUID。且两者的 UUID 相同。
  2. App 崩溃后,系统生成一份崩溃日志,并且在其中记录下 UUID
  3. 通过 UUID 找到关联的 dSYM 文件完成符号化。

崩溃日志中的 UUID 一般在 Binary Images 中的下一行:

dSYM 文件的 UUID 可以通过 dwarfdump --uuid 命令获取:

符号化方式

崩溃日志的符号化一般有两种方式:

  1. 使用 symbolicatecrash 命令,传入 dSYM 文件和崩溃日志,可以生成符号化以后的崩溃日志。
  2. 使用 atos 命令,传入 dSYM 文件和崩溃日志中的具体地址,可以得到这个地址对应的函数符号。

经过测试验证,我们发现:

  1. 第一种符号化方式,要求 dSYM 文件和崩溃日志中的 UUID 相同才能解析,一般个人用户会使用这种方案。
  2. 第二种符号化方式,由于不涉及崩溃日志,表面上看不需要关联 UUID。著名的 Fabric 平台,和公司内的 Slardar 平台采用这种方式,并且单独存储 dSYM 文件。但在解析崩溃日志时,依然依赖 UUID 字段去找到对应 dSYM 文件。

需要说明的是,UUID 仅用于两个文件之间的关联,苹果并不对它们的值有任何限制。以 symbolicatecrash 命令为例,崩溃日志和 dSYM 文件的 UUID 只要一致,不管值是什么,均可以成功符号化

异步导出 dSYM 的方案

至此,我们已经摸清楚了最初异步导出 dSYM 方案失败的原因。在两次打包中,即使代码和缓存都一样,系统依然会产生两个 UUID。

假设第一次编译产生的 .app 文件的 UUID 为 A,第二次编译产生的 dSYM 文件的 UUID 为 B。在 Slardar 解析时,崩溃日志中的 UUID 为 A,但是平台只存储了 UUID 为 B 的 dSYM,虽然两者实际上可以通用,但是无法在平台上正确的关联上,导致解析失败。

因此,解决方案有以下几种:

  1. 保存一份 A -> B 的 UUID 映射表,Slardar 平台根据这个关联
  2. 保存一份 A -> B 的 UUID 映射表,hook 系统生成 crash 日志的流程,将其中的 UUID 从 A 改成 B。
  3. 修改第二次编译生成的 dSYM 文件,将它的 UUID 改成和第一次生成的 .app 文件的 UUID 一致。

显然第三种方案操作更简单,并且对已有系统完全透明,无侵入和耦合。

Mach-O 文件

结构

top Created with Sketch.