[译]优化 Swift 编译时间

原文版本:Commits on Dec 11, 2017 ce6da1f3a47220259c3924df62f44f06bc45e222

翻译:Yuen博客地址。如果对翻译质量存疑,欢迎提出问题建议)

Swift 不停在改进 ❤️。然而目前,对于中大型项目而言,漫长的编译时间仍是一个巨大问题。这个仓库的目的就是搜集相关 Tips,帮你减少项目的编译时间。

👷🏻 维护者:Arek Holko. 少了什么? 欢迎提 Issues 或者 PR!

Table of Contents

1、函数和表达式的类型检测(Type checking of functions and expressions)

Build Settings --> Other Swift Flags 中加入

  • -Xfrontend -warn-long-function-bodies=100 (100 意味着 100 毫秒, 这个数字具体设置多少要依电脑配置和项目大小而定,建议多调整几遍找到大小相对合适的数字,ps: 1 秒= 1000 毫秒)
  • -Xfrontend -warn-long-expression-type-checking=100

编译后会看到如下图所示的场景:
[图片上传失败...(image-40c51f-1514117379893)]

📖 Sources:

2、编译缓慢的那些文件(Slowly compiling files)

上一段针对的是函数级别和表达式级别,现在我们关注整个文件的编译时间
因为这里我们必须用 CLI 命令行界面来编译项目,所以务必将参数设置正确了:

xcodebuild -destination 'platform=iOS Simulator,name=iPhone 8' \
  -sdk iphonesimulator -project YourProject.xcodeproj \
  -scheme YourScheme -configuration Debug \
  clean build \
  OTHER_SWIFT_FLAGS="-driver-time-compilation \
    -Xfrontend -debug-time-function-bodies \
    -Xfrontend -debug-time-compilation" | \
tee profile.log

上述代码针对的是 .xcodeproj 的项目,如果项目是 .xcworkspace 的话,把上面第二代码中的
-project YourProject.xcodeproj 替换成为 -workspace YourProject.xcworkspace

然后从之前生成的 profile.log 文件中提取出编译所需时间。

awk '/Driver Compilation Time/,/Total$/ { print }' profile.log | \
  grep compile | \
  cut -c 55- | \
  sed -e 's/^ *//;s/ (.*%)  compile / /;s/ [^ ]*Bridging-Header.h$//' | \
  sed -e "s|$(pwd)/||" | \
  sort -ReactNative | \
  tee slowest.log

成功执行之后,就会得到一个 slowest.log 文件,内容格式如下:

2.7288 (  0.3%)  {compile: Account.o <= Account.swift }
2.7221 (  0.3%)  {compile: MessageTag.o <= MessageTag.swift }
2.7089 (  0.3%)  {compile: EdgeShadowLayer.o <= EdgeShadowLayer.swift }
2.4605 (  0.3%)  {compile: SlideInPresentationAnimator.o <= SlideInPresentationAnimator.swift }

📖 Sources:

3、Debug 时只编译 active 架构(Build active architecture only)

默认就是这个设置,不过安全起见,可以去在 Build Settings --> Build active architecture only 确认一下
[图片上传失败...(image-b98285-1514117379893)]

📖 Sources:

4、生成 dSYM(dSYM generation)

新项目的默认设置是,Debug 配置编译时不生成 dSYM 文件。但有时候为了在开发时进行 Crash 日志解析,会去修改这个参数。生成 dSYM 会消耗大量时间,可以去确认一下。

📖 Sources:

5、Module 优化(Whole Module Optimization)

另一个公认的方法是

  • 修改 Debug 配置 Build Settings --> Optimization LevelFast, Whole Module Optimization
  • 只在 Debug 配置添加 -Onone flag 到 Build Settings --> Other Swift Flags
    [图片上传失败...(image-826b1e-1514117379893)]

这告诉了编译器哪些东西?(译者注:这里感觉自己的翻译不到位,放原文吧)

