转载

[译] iOS 开发中可以节省 50% 编译等待时间的几个措施

编者注:这篇文章发布与2013年11月4日,那个时候的Xcode设置选项和当前版本多少有些同,原理相同。

写给没有耐心的人

如果你不想或者由于时间关系不能读完整篇文章,那么你至少应该在你的iOS开发流程(例如未发布的构建过程中)中使用下面的构建设置。

  • 使用普通的 “DWARF”而不是“DWARF with dSYMFile” 作为你的“Debug Information Format”
  • 不要使用 –O4 标识编译项目代码,也不要使用带有-O4编译的静态链接库。因为这样Clang会开启链接时优化(LTO),这会延缓链接速度。最多使用-O3 。这些设定变更是近期才应用到我们自己的iOS代码库中的,效果非常明显。

问题

上周我参加了Spotify的内部移动训练营,这是一个为期一周的移动端开发介绍。这个训练营旨在告知与会者关于app的前沿技术,所以开发者无论有无移动开发经验,都可以修改代码,或者追加新的特征。我参加了iOS的课程。

训练营对于像我这种几乎没有iOS开发经验的人来说应该是非常有趣的。然而,这一周课程期间,在等待Xcode完成build的过程中,我经常感到沮丧。每次按下 [译] iOS 开发中可以节省 50% 编译等待时间的几个措施 来测试修改过的内容,看着那几乎无止境的等待循环,我都禁不住沮丧。对于像我这种经常使用试错法的非专业人士来说,这种状况特别糟糕。

缩短build时间,似乎是一个有待解决也很有趣的难题,也是一个了解OSX内部结构的大好机会。因此,我决定尝试一下。非常幸运,取得了显著效果:我减少了50%的等待时间。

解决方案

我的开发流程(也是最常见的一种情况)如下:

  1. 修改一些源文件。
  2. 按下Xcode中的 [译] iOS 开发中可以节省 50% 编译等待时间的几个措施 按钮。
  3. 观察在phone或者模拟器上的效果(我的例子中使用的是模拟器)。
  4. 跳到第一步。在我修改了一个Spotify的iOS客户端中相对较小的 Objective C 源文件之后,我记录了一下步骤(2)到步骤(3)花费的时间,直到模拟器加载完应用程序:我的家用iMac(说实话,已经很旧了)花费了82秒(平均值)。通过观察Xcode的编译流程我意识到大部分时间花费在“Linking”和“Generating dSYM file”阶段。

[译] iOS 开发中可以节省 50% 编译等待时间的几个措施 [译] iOS 开发中可以节省 50% 编译等待时间的几个措施

在命令行中进行一些测量证实了这一点,平均而言:

  • Linking花费了29秒
  • 生成dSYM 花费了25秒这两个阶段占用了等待时间的(29 + 25) / 82 * 100 = 62 % 。但是,毕竟,Spotify的iOS客户端代码库是非常大的(链接器要把大约2000个目标文件组合起来),花费这么多时间或许也有些道理。然而,并非完全如此……

dSYM 文件生成

老实说我对dSYM bundles了解不多,只知道其中包含调试信息。得知dSYM最初是作为Apple的 一个“临时解决方案” ,我感到非常惊讶。事情是这样的:在OSX早期阶段,Apple为了避免在链接器中引入 DWARF 支持的麻烦,创建了独立的链接器(dsymutil),这个链接器将调试信息从目标文件中取出,放到一个同一个地方:一个dSYM bundle中。

dSYM bundles 对于发行版本有用,但在开发过程中并不需要。调试器可以从中间目标文件中获取调试信息,这些文件在 build 完成后仍然存在。

Xcode 允许开发者设置自己工程的 “Debug Information Format”,可以选择使用“DWARF”而不是“DWARF 和 dSYM File”。

[译] iOS 开发中可以节省 50% 编译等待时间的几个措施

由此,在XCode中简单地改变一个选项,就可以减少大约25秒等待时间:非常棒!

Linking

不幸的是,没有神奇的选项可以跳过链接。或许,你也能跳过它,但是你最终会得到一大堆无用的目标文件:)

