前言

贝聊目前开发的两款App分别是贝聊家长版和贝聊老师版,最近因为在快速迭代开发新功能,项目规模急速增长,单个端业务代码约23万行,私有库约6万行,第三方库代码约15万行,单个客户端的代码行数约60万。现在打包一次耗时需要11~12分钟。虽然还远远比不上 Facebook 的40分钟,但是我们在内测的时候,经常一天要发布内测版两到三次。打包时CPU占用基本上是百分百的,因为没有专门的 CI 机器,对负责打包的同事(其实就是我自己)的工作时间占用比较多,所以最近一直在寻找加快打包速度的方案。

目前的项目架构

我们的项目使用 CocoaPods 来管理第三方库和私有库的依赖,对大部分项目来说应该是标配了。目前还是纯 Objective-C 的项目,没有引入 Swift。

调研过的方案

下面列出我研究过的一些主流方案以及我最后没有采用的原因,这些方案有各自的局限性,但是也给了我不少启发,思考过程跟最终方案一样有价值。

cocoapods-packager

cocoapods-packager 可以将任意的 pod 打包成 Static Library,省去重复编译的时间,一定程度上可以加快编译时间,但是也有自身的缺点:

  1. 优化不彻底,只能优化第三方和私有 Pod 的编译速度,对于其他改动频繁的业务代码无能为力
  2. 私有库和第三方库的后续更新很麻烦,当有源码修改后,需要重新打包上传到内部的 Git 仓库
  3. 过多的二进制文件会拖慢 Git 的操作速度(目前还没部署 Git 的 LFS)
  4. 难以调试源码

Carthage

这个方案跟 cocoapods-packager 比较类似,优缺点都差不多,但 Carthage 可以比较方便地调试源码。因为我们目前已经大规模使用 CocoaPods,转用 Carthage 来做包管理需要做大量的转换工作,所以不考虑这个方案了。

Buck

Buck 是一套通用的构建系统,由 Facebook 开源。最大的特色是智能的增量编译可以极大地提高构建速度。最早听说 Buck 的时候,它还只能用在安卓上,现在已经适配了 iOS。

它能增快构建速度的主要原因是缓存了编译结果,通过持续监视项目目录的文件变化,每次编译时只编译有改动的文件。另外一个让我很受启发的功能是 HTTP Cache Server,通过一台缓存文件服务器来保存大家的编译结果,这样只要团队里其中一人编译过的文件,其他人就不用再编译了,直接下载就行。

Buck 是个相当完备的解决方案,很多国外的大公司例如 Uber 都已经用上。我也花了很多时间来研究,最终还是认为对我们的项目和团队来说,目前并不是很适合,主要原因是:

  1. Buck 抛弃了 Xcode 的项目文件,需要手工编写配置文件来指定编译规则,这要对现有项目作出大幅度的调整。我们目前还在快速迭代新功能,没有余暇和人手来实施。
  2. 开发和调试的流程都得做出很大的改变。因为 Buck 接管了项目编译的过程,想调试项目不能简单地在 Xcode 里面 ⌘+R 了,得先反过来让 Buck 生成 Xcode 的项目文件。Uber 的工程师甚至推荐使用 Nuclide 来代替 Xcode 作为开发环境。虽然原理上是可行的,但是团队需要花不少时间来适应,短期内效率降低无可避免。
  3. 用 Xcode 调试代码享受不到加快编译速度的好处。虽然可以用 buck 命令启动 App,然后在命令行里启动 lldb 来调试,但那就无法使用 Xcode 的调试工具 例如 View Debugging 和 Memory Graph Debugger。

Bazel

Bazel 跟 Buck 很相似,是 Google 开源的,优缺点跟 Buck 都差不多,不再详细说了。

distcc 分布式编译

原理是把一部分需要编译的文件发送到服务器上,服务器编译完成后把编译产物传回来。我尝试了一下比较出名的 distcc,搭建过程比较简单,最后也能成功地把编译任务分派到内网的多台服务器上。但是其他编译服务器的 CPU 占用总是很低,只有 20% 左右;也就是说分派任务的速度甚至还赶不上服务器编译的速度,分派任务然后回传编译产物这个过程所耗费的时间超过了本地直接编译。不停调整参数反复试验了很多次,最后发现编译时间完全没有变快,甚至还有点变慢了。可能以我们目前项目的规模并不适合使用分布式编译。

最终方案:CCache

先来看看我对于解决方案的诉求:

  1. 能大幅度地提升编译速度,起码要减少掉 50% 的编译时间
  2. 不需要对项目作出重大调整
  3. 不需要改变开发工具链

