编译时间优化——Part 1
swift 编译是真的慢!!! 下面开始讲讲我是怎么对项目进行优化,将项目编译时间减少50% 以上的。
测量当前项目的编译时间
- Xcode 保留了每次编译的日志,可以在 Report Navigator 中很快地找到。

- 在 Xcode 的 activity viewer 中显示编译时间, 开启这个选项需要在命令行中执行以下命令:
defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES编译成功后,编译时间将显示在 Succeeded 后面,如下图所示。

耗时分析
通过 Xcode 的 Build With Timing Summary 可以看到在编译过程中各个阶段的耗时, Product > Perform Action > Build with Timing Summary 或者 xcodebuild -buildWithTimingSummary。

在 Report Navigator 中可以看到下面的时间统计

可以看到 CompileSwiftSources 、 CompileC、CompileXIB 花费了大量时间。
通过查看编译日志发现,PhaseScriptExecution 阶段有重复的和可以精简的 phase。

其中红框内的 phase 重复了,绿框中的生成 dSYM 的操作在 Debug 配置下是不需要的。
从图中可以发现,CompileSwiftSources 的时间比总时间还要长, 是因为多个 swift 源文件的编译是并行的,
结合 swift 官方的 CompilerPerformance,我将通过以下以及方面来优化编译时间:
- 修改 build settings
- 精简 build phases
- 调整源码
- 其他
修改 build settings
Build Active Architecture Only (ONLY_ACTIVE_ARCH)
开启时,xcode 只会对当为当前 CPU 架构创建二进制文件,在开发阶段,我们只在真机或模拟器(Active Architecture)编译工程。在 Release 的编译中,应该包含所有支持的 CPU 架构,因为该二进制文件将通过 App Store 下发到用户各种各样的设备上。确保在 Debug 配置下设置为 YES,在 Release 配置下设置为 NO

Compilation Mode (SWIFT_COMPILATION_MODE)
此设置决定了 swift 源文件被重新编译的策略。在 Debug 配置下设置为 Incremental, 只重新编译 “过期” 的 swift 源文件。在 Release 配置下设置为 Whole Module, 编译所有 swift 源文件以应用某些代码优化。

Optimization Level (SWIFT_OPTIMIZATION_LEVEL)
优化级别设置定义了优化构建的方式。由于优化过程涉及额外的工作,因此代码优化会导致构建时间变慢。Debug 配置想应设置为“No Optimization”,因为我们需要快速的编译时间。在Release 配置下,将其设置为“Optimize for Speed”。

Debug Information Format (DEBUG_INFORMATION_FORMAT)
包含用于符号化和解释崩溃报告的调试信息。Release 配置下应该始终创建此文件,Debug 配置下无需生成此文件。

Enable Bitcode (ENABLE_BITCODE)
BitCode 是 iOS 9 引入的新特性,是由 LLVM 引入的一种中间代码,当这个属性设置为 YES 的时候,Xcode 在打包的时候,会将项目编译成很多个设备对应的安装包,这样在编译打包的时候就比较耗时,但对于内部打包不需要上传 AppStroe, 因此我们可以关闭此特性以减少编译打包时间。

精简 build phases

调整源码
调整依赖库
- 使用 cocoapods-binary 预编译依赖库,直接使用二进制参与编译链接
- 精简依赖库列表,移除未使用到的库
- 裁剪第三方依赖库,即通过删除无用类、无用方法、重复方法等减少不必要编译的代码
使用 cocoapods-binary 的缺点有:
- 开发过程中看不到第三方库的的实现
- 有 bug, 依赖库更新,编译生成的framework没有更新,依然是之前的。需要清除原来的,再重新 pod install
减少代码间的依赖
对于一个Swift/Objective-C混编项目来说,Objective-C Bridging Header 是 Objective-C 向 Swift 暴露的接口,Swift 生成的 *-Swift.h 代表的是 Swift 向 Objective-C 暴露的接口,相互桥接和引用过多的头文件,将造成每次编译时间的指数增长,使用以下方式来减少编译依赖。
- 删除无用的头文件引用
- 使用 Clang modules 技术,用 @import 来替代 #import。
- 使用 Forward declaration, 用 @class 声明引用。
- 尽可能的缩小访问控制范围, 使用 Static dispatch 代替 Dynamic dispatch。
改进源码
swift 的编译器内置了多个诊断选项,来检测编译器的性能。我们可以通过这些选项,来检测那些占用了大量时间的方法、表达式和源文件.
- -Xfrontend -debug-time-function-bodies: - 打印对每个 function 进行类型检查所花费的时间 (time spent typechecking every function in the program.)
- -Xfrontend -debug-time-expression-type-checking: 与 -Xfrontend -debug-time-function-bodies 类似,打印对每个表达式进行类型检查花费的时间 (time spent typechecking every expression in the program.)
添加诊断选项后,xcodebuild 的输出中将会增加如下内容

通过统计可以得出某个文件的类型检查时长,每个表达式的类型检查时长。 此类工具有 BuildTimeAnalyzer、XCLogParser 等,或者自行编写脚本进行分析。
BuildTimeAnalyzer

XCLogParser
安装
brew install xclogparser
使用
添加诊断选项,编译后,在工程目录下执行
xclogparser parse --project MyApp --reporter html
会在工程目录的 build/xclogparser/reports/ 路径下生成报告

对于耗时比较多的方法和表达式进行优化,以降低峰值
优化对比
优化前
| 文件数 | 代码行数 |
|---|---|
| 2086(Swift)、1286(C) | 325155(Swift)、164369(C) |
| 共 3372 个 | 共 489524 行 |
| 255(4 分 15秒) | 488(8 分 8 秒) | 411(6 分 51 秒) | 253(4 分 13 秒) | 256(4 分 16 秒) |
|---|---|---|---|---|
| 260(4 分 20 秒) | 266(4 分 26 秒) | 259(4 分 19 秒) | 264(4 分 24 秒) | 240(4 分) |
十次全量编译数据:平均 4 分 55 秒(295 秒)
优化后
| 文件数 | 代码行数 |
|---|---|
| 1351(Swift)、679(C) | 216665(Swift)、89987(C) |
| 共 2030 个 | 共 306652 行 |
| 147(2 分 27 秒)| 143(2 分 23 秒) | 151(2 分 31 秒) | 141(2 分 21 秒) | 145(2 分 25 秒)| | :—: | :—: | :—: | :—: | :—: | | 142(2 分 22 秒) | 144(2 分 24 秒) | 142(2 分 22 秒) | 149(2 分 29 秒) | 140(2 分 20 秒) | 十次全量编译数据:平均 2 分 24 秒(144 秒)
编译速度提升 51.2%