如何提升iOS 工程打包速度

过慢的编译速度有非常明显的副作用。一方面,程序员在等待打包的过程中可能会分心,比如刷刷朋友圈,看条新闻等等。这种认知上下文的切换会带来很多隐形的时间浪费。另一方面,大部分 app 都有自己的持续集成工具,如果打包速度太慢, 会影响整个团队的开发进度。

因此,本文会分别讨论日常开发和持续集成这两种场景,分析打包速度慢的瓶颈所在,以及对应的解决方案。利用这些方案,笔者成功的把公司 app 的持续集成时间从 45 min 成功的减少到 9 min,效率提升高达 80%,理论上打包速度可以提升 10 倍以上。如果用一句话总结就是:

在绝对的实力(硬件)面前,一切技巧(软件)都是浮云

日常开发

其实日常开发的优化空间并不大,因为默认情况下 Xcode 会使用上次编译时留下的缓存,也就是所谓的增量编译。因此,日常开发的主要耗时由三部分构成:

总耗时 = 增量编译 + 链接 + 生成调试信息(dSYM)

这里的增量编译耗时比较短,即使是在我 14 年高配的 MacBook Pro(4核心,8 线程,2.5GHz i7 4870HQ,下文简称 MBP) 上,也仅仅耗时十秒上下。我们的应用代码量大约一百多万行,业内超过这个量级的应用应该不多。链接和生成调试信息各花费不到 20s,因此一次增量的编译的时间开销在半分钟到一分钟左右,我们逐个分析:

增量编译: 因为耗时较短(大概十几秒或者更少),几乎不存在优化的空间,但是非常容易恶化。因为只有头文件不变的编译单元才能被缓存,如果某个文件被 N 个文件引用,且这个文件的头文件发生了变化,那么这 N 个文件都会重编译。APP 的分层架构一般都会做,但一个典型的误区是在基础库的头文件中使用宏定义,比如定义一些全局都可以读取的常量,比如是否开启调试,服务器的地址等等。这些常量一旦改变(比如为了调试或者切换到某些分支)就会导致应用重编译。

链接:链接没有缓存,而且只能用单核进行,因此它的耗时主要取决于单核性能和磁盘读写速度。考虑到我们的目标文件一般都比较小,因此 4K 随机读写的性能应该会更重要一些。

调试信息:日常开发时,并不需要生成 dSYM 文件,这个文件主要用于崩溃时查找调用栈,方便线上应用进行调试,而开发过程中的崩溃可以直接在  Xcode 中看到,关闭这个功能不会对开发产生任何负面影响

日常开发的优化空间不大,即使是庞大的项目,落后的机器性能,关闭 dSYM 以后也就耗时 30s 左右。相比之下,打包速度可以优化和讨论的地方就比较多了。

持续集成

在利用 Jenkins 等工具进行持续集成时,缓存不推荐被使用。这是因为苹果的缓存不够稳定,在某些情况下还存在 bug。比如明明本地已经修复了 bug,可以编译通过,但上次的编译缓存没有被正确清理,导致在打包机器上依然无法编译通过。或者本地明明写出了 bug,但同样由于缓存问题,打包机器依然可以编译通过。

因此,无论是手动删除Derived Data文件夹,还是调用xcodebuild clean命令,都会把缓存清空。或者直接使用xcodebuild archive,会自动忽略缓存。每次都要全部重编译是导致打包速度慢的根本原因。以我们的项目为例,总计 45min 的打包时间中,有 40min 都在执行xcodebuild这一行命令。

使用 CCache 缓存

最自然的想法就是使用缓存了,既然苹果的缓存不靠谱,那么就找一个靠谱的缓存,比如 CCache。它是基于编译器层面的缓存,根据目前反馈的情况看,并不存在缓存不一致的问题。根据笔者的实验,使用 CCache 确实能够较大幅度的提升打包速度,删除缓存并使用 CCache 重编译后,耗时只有十几分钟。

然而,CCache 最致命的问题是不支持 PCH 文件和 Clang modules。PCH 的本意是优化编译时间,我们假设有一个头文件 A 依赖了 M 个头文件,其中每个被依赖的头文件又依赖了 N 个 头文件,如下图所示:

由于#import的本质就是把被依赖头文件的内容拷贝到自己的头文件中来,因此头文件 A 中实际上包含了 M * N 个头文件的内容,也就需要 M * N  次文件 IO 和相关处理。当项目中每增加一个依赖头文件 A 的文件,就会重复一次上述的 M * N  复杂度的过程。

PCH 文件的好处是,这个文件中的头文件只会被编译一次并缓存下来,然后添加到项目中所有的头文件中去。上述问题倒是解决了,但很智障的一点是,所有文件都会隐式的依赖所有 PCH 中的文件,而真正需要被全局依赖的文件其实非常少。因此实际开发中,更多的人会把 PCH 当成一种快速import的手段,而非编译性能的优化。前文解释过,PCH 文件一旦发生修改,会导致彻彻底底,完完整整的项目重编译,从而降低编译速度。正是因为 PCH 的副作用甚至抵消了它带来的优化,苹果已经默认不使用 PCH 文件了。

用来取代 PCH 的就是 Clang modules 技术,对于开启了这一选项的项目,我们可以用@import来替代过去的#import,比如:

@import UIKit;

等价于

#import

抛开自动链接 framework 这些小特性不谈,Clang modules 可以理解为模块化的 PCH,它具备了 PCH 可以缓存头文件的优点,同时提供了更细粒度的引用。

说回到 CCache,由于它不支持 PCH 和 Clang modules,导致无法在我们的项目中应用。即使可以用,也会拖累项目的技术升级,以这种代价来换取缓存,只怕是得不偿失。

distcc

distcc 是一种分布式编译工具,可以把需要被编译的文件发送到其他机器上编译,然后接收编译产物。然而,经过贴吧、贝聊、手Q 等应用的多方实验,发现并不适合 iOS 应用。它的原理是多个客户端共同编译,但是绝大多数文件其实编译时间非常短,并不值得通过网络来回传送,这种方案应该只适合单个文件体量非常大的项目。在我们的项目中,使用distcc大幅度增加了打包时间,大约耗时 1 小时左右。

定位瓶颈

在寻求外部工具无果后,笔者开始尝试着对编译时间直接做优化。为了搞清楚这 40min 究竟是如何花费的,我首先对xcodebuild的输出结果进行详细分析。

使用过xcodebuild命令的人都会知道,它的输出结果对开发者并不友好,几乎没有可读性,好在还有xcpretty这个工具可以格式化它:

gem install xcpretty

通过gem安装后,只要把xcodebuild的输出结果通过管道传给xcpretty即可:

xcodebuild -scheme Release ... | xcpretty

下面是官方文档中的 Demo:

我只对其中的编译部分感兴趣,所以简单的做下过滤,我们就可以得到格式高度统一的输出:

Compiling A.m

Compiling B.m

Compiling ...

Compiling N.m

到了这一步,终于可以做最关键的计算了,我们可以通过设置定时器,计算相邻两行输出之间的间隔,这个间隔就是文件的编译时间。当然,也有类似的辅助工具做好了这个逻辑:

npm install gnomon

简单的做一下排序,就可以看到最耗时的前 200 个文件了,还可以针对文件后缀作区分,计算总耗时等等。经过排查,我们发现一半的编译时间都花在了编译 protobuf 文件上。

工程设置

除了针对超长耗时的文件进行 case-by-case 的分析外,另一种方案是调整工程设置。一般来说,我们的持续集成工具主要是用来给产品经理或者测试人员使用,用来体验功能或者验证 Bug,除非是需要上架 App Store,否则并不需要关心运行时性能。然而在手机上使用的 Release 模式,默认会开启各种优化,这些优化都是牺牲编译性能,换取运行时速度,对于上架的包而言无可厚非,但对于那些 Daily Build 包来说,就显得得不偿失了。

因此,加速打包的思路和优化的思路是完全互逆的,我们要做的就是关闭一切可能的优化。这里推荐一篇文章:关于Xcode编译性能优化的研究工作总结,可以说相当全面了。