CCache 是一个能够把编译的中间产物缓存起来的工具,在其他领域已经有不少应用,只是在 iOS 界的实践比较少。经过我的实践,它能够满足我前面的三点要求。我最早认识到它是搜到了这篇文章:https://pspdfkit.com/blog/2015/ccache-for-fun-and-profit/

如果你不使用 CocoaPods,参照上面的文章即可。因为针对 CocoaPods 需要作出一些额外的调整,所以还是说明一下。下面就来说说要怎样把 CCache 应用在用 CocoaPods 作为包管理工具的 iOS 项目中。

安装步骤:

注意:项目路径不能有中文,否则会影响 CCache 的正常工作

安装 CCache

首先你需要在电脑上安装 Homebrew,对使用 macOS 的程序员来说应该是标配,略过。

通过 Homebrew 安装 CCache, 在命令行中执行
$ brew install ccache

命令跑完后即安装成功。

创建 CCache 编译脚本

为了能让 CCache 介入到整个编译的过程,我们要把 CCache 作为项目的 C 编译器,当 CCache 找不到编译缓存时,它会再把编译指令传递给真正的编译器 clang。

新建一个文件命名为ccache-clang, 内容为下面这段脚本,放到你的项目里

ccache-clang

#!/bin/sh
if type -p ccache >/dev/null 2>&1; thenexport CCACHE_MAXSIZE=10Gexport CCACHE_CPP2=trueexport CCACHE_HARDLINK=trueexport CCACHE_SLOPPINESS=file_macro,time_macros,include_file_mtime,include_file_ctime,file_stat_matches# 指定日志文件路径到桌面,等下排查集成问题有用,集成成功后删除,否则很占磁盘空间export CCACHE_LOGFILE='~/Desktop/CCache.log'exec ccache /usr/bin/clang "$@"
elseexec clang "$@"
fi

在命令行中,cd 到 ccache-clang 文件的目录,把它的权限改成可执行文件
$ chmod 777 ccache-clang

如果你的代码或者是第三方库的代码用到了C++,则把ccache-clang这个文件复制一份,重命名成ccache-clang++。相应的对clang的调用也要改成clang++,否则 CCache 不会应用在 C++ 的代码上。

ccache-clang++

#!/bin/sh
if type -p ccache >/dev/null 2>&1; thenexport CCACHE_MAXSIZE=10Gexport CCACHE_CPP2=trueexport CCACHE_HARDLINK=trueexport CCACHE_SLOPPINESS=file_macro,time_macros,include_file_mtime,include_file_ctime,file_stat_matches# 指定日志文件路径到桌面,等下排查集成问题有用,集成成功后删除,否则很占磁盘空间export CCACHE_LOGFILE='~/Desktop/CCache.log'exec ccache /usr/bin/clang++ "$@"
elseexec clang++ "$@"
fi

完成后项目中应该有这两个文件

scripts

Xcode 项目的调整

定义CC常量

在你项目的构建设置(Build Settings)中,添加一个常量CC,这个值会让 Xcode 在编译时把执行路径的可执行文件当做 C 编译器。

user-defined-build-settings

CC

CC常量的值为 $(SRCROOT)/ccache-clang,如果你的脚本不是放在项目根目录,则自行调整路径。如果一运行项目就报错,检查下路径是不是填错了。

关闭 Clang Modules

因为 CCache 不支持 Clang Modules,所以需要把 Enable Modules 的选项关掉。这个问题在 CocoaPods 上如何处理,后面会讲。

enable-modules

关闭了 Enable Modules 后需要作出的调整

因为关闭了 Enable Modules,所以必须删除所有的 @import语句,替换为#import的语法
例如将 @import UIKit 替换为 #import <UIKit/UIKit.h>。之后,如果你用到了其他的系统框架例如 AVFoundation、CoreLocation等,现在 Xcode 不会再帮你自动引入了,你得要在项目 Target 的 Build Phrase -> Link Binary With Libraries 里面自己手动引入。

测试效果

尝试编译一遍,然后在命令行里输入 ccache -s 就能看见类似下面的 ccache 运行情况统计:

cache directory                     /Users/mac/.ccache
primary config                      /Users/mac/.ccache/ccache.conf
secondary config      (readonly)    /usr/local/Cellar/ccache/3.3.4_1/etc/ccache.conf
cache hit (direct)                 14378
cache hit (preprocessed)            1029
cache miss                          7875
cache hit rate                     66.18 %
called for link                       61
called for preprocessing              48
compile failed                         2
preprocessor error                     4
can't use precompiled header          70
unsupported compiler option         2332
no input file                         11
cleanups performed                     0
files in cache                     35495
cache size                           1.3 GB
max cache size                       5.0 GB

如果成功接入,就能看见 cache miss 不为0。因为第一次编译没有缓存,肯定是全 miss 的。接着编译第二遍,如果能看见 cache hit 的数字开始飙升,恭喜你,接入成功了。