It runs one compiler job with all source files in a module instead of one job per source file

Less parallelism but also less duplicated work

It's a bug that it's faster; we need to do less duplicated work. Improving this is a goal going forward

📖 Sources:

6、CocoaPods 的 Module 优化(Whole Module Optimization for CocoaPods)

如果使用 CocoaPods 的话,上一步 WMO 的配置也要考虑放到使用的 CocoaPods 项目中。
要实现 WMO 配置,需要在项目的 Podfile 文件中添加如下的 post_install hook:

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      if config.name == 'Debug'
        config.build_settings['OTHER_SWIFT_FLAGS'] = ['$(inherited)', '-Onone']
        config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-Owholemodule'
      end
    end
  end
end

7、第三方依赖(Third-party dependencies)

在项目中有两种途径可以引入第三方依赖:

  1. 源码形式,在你每次 clean 项目编译内容的时候都会都会重新编译,例如:CocoaPods,git submodules, 复制粘贴的 code, 应用 target 依赖的子项目(subprojects)形式
  2. 提前编译好的 framework/library形式,例如:Carthage, 第三方不透露源码、提供的静态库

因此在你每次 clean build 之后,CocoaPods 大多情况下会重新编译,导致大量的编译时间花销。

Carthage 虽然难用一些,但当你很在意编译时间的话却不失为一个好选择。只有当你在更新依赖列表(添加新 framework,更新 framework 的新版本)的时候,你才会去编译外部依赖。Carthage 也许需要你花 5-15 分钟的时间去完成,但相比使用 CocoaPods 做依赖,单从长远的编译时间花销来看,Carthage 或许会节省不少时间。

📖 Sources:

  • time spent waiting for Xcode to finish builds 😅

8、模块化(Modularization)

Swift 的增量编译并不完美。有时一些增量编译中,也许仅仅是修改了一个字符串,就会导致整个项目重新编译。这是一个亟待解决的问题。

为了避免这个问题,你可以考虑将 app 拆分为一个个模块。在 iOS 里,有2中方案:动态库和静态库(Xcode9 Beta4 版本开始支持 Swift 静态库)。

假设你的 app 依赖一个叫做 DatabaseKit 的内部 framework。模块化的方法能够保证在你对 app 项目做了一些修改时,DatabaseKit 不会因为这个增量编译的行为而重新编译。

📖 Sources:

9、XIBs

代码 和 XIBs/Storyboards 的取舍一直是个热门话题。不过我们这里只是片面的谈一谈这个问题。这个情况很有趣:当你在 IB 中做了一些改动时,只有这个 IB 文件被编译了(成为 NIB 格式);与之成鲜明反差的是,有时候当你在 UIView 的一个子类中修改了一行代码,Swift 编译器可能会重新编译项目中的很大一部分代码。

📖 Sources:

10、Xcode Schemes

假设我们有一个常见设置的项目,它有3个 target:

  • App
  • AppTests
  • AppUITests

只在一个 scheme 上工作没有问题,但是我们还可以优化。下方的配置是我们一直在使用的,包了3个 scheme:

App

⌘+B 只会编译这个 app。只跑单元测试不跑 UI 测试。 对于 short iterations 很有用, 比如在 UI 代码上, 因为只有需要用到的代码被编译了。

[图片上传失败...(image-59dd50-1514117379893)]

App - 单元测试工作流(App - Unit Test Flow)

编译内容会包含这个 app 和单元测试 target。运行的话,只会跑单元测试。在涉及到单元测试的代码时很有用,因为只要一编译完项目,就能立即发现在测试里的编译错误了。甚至不需要去运行他们。

当你的 UI 测试需要话费很久的时候,这个Scheme很有用。

[图片上传失败...(image-efc09e-1514117379893)]

App - 所有测试工作流(App - All Tests Flow)

编译应用和所有 target。跑所有测试用例。当涉及到那些和 UI 关系紧密包含 UI 测试的代码时作用较大。