最初,我认为缩短30秒链接时间(像永恒那么久!)的唯一途径是研究Apple的链接器,Id64,实现 增量链接 。这项任务相当复杂,所以我决定首先 profile 一下 Id64 的执行过程来寻找已有的实现方案。

Apple提供了Id64的C++ 源代码 ,尽管是 过时版本 ,我还是尝试了一下,希望和XCode中包含的最新版本之间不要有根本性的差别。源代码不是编译好即时可用的,我参照 Macports 的 代码 ,快速地在 instrument 中运行起来 Instruments )。

[译] iOS 开发中可以节省 50% 编译等待时间的几个措施

上面的截图,也许你一眼看上去并不能获得太多信息,但是如果你读过 Id64的设计文档 ,你会迅速得出:ld::tool::Resolver::resolve()相当于Id64管道的Resolving 阶段。简言之,这个阶段负责将表示各个目标文件的图表放到一个更大的全局表里面。

早些时候已经完成了目标文件各自图表的加载,因此,原则上,解析不会太费时,当然不应该影响执行时间(占执行时间的76.9%)。因此,如果你仔细观察,你会发现ld::tool::Resolver::linkTimeOptimize()占用了51.3%的链接时间。

linkTimeOptimize()执行 链接时优化 (LTO)。在Clang/LLVM领域,这意味着 链接器获得的是LLVM字节码 ,而不是通常的目标文件。这些字节码在一种更抽象的层次上代表程序的执行过程,允许LTO得以进行,但是坏处是,仍然需要将他们转换成机器代码,在链接时需要额外的处理时间。

在Id64加入了一些logging做了一些枯燥的探索,我成功定位那些令人生厌的二进制文件。这些二进制文件是一个静态库的一部分。而这个静态库,出于善意,编译优先级设定为-O4。 Quoting Clang手册记述如下:

-O4能够优化链接时间;目标文件以LLVM二进制文件格式存储,在链接期间,优化了整个程序。

我将优先级标志降为-O3,然后链接器速度提高了51%,每个周期节约了大约15秒。

成果

在我的旧机器上,每个周期大约缩短了25 + 15 = 40秒(占等待时间的40 / 82 * 100 = 49 %)。我们大胆估算一下大约能节省多少时间:

假设:

  • 由于使用新的硬件,效率提高了2倍。
  • 开发者每10分钟一个Edit-Build-Test周期。
  • 开发者每天工作5个小时。这样每人每天可以节省 40 秒/ 2 (5 60 / 10) = 600 秒 = 10 分钟。在一些大的 iOS 团队像 Spotify,每10个iOS开发者,每天将会节省1小时40分钟,完全不需要花费任何代价(尽管我花费几小时找出这些,但是可以忽略不计).这只是粗略估计,我们也要有所保留。我并没有打算建立一个严格的测试对比。

总结

优化build过程真的是值得的。只需要偶尔花费一些时间,理论上每几个月花费几个小时就够了。这样做节约了大量时间和金钱。然而,最重要的是,使得开发者更开心。

未来工作展望

进一步优化链接阶段是一个难题,因为不同于编译大量的源文件,这个过程不是并行的。但是,正如我前面提到的那样,增量链接是一个众所周知的好方法。 Google的Gold链接器 )支持增量链接,并且有完善的说明文档(例如可以看这个 视频 ),但不幸的是,它目前不支持 Mach-O 并且以后 也不太可能支持 。下个内部黑客周,如果那时Apple发布了最新版的源代码,我可能会花费那段时间去研究Id64的增量链接。

改善Edit-Build-Test之外的工作流程也很有意义。一些开发者经常改变分支程序,每次都需要全部重新build。不幸的是, Apple从XCode 4.3 开始不再支持分布式构建 。我之前已经谈论过创建一个 Cocoa Pods 的二进制版本,甚至一个利用git散列的object/library storage源来,但现在我们只能期待他们会成功。

最后同样重要的是,我们看一下 Android 的构建流程可以如何优化也是很有趣的。

正文到此结束
Loading...