CocoaPods 的 处理

如果你的项目不用 CocoaPods 来做包管理,那你已经完全接入成功了,不用执行下面的操作。

因为 CocoaPods 会单独把第三方库打包成一个 Static Library(或者是Dynamic Framework,如果用了 use_frameworks!选项),所以 CocoaPods 生成的 Static Library 也需要把 Enable Modules 选项给关掉。但是因为 CocoaPods 每次执行 pod update 的时候都会把 Pods 项目重新生成一遍,如果直接在 Xcode 里面修改 Pods 项目里面的 Enable Modules 选项,下次执行pod update的时候又会被改回来。我们需要在 Podfile 里面加入下面的代码,让生成的项目关闭 Enable Modules 选项,同时加入 CC 参数,否则 pod 在编译的时候就无法使用 CCache 加速:

post_install do |installer_representation|installer_representation.pods_project.targets.each do |target|target.build_configurations.each do |config|#关闭 Enable Modulesconfig.build_settings['CLANG_ENABLE_MODULES'] = 'NO'# 在生成的 Pods 项目文件中加入 CC 参数,路径的值根据你自己的项目来修改config.build_settings['CC'] = '$(PODS_ROOT)/../ccache-clang' endend
end

需要注意的是,如果你使用的某个 Pod 引用了系统框架,例如AFNetworking引用了System Configuration,你需要在你自己项目的Build Phrase -> Link Binary With Libraries里面代为引入,否则你编译时可能会收到 Undefined symbols xxx for architecture yyy一类的错误。有点回到了原始时代的感觉,但考虑到编译速度的极大提升,这一点代价可以接受。

集成问题排查

重点关注日志文件的输出和ccache -s 命令的统计,如果在日志中看到了 unsupported compiler option -fmodules 这样的字眼,就是你的 Enable Modules 没有关掉了,根据前面的步骤仔细检查。其他问题,参考官方文档的 Troubleshooting。

进一步的优化

移除 Precompiled Header File

PCH 的内容会被附加在每个文件前面,而 CCache 是根据文件内容的 MD4 摘要来查找缓存的,因此当你修改了 PCH 或者 PCH 引用到的头文件的内容时,会造成全部缓存失效,只能全体重新编译。CCache 在首次编译的时候因为需要更新缓存,会造成编译时间变长,对贝聊的项目来说变长了差不多一倍。因此如果 PCH 或者 PCH 引入的文件被频繁修改的话,缓存就会频繁地 miss,这种情况下还不如不用 CCache。

为了避免以上这种情况,我建议在 PCH 里面尽量少引入头文件,只保留比较少更改的系统框架和第三方类库的头文件。最好是把 PCH 彻底删除,反正苹果现在也不建议使用 PCH 了,Xcode 新建的项目默认都是不带 PCH 的。

在团队内部共享缓存文件夹

这个优化方式我尝试过,最终效果不是很好,因此没有采用。CCache 的官方文档中有一段关于共享缓存文件夹的说明,描述了如何修改 CCache 的配置,让编译缓存能够在多台电脑之间公用,理论上只要其中一个人编译过的文件其他人就能直接下载到了,节约了整个团队的时间。因为 Buck 也有类似的机制,我觉得值得尝试一下,便在公司局域网内搭建了一个 OwnCloud 网盘,让大家把自己电脑上的 CCache 缓存目录放上去共享。虽然试验是成功了,但是实际效果并不好。因为同步在多台电脑上大小达到几个G的缓存目录,需要在后台进行很多文件的对比和传输的工作,在编译的同时进行这些操作会耗费不少计算资源,反而会拖慢编译速度。加上移除掉 PCH 后,其实缓存的命中率已经相当可观了,不太需要通过共享缓存来进一步提高缓存命中率,所以我最后放弃了共享缓存这个想法。如果你对缓存命中率还是不满意的话,可以考虑往这个方向尝试一下。

总结

通过集成 CCache,我们的项目在 Xcode 里面的打包(在菜单里面选择 Product -> Archive)时间从 11~12分钟减少到了 130 秒,大概有五倍的提升,成果喜人。集成的过程其实很简单,我从开始尝试到集成成功总共就花了两个小时。如果你也被过长的编译时间困扰,建议尝试一下。

文章同步发布在 https://zhuanlan.zhihu.com/p/27584726

作者:贝聊科技
链接:https://www.jianshu.com/p/67bf747658fe
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

