优化 iOS 项目的构建时间(二)

3,708 阅读5分钟

作者介绍:钟子豪,贝聊科技高级 iOS 工程师

前言

之前一篇介绍 CCache 的文章探讨了如何使用 CCache 来优化应用构建的时间,评论里面收到了不少朋友反馈在使用的过程遇到了困难,最后无法成功应用上 CCache。其中的绝大部分问题我们在贝聊项目的集成过程中也遇到过,本文主要针对这些问题给出相应的解决方案,并从其他方面给出一些优化应用构建时间的建议。

提升缓存命中率

通过命令 ccache -s 可以查看 CCache 的整体缓存命中率。要使 CCache 真正能减少编译时间,命中率大约要达到 90% 。前文已经提过,频繁地缓存 miss 会比不使用 CCache 还慢,因此在集成 CCache 后需要保证90%的缓存命中率,才能确实地提高构建速度,如果发现缓存命中率过低,则需要分析日志排查原因。

移除 Precompiled Prefix Header

使用了 PCH 文件是最常见的导致缓存命中率过低的原因。很多项目都会为了方便,把一些常用的类在 PCH 中 import 一次,而在编译时 PCH 的内容会附加在每个文件之前,PCH 内容的改变会导致整个编译对象的全部文件的内容改变,也就导致全部 CCache 缓存失效。如果你遇到过明明只修改了一两个源文件,运行项目却会全量编译的情况,看看是不是PCH里面import了太多依赖,如果暂时无法移除 PCH 则尽量减少里面 import 的文件。

以文件内容作为编译缓存的 Key

因为常见的 git 切换分支,pod update等操作都会造成大量文件的最后编辑时间变化,如果用文件最后编辑时间作为缓存的 key,经常会看见切换 git 分支或者 pod update 之后大量文件缓存失效。 CCache 默认就是用文件内容的 MD4 摘要值来作为缓存 key 的一部分的,只要不加上 file_stat_matches 选项即可。虽然计算并对比文件内容的摘要值比简单对比文件的修改时间和大小要耗费更长的时间,但经过实践发现使用文件内容来作为 key 会有更稳定的编译优化效果,以下是我目前在用的效果比较好的 CCache 配置:

#!/bin/sh
if type -p ccache >/dev/null 2>&1; then
  export CCACHE_MAXSIZE=10G
  export CCACHE_CPP2=true
  export CCACHE_HARDLINK=true
  export CCACHE_SLOPPINESS=file_macro,time_macros,include_file_mtime,include_file_ctime,pch_defines
  
  exec ccache /usr/bin/clang "$@"
else
  exec clang "$@"
fi

只在 Release 构建启用 CCache

在使用 Debug 配置开发期间,Xcode 本身自带增量编译,只是在使用 Release 配置构建 AdHoc 或者 AppStore 的时候不能使用增量编译。因此在 Debug 模式下启用 CCache 其实意义不大,开发时文件频繁变更导致缓存命中率低,反而会拖慢日常开发节奏。

对单个 Pod 不启用 CCache

使用 CCache 需要关闭Clang Module功能,而有些第三方库因为使用了@import语法导致如果关闭 Clang Module 则无法使用@import 语法继而导致编译出错。这种情况可以单独设置一个 Pod 不要启用 CCache 即可。

下面的 Podfile 配置代码同时演示了如何只在 Release 构建启用 CCache 以及对单个 Pod 不使用 CCache:

# Podfile

target 'YourApp' do # 替换为你的 target 名
  post_install do |installer_representation|
    installer_representation.pods_project.targets.each do |target|
      if config.name != 'Debug' && target.name != 'SomePod' # 替换为你想要排除的 Pod 的名字
        config.build_settings['CC'] = '$(PODS_ROOT)/ccache-clang' # 替换为你的 ccache-clang 文件路径
      end
    end
  end
end
  

其他的优化点

这些优化点与 CCache 无关,但我尝试过之后发现对编译速度和开发体验有一定提升,因此一并列出。

调整编译的最大并发数

更新:Xcode 9.3 发布后,我又用同样的项目验证了一次,得出了近似的结果,但差异较小,建议先进行 Benchmark 再决定是否要应用到自己项目中。

我之前通过这条命令

defaults write com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks `sysctl -n hw.ncpu`

把编译时的最大并发数设置为 CPU 的核心数,没想到这竟然不是最好的配置。我最早是从下面这篇文章发现的: The best hardware to build with Swift is not what you might think | 领英

虽然原文作者说在 Xcode 9 已经修复了这个问题,但我还是用一个纯 Objective-C 的项目做实验验证,结果如下:

编译并发数 编译三次平均耗时(秒)
4 118.3
6 110.6
8 126.5

实验结果也是让我很意外,没想到 8 线程的编译速度居然还不如 4 线程,虽然差距不算大,但通过调整编译并发数对编译速度的确有影响,大家可以在自己的构建机器上用自己的项目实验一下,找到最优的配置。

另外我顺便用同一个项目测试了一下 Xcode 9 的 New Build System 的性能,结果如下

编译并发数 编译三次平均耗时(秒)
4 132.3
6 133.7
8 134.5

发现 New Build System 的编译速度略逊于旧的编译系统,但编译过程更加严谨,例如在 Copy Bundle Resources 步骤时,如果发现缺少了某个图片资源文件会作为错误而不是警告抛出。如果你的项目已经切换到了 New Build System,我建议继续使用,毕竟未来苹果会针对其做大量优化,这一点性能问题应该会得到解决。

避免头文件的递归查找

在排查编译时间过长的元凶时,发现在项目的 Build Settings-> Header Search Paths 中居然有一行 ${PROJECT_DIR}/**,可能是以前某个同事遇到找不到头文件编译错误,没有细想就加进去的。这项配置会导致编译文件时在整个项目目录中递归查找头文件,显著地增加查找头文件的时间,单个文件的编译时间可能会增加2~3倍之多。

清理废弃代码

没有代码的编译速度能快得过「没有代码」,这是很显而易见的优化手段,不过实现起来可能工作量较大。在公司业务快速迭代和更新时很容易就会产生一些不再使用的模块,定期清理既能减少项目的构建时间也能减小安装包的体积,一举两得。如果觉得这些代码以后还会用得上,可以只移除 Xcode 项目的引用而保留下来文件以作参考。

在 Release 配置下关闭警告信息

clang 在编译的过程中如果遇到不规范的代码,例如有未使用的变量,会生成一段警告信息并输出到控制台或者日志,处理这些警告信息也是需要占用一些的计算资源的。警告信息建议在开发阶段就处理掉,如果警告信息是来自 CocoaPods 集成的第三方库的话,可以在 Podfile 中把inhibit_all_warnings! 选项打开。贝聊的项目通过这个操作把编译时间减少了约 40 秒。

总结

我们的项目通过上面这些手段,最终把平均的编译时间从 1300 多秒减少到 300 秒内,视乎 CCache 的命中率会有所浮动,但总体上是大幅减少,希望这些经验能对你的项目有所帮助。