[图片上传失败...(image-7e5e00-1514117379893)]

📖 Sources:

11、使用全新的 Xcode 编译系统(Use the new Xcode build system)

在 Xcode 9 苹果 介绍了一种新的 Xcode 编译系统. 这还只是“预览”版本,默认并没有开启。这个新系统比默认的原有的编译系统快的多。
如果想要使用它,到 Xcode 的 File 菜单进入Workspace 或 Project Settings 的页面,就可以切换到新的编译系统了。
[图片上传失败...(image-577790-1514117379893)]

📖 Sources:

12、启用 Concurrent Swift Build Tasks(Enable Concurrent Swift Build Tasks)

Xcode 9.2 对于增加 Swift 项目的 concurrent build tasks 有一个实验性质的支持。对于一些项目,这个特性可能会大大改善编译时间。另外要注意,当启用这个选项的时候,Xcode 可能会增加大量内存开销。

如果要启用这一特性,退出 Xcode,然后在 Terminal 窗口中输入以下命令:

$defaults write com.apple.dt.Xcode BuildSystemScheduleInherentlyParallelCommandsExclusively -bool NO

测试以下这个改动对你的项目编译产生了什么影响。对于很多项目来说,这不会导致什么变化;但是对另外一些,这个改动非常重要。如果要弃用这个特性的话,在 Terminal 窗口中输入以下命令,然后重启 Xcode:

$defaults delete com.apple.dt.Xcode BuildSystemScheduleInherentlyParallelCommandsExclusively

(译者注:为什么是设为 NO 而不是 YES,开发者在 推特上 说是当时写错了😂。值得注意的是,根据这里所说,开启这个新特性的操作,只会影响原有的默认编译系统 standard build system;新的 build system 已经启用这一特性。

📖 Sources:

13、在 Xcode 中显示编译时间(Showing build times in Xcode)

最后,为了能够准确知道你的编译时间是否改善了,你应该在 Xcode 的 GUI 界面中显示时间。在命令行中运行:

$ defaults write com.apple.dt.Xcode ShowBuildOperationDuration -bool YES

成功之后,在 ⌘+B 编译一个项目后你会看到:
[图片上传失败...(image-a28378-1514117379893)]

建议每次都在一个相对公平的环境下比较编译时间,比如:

  1. 退出 Xcode
  2. 清理 Derived Data ($ rm -rf ~/Library/Developer/Xcode/DerivedData)
  3. 打开 Xcode 中的项目
  4. 在 Xcode 打开或者完成索引阶段(indexing phase)后,立刻开始编译。因为使用 Xcode 9 编译也会执行索引,所以 The first approach 看起来更具代表性。(The first approach seems to be more representative because starting with Xcode 9 building also performs indexing)

另外,你也可以利用命令行统计编译时间:

$ time xcodebuild other params

📖 Sources:

推荐阅读更多精彩内容

  • 七年以来,我最大的苦难便是华丽丽的遇你又同样华丽丽的失去。 ...
    三丫d阅读 68评论 0 0
  • 很浓重的霾,早晨醒来就发觉自己深中霾伏。然后是各种不适,嗓子不舒服,出门戴着口罩,都会不舒服,脑袋晕晕的一天,...
    遗世Caroline阅读 51评论 0 0
  • 因寒假遇上春节,我们的帅哥老师要回家探亲过节,完美天籁合唱团从1月份开始放假,至今一个多月了,今晚终于又开始上课,...
    叶滢YeYing阅读 182评论 4 3
  • 流光听得见,说“我爱你”的甜言。 铭记你于心尖,心只为你执着不变。 海枯石烂的诺言,(只为你说)(只要你说)都不厌...
    BULABULA小八阅读 50评论 0 1
  • 问题导向,用更少做更多。 给API传参数,和回调处理,是每个业务工程师在网络模块封装好的前提下,依然必须要做的事情...
    利炳根阅读 596评论 1 2