[贝聊科技]如何将 iOS 项目的编译速度提高5倍相关推荐

  1. [贝聊科技]如何在iOS开发中更好的做假数据?

    当工期比较紧的时候,项目开发中会经常出现移动端等待后端接口数据的情形,不但耽误项目进度,更让人有种无奈的绝望.所以在开发中,我们常常自己做些假数据,以方便开发和UI调试.然而做假数据方法不同,效率和安 ...

  2. ios系统脚本服务器加速,提高iOS项目的编译速度

    项目结构 CocoaPods做业务划分,每条业务线一个工程,每个工程依赖基础框架,业务线之间解耦依赖基础模块 纯Objective-C代码,没有引入Swift代码 独立服务器安装Xcode通过Jenk ...

  3. 微信团队分享:极致优化,iOS版微信编译速度3倍提升的实践总结

    1.引言 岁月真是个养猪场,这几年,人胖了,微信代码也翻了. 记得 14 年转岗来微信时,用自己笔记本编译微信工程才十来分钟.如今用公司配的 17 年款 27-inch iMac 编译要接近半小时:偶 ...

  4. [贝聊科技]如何实现一个 AttributedLabel

    作者:陈浩 贝聊科技移动开发部 iOS 工程师 Core Text 是苹果提供的富文本排版技术,可以定制开发图文混排功能,DTCoreText.Nimbus.YYLabel 等优秀的开源库底层都是基于 ...

  5. [贝聊科技]贝聊 iPhone X 适配实战

    @NewPan 贝聊科技 iOS 菜鸟工程师 这款为天猫定制的 iPhone,你买了吗?由于没摸过真机,所以严格意义上来说,这篇文章应该有一个更加接地气的名字:"模拟器适配实战". ...

  6. [贝聊科技]贝聊 IAP 实战之订单绑定

    大家好,我是贝聊科技 的 iOS 工程师 @NewPan. 注意:文章中讨论的 IAP 是指使用苹果内购购买消耗性的项目. 这次为大家带来我司 IAP 的实现过程详解,鉴于支付功能的重要性以及复杂性, ...

  7. [贝聊科技]iOS 代码架构(一)如何创建一个易复用的组件

    前言 贝聊的移动客户端分别有家长端和老师端,一家公司里同时维护多个业务上有关联性的app这种情况其实很常见,例如一些提供 O2O 服务的公司,经常会分用户端和商家端.这些客户端虽然各自负责着一个业务环 ...

  8. [贝聊科技]谈谈 iOS 如何动态切换 APP 的主题

    在移动互联网的下半场,越来越多的 APP 更加注重用户体验,以期来打动用户.主题的切换就是可以增强用户体验.结合运营活动的一个点:譬如 QQ 的夜间模式,节日里电商 APP 的皮肤切换等等的这些小细节 ...

  9. [贝聊科技]有关Android应用桌面角标(BadgeNumber)实现的探讨

    作者:小强 贝聊移动开发部 Android工程师 前言:本文主要讲述了以下三方面: 怎么在Android系统下让自家的应用图标像iOS系统那样支持数字角标的显示? 在网上找不到现成的解决方案的情况下, ...

最新文章

  1. 超易懂的MapReduce思想讲解
  2. C语言迷题:有符号数与无符号数的问题(转)
  3. 【机器学习】GitHub 标星17.4K:机器学习 100 天!
  4. 表的插入、更新、删除、合并操作_5_通过其它表插入
  5. Windows phone listbox动态添加列表项
  6. mysql同步到另一台服务器_mysql两台服务器主从同步
  7. CIKERS Shane 20190530
  8. Apache Ignite变得简单:第一个Java应用程序
  9. Python 数据分析三剑客之 Matplotlib(四):线性图的绘制
  10. 基于OpenCV检测影像中的车辆
  11. Bailian2909 字符串加空格【指针】
  12. ANDROID常用的命令(转载,后续自己完善)
  13. 华为lab-rs-v1-1.5_smart link
  14. python计算导数_科学网—python计算导数jacobian以及hessian - 于博的博文
  15. 【转载】一文读懂MCMC算法原理
  16. 程序员福音,关于如何使用Markdown写出一份漂亮的简历 —— 程序员简历 | md文档简历制作教程
  17. firefox 网页无法播放视频问题
  18. 高通骁龙200、400、600、800处理器规格分享
  19. 学习笔记———《最大类间方差法》
  20. 测试工程师如何提升自己

热门文章

  1. Python挺酷的 | PyCharm MicroPython ESP32 开发环境搭建
  2. Java期末复习题 超级无敌全
  3. 魔众工具箱系统 v1.1.0 随机串生成器工具、日期计算器工具、ASCII码对照表工具
  4. 混得不好,就别去思考
  5. getClass和getName
  6. 学完平面设计没有工作过如何找工作
  7. bilibili获取cookie
  8. STM32—驱动BT-06蓝牙模块传输数据
  9. 物业源码app 物业管理源码 物业源码
  10. RNA-seq技术原理