您好,欢迎来电子发烧友网! ,新用户?[免费注册]

您的位置:电子发烧友网>源码下载>通讯/手机编程>

怎样提高iOS工程打包的速度

大小:0.3 MB 人气: 2017-09-25 需要积分:1

过慢的编译速度有非常明显的副作用。一方面,程序员在等待打包的过程中可能会分心,比如刷刷朋友圈,看条新闻等等。这种认知上下文的切换会带来很多隐形的时间浪费。另一方面,大部分 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 个 头文件,如下图所示:

怎样提高iOS工程打包的速度

由于 #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 《UIKit/UIKit.h》

抛开自动链接 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

非常好我支持^.^

(0) 0%

不好我反对

(0) 0%

      发表评论

      用户评论
      评价:好评中评差评

      发表评论,获取积分! 请遵守相关规定!