经过对其中各个参数的查找资料和尝试关闭,按照提升速度的降序排列,简单整理几个:

仅支持 armv7 指令集。手机上的指令集都属于 ARM 系列,从老到新依次是 armv7、armv7s 和 arm64。新的指令集可以兼容旧的机型,但旧的机型不能兼容新的指令集。默认情况下我们打出来的包会有 armv7 和 arm64 两种指令集, 前者负责兜底,而对于支持 arm64 指令集的机型来说,使用最新的指令集可以获得更好的性能。当然代价就是生成两种指令集花费了更多时间。所以在急速打包模式下,我们只生成 armv7 这种最老的指令集,牺牲了运行时性能换取编译速度。

关闭编译优化。优化的基本原理是牺牲编译时性能,追求运行时性能。常见的优化有编译时删除无用代码,保留调试信息,函数内联等等。因此提升打包速度的秘诀就是反其道而行之,牺牲运行时性能来换取编译时性能。笔者做的两个最主要的优化是把Optimize level改成 O0,表示不做任何优化。

使用虚拟磁盘。编译过程中需要大量的磁盘 IO,这主要发生在Derived Data目录下,因此如果内存足够,可以考虑划出 4G 左右的内存,建一个虚拟磁盘,这样将会把磁盘 IO 优化为 内存 IO,从而提高速度。由于打包机器每次都会重编译,因此并不需要担心重启机器后缓存丢失的问题。

不生成 dYSM 文件,前文已经介绍过。

一些其他的选项,参考前面推荐的文章。

在以上几个操作中,精简指令集的作用最大,大约可以把编译时间从 45 min 减少到 30min 以内,配合关闭编译优化,可以进一步把打包时间减少到 20min。虚拟磁盘大约可以减少两三分钟的编译时间,dSYM 耗时大约二十秒,其它选项的优化程度更低,大约在几秒左右,没有精确测算。

因此,一般来说只要精简指令集并关闭优化即可,有条件的机器可以使用虚拟磁盘,不建议再做其它修改。

二进制化

二进制化主要指的是利静态库代替源码,避免编译。前文已经介绍过如何分析文件的耗时,因此二进制化的收益非常容易计算出来。由于团队分工问题,笔者没有什么二进制化的经验,一般来说这个优化比较适合基础架构组去实施。

硬件加速

以上主要是通过修改软件的方式来加速打包,自从公司申请了 2013 年款 Mac Pro(Xeon-E5 1630 6 核 12 线程,16G 内存,256G SSD 标配,下文简称 Mac Pro)后,不需要修改任何配置,仅仅是简单的迁移打包机器,就可以把打包时间降低到 15 min,配和上一节中的前三条优化,最终的打包时间大概在 10min 以内。

在我的黑苹果(i7 7820x 8 核 16 线程,16G 内存,三星  PM 961 512G SSD,下文简称黑苹果)上,即使不开启任何优化,从零开始编译也仅需 5min。如果将 protobuf 文件二进制化,再配合一些工程设置的优化,我不敢想象需要花多长时间,预计在 4min 左右吧,速度提升了大概 11 倍。

编译是一个考验多核性能的操作,在我的黑苹果上,编译时可以看到 8 个 CPU 的负载都达到了 100%,因此在一定范围内(比如 10 核以内),提升 CPU 核数远比提升单核主频对编译速度的影响大。至于某些 20 核以上、单核性能较低的 CPU 编译性能如何,希望有经验的读者给予反馈。

优化点总结

下表总结了文章中提到的各种优化手段带来的速度提升,参考原始时间均为 45 min(打包机器:13 寸  MacBook Pro):

方案序号优化方案优化后耗时 (min)时间减少百分比

1不常修改的文件二进制化2544.4%

2精简指令集2740%

3关闭编译优化3815.6%

4使用 Mac Pro1566.7%

5虚拟磁盘426.7%

6公司现行方案(2+3+4+5)980%

7黑苹果588.9%

8终极方案(1+2+3+5+7)4(预计)91.1%(预计)

严格意义上讲,文章有点标题党了,因为一句话来说就是:

能用硬件解决的问题,就不要用软件解决。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容