一、概述
经过多年的发展,美柚iOS项目代码已经达到40W行+的规模,所使用的 Pod 库的数量达到了110+,App Store 安装包210M+,在这么大的项目规模下(CI机器 MAC配置:3 GHz 8-Core Intel Xeon E5;时间:发布20min+),(开发机器iMac :Retina 5K, 27-inch, 2017 融合硬盘;时间:build30min+)打包、编译问题逐步成为我们团队一个躲不过的痛,严重影响了我们的研发效率与其他团队之间的协作。
我们一台13年的ci机器同时需要承接七八个项目、多个分支的打包任务,在有多个项目同时打包的情况,显得尤其地力不从心。
在硬件资源有限的情况下,并且在无侵入、无影响现有的业务的前提下,如何解决这些摆在团队面前的难题,便成了我们迫在眉睫的迫切需求,最近半年多来一直在寻找加快打包速度的方案。
二、编译提速探索与尝试
1、CCache
CCache 是一个编译缓存器,一个能够把编译的中间产物缓存起来的工具
其原理是通过把项目的源文件用ccache编译器编译,然后缓存编译生成的信息,从而在下一次编译时,利用这个缓存加快编译的速度,目前支持的语言有:C、C++、Objective-C、Objective-C++
下面这张图基本就阐述了CCache的工作原理。
在项目中的实际编译流程
Ccache我们经过在工程的一番尝试、确实在某些方面上极大的提升了我们出包的速度。美柚iOS Ci打包从之前的最快20min+出包到最快10min,确实能够给我们带来比较不错的提升,大大加快了我们项目的出包速度。在我们项目运行了几个月后,对于我们项目的情况,也发现了一些问题,现在总结了以下几点:
优点:
- 满足我们追求的无侵入、无影响现有的业务的要求,无入侵、且开发人员无感知。
- 确实能大幅度地提升编译速度,美柚项目上最快时提高3倍以上的编译速度。
- 不需要对项目作出大调整,只需部署相关环境和一些脚本支持。
- 不需要改变开发工具链。
- 同一个目录下,CCache 的缓存命中率相对稳定。
对我们项目中有存在些问题点:
- 在未有缓存的情况下,首次打包编译的时间比原来的翻近一倍,原来20+min,首次将近40+min,在资源紧张的情况下,甚至是70min+。
- 修改一些引用较多的文件(如公共库、底层库改动),容易造成大范围的缓存失效,速度会变得比原来未使用ccache时更慢。
- 多个项目相同的组件不支持缓存共享,我们有多个分支打包的需求,修改目录名称后,缓存即失效。
- 我们机器的Ccache最大的缓存上限约18GB,且Debug/Release区别缓存,美柚iOS项目占用5GB+的缓存,多个项目、多个分支很容易超出上限,一台Ci机器同时支持多个项目会触发CCache清缓存。
- 对机器硬盘读写要求高,如不是全部固态硬盘,速度影响大。
- CCache 不支持 Clang Modules,系统框架例如 AVFoundation、CoreLocation等, Xcode 不会再帮你自动引入,会导致编译失败。
- CCache 不支持 PCH 文件
- CCache 目前不支持 Swift
2、静态库二进制方案的探索
虽然我们已经在Ci的在应用了Ccache已经有提升近一倍的出包速度了,但是存在的问题也比较明显。
在去年的某次技术周会上,我们的大佬提出了使用二进制编译的自研任务,可以更进一步提高研发效率。得到了大佬的启发后,就一直在实践与探索二进制之路上。
我们的项目使用 CocoaPods 来管理第三方库和私有库的依赖,对大部分项目来说应该是标配了。目前还是纯 Objective-C 的项目,有少量C++,暂没有引入 Swift。
3、 调研过的二进制组件方案
下面列出研究过的一些主流方案以及最后没有采用的原因,这些方案有各自的局限性,但是也给了我不少启发,思考过程跟最终方案一样有价值。
3.1、Carthage
Carthage可以将一部分不常变的库打包成framework,再引如到主工程,这样可以减少开发过程中的编译时间。Carthage 可以比较方便地调试源码。因为我们目前已经大规模使用 CocoaPods,转用 Carthage 来做包管理需要做大量的转换工作,变动太大,不满足我们的无侵入、无影响现有的业务,所以不考虑这个方案了。
3.2、cocoapods-packager
cocoapods-packager 可以将任意的 pod 打包成 Static Library,省去重复编译的时间,一定程度上可以加快编译时间,但是也有自身的问题:
- 优化不彻底,只能优化第三方和私有 Pod 的编译速度,对于其他改动频繁的业务代码无能为力
- 私有库和第三方库的后续更新很麻烦,当有源码修改后,需要重新打包上传到内部的 Git 仓库
- 过多的二进制文件会拖慢 Git 的操作速度(目前还没部署 Git 的 LFS)
- 难以调试源码,不共享编译缓存
- 打包成 Static Library 过程缓慢,需要通过pod lint,各个组件间又层层嵌套依赖,在现有阶段来说,是难以实现的。
3.3、cocoapods-binary
Cocoapods-Binary(Cocoapods 官方推荐的二进制插件), 是一个即时生成二进制包并缓存,而非像 CocoaPods-Packager 仅仅针对单个私有库的。原理是通过 CocoaPods 提供的 pre_install hook 在 pod install 的 prepare 阶段拦截到当前的 pod install context,进而 fork 出一份独立的 installer 以完成将预编译源码 clone 至 Pod/_Prebuild 目录下,同时也存在几个不足之处:
- 单私有源,无法实现服务端缓存,在没有对应二进制包版本时,pod install 后会额外去做二进制包的生成,一定程度上会影响 pod install的速度。
- 开发者切回源码调试,二进制缓存会一并清空,需求重新编译。
- 多个项目、不同分支的相同组件依旧无法共享
- 只支持framework,对我们项目现状需要比较大的头文件引用方式改动。
3.4、cocoapods-bin 双私有源
该插件进行二进制化的策略是采用双私有源,即2个源地址,一个静态服务器保存预先打好包的framework,一个是我们现在保存源码的服务地址,在install的时候去选择使用下载那个,是个很不错的项目,深受启发。
优点:
- 源码和二进制文件之间可以来回切换,速度比较快
- 不影响未接入二进制化方案的业务团队
- 无二进制版本时,自动采用源码版本
- 接近原生 CocoaPods 的使用体验
对于在我们项目中存在的不足之处:
- 不支持指定分支,:podspec =>'', :git 方式的引用,对需要支持多个分支、多个业务线的项目是致命的。
- Archive二进制文件时,只能去spec仓库下载源码,无法根据指定的分支去下载依赖库,导致编译失败、错乱的问题
- 依赖的组件需要推送到spec仓库,很多私有库并没有推送到仓库,且对于频繁改动的私有库,推送到仓库的verify很慢且与我们的开发习惯不符。
- 不支持.a静态文件输出,项目中大量类似 #import "IMYPulic.h"需要一个个库去编译替换为#import ,想想那110多个组件库~
- 只支持一套环境,对于有Debug/Release/Dev开发环境需求的无法满足
- 不支持二进制组件的源码调试
- 不能流畅的支持频繁变动的业务组件,操作会异常繁琐。
- 针对于我们的项目,目前存在较大的障碍,无法使用起来。
4、 思考与总结
经过一个多月来对业界存在的*的分析和思考,并在一定的实践后,最后我们决定自己造一个灵活的、可配置的、简便的、无入侵的、双私有源二进制组件辅助插件。
接下来就撸起袖子,努力干吧~,骚年
三、双私有源二进制组件简介
在受到cocoapod-bin启发后,在借鉴它的部分框架下,我们实现了自己的二进制辅助插件cocoapods-imy-bin,并新增了几个命令和二进制源码调试能力。
1、能做什么?只要能编译通过,就制作
在cocoapods-imy-bin的辅助下,能无侵入式自动化地制作所有符合条件的组件为二进制,且对于频繁的业务组件也能轻松的应用上二进制组件,无需多余操作,一切交给cocoapods-imy-bin自动化运行。
同时对于研发人员,也能提供独立的二进制组件给研发人员使用,解决日常的编译 效率、跑真机效率低下,被墙等各种问题。
我们的口号是: 只要能编译通过,就制作。 一次编译到处使用,无入侵。
即使独立的组件库编译不通过,整体项目能编译通过也制作。
整套环境下来,没有让我们的开发人员改变原来的开发习惯,没有改动业务中相关的代码,基本上做到了使用人员无感知状态。
2、Ci打包效果
2.1 单项目 - 编译最快2分钟一次
上图是个由我们打了几千个包的经验得出对单个项目编译时间大致的曲线图。这里假设一台机器只一次只有一次job。Y轴编译时间,X轴某次的编译, 红色线条表示的是原生(未使用Ccache和二进制组件),黄色线表示使用了Ccache,蓝色表示使用了二进制组件。
由图可以看出来在无任何辅助下原生的编译时间曲线(红色)是趋于平缓,在20min上下左右。Ccache和二进制第一次在无任何缓存的情况下,在一定程度上是会比原生的耗时,Ccache主要耗时在边编译边缓存项目的编译产物。二进制主要耗时在编译完成后,对.a编译产物的组装和push到私有源仓库的时间上(这个跟所采用有关系,如果没有利用Jenkins 编译后的产物制作二进制就不存在。)。
在ccache完全命中、二进制文件完全都存在的情况下,ccache比原生的提高一倍以上, 二进制会比ccache编译时间再提高一倍,且稳定在2分钟左右。二进制在之后的表现更趋于平稳,而ccache在修改了某个被引用较多的文件时、如底层的公共文件后,命中率就会大大地降低,有时会比不用ccache更耗时,如#4位置。在ci有多个job同时并发在跑的情况下,由于ccache 需要对IO频繁地读写操作,耗时表现可能会更糟糕些,我们经常遇到过等了七十几分钟才出包的情况。
二进制的编译时间相对平稳很多(蓝色曲线),在我们架构强有力的支撑下,划分出110多个独立组件,每次的打包基本上是就耗在某个组件的编译+archive。如果是某些变更比较频繁的组件,我们还可以考虑对颗粒较大组件配上ccache,做双层编译缓存。双层编译缓存原理是Pods组件库无二进制组件采用源码编译时,源码编译同时应用ccache缓存支持,加速源码组件的编译。
同时组件库可以配合Gitlab-Ci的runner的应用,每次已提交代码就触发独立组件的制作二进制,让每次的编译速度都达到最快,蓝色二进制曲线将会更接近直线。Gitlab-Ci具体的使用教程参见后文。
如果存在有独立组件无法编译问题和版本依赖问题,也可以再跑个定时Job,或者其他轮询条件Job,及时提供最新二进制组件。
2.2、多项目情况
一台机器上多个项目的ccache显得是比较吃力的,且不稳定,超出ccache的缓存最大值就会被清掉。
使用了二进制后,即使是多个项目编译时间都是趋于比较平稳的。这里面的原理估计大家都能想得到为什么。
3、开发使用效果 - 10倍以上的提升
在Podfile引入插件后,在pod install/update后,符合条件的情况下,会自动转换为二进制组件。
在我们的开发机器(iMac :Retina 5K, 27-inch, 2017 融合硬盘;)上,全量代码之前Build需要30min+,现在使用全部使用二进制后,编译最快只需要2min+就可以,提高的效率达到10倍以上。
当您在使用独立组件库编译开发的时候,其实不妨试试这个二进制的方案去跑整个项目,说不定二进制的方案比独立组件库跑起来还迅速。
3.1.源码编译
Ps:110+个Pods库中,有20+个稳定Pods库已经被制作为二进制库,并非全部源码编译,如何全部转换为源码编译,实际数字会比这多出很多。
3.2. 二进制编译 - 全量最快2分钟
Ps:有2个Pods和5个Action Extension使用源码编译,其他全部是二进制Pods。
在二进制Build 127秒中(arm64和armv7),除了源码编译的时间外,约45秒消耗在copy pods Resource。
实际在编译模拟器x86_64架构时只需要90秒不到的时间。
全量编译中,13496个Tasks/727个Tasks,1710秒(28.5分钟)/127秒(2分钟),编译速度提升的速度远远超过10倍。
3.3 演示
在环境搭建完后,开发人员在Podfile中,加入以下两句,就能享用到自动切换为二进制组件,体验极速编译。
plugin 'cocoapods-imy-bin'
use_binaries!
4、功能点
目前cocoapods-imy-bin插件支持的功能如下
- 无侵入、无影响现有的业务。
- 不影响未接入二进制化方案的业务团队,提供配置文件。
- 只要项目能编译通过就制作,即使独立组件编译失败。
- 支持无二进制版本时,自动采用源码版本。
- 支持只需项目能编译通过就能制作二进制组件,无需再关心pod lint等。
- 支持pod bin local 命令一键自动化制作、上传、存储项目本地已经存在的二进制组件,可配合ci打包的编译产物使用。
- 支持指定依赖分支、支持:podspec =>'', :git 方式的引用
- 支持同时 .a、Framework 静态库产出
- 支持archive时,根据Podfile自动获取podsepc依赖的库,无需强制去spec仓库拉取。
- 支持多套隔离环境,如Debug/Release/Dev配置,方便为Debug/Release/Dev各种环境提供专用二进制组件。
- 支持输出.a二进制组件制作binary.podsepc无需模板。
- 支持稳定的二进制组件,在上传二进制组件的binary.podsepc跳过pod lint验证,加快速度。
- 支持pod bin auto 命令一键自动化制作、上传、存储单个二进制组件
- 支持pod bin auto --all-make 命令一键自动化制作、上传、存储该项目下所有组件的二进制组件
- 支持 是否使用二进制文件、是否制作二进制文件和二进制/源码调试功能的白名单设置
- 支持pod install/update 多线程模式,加快pod过程,Pod速度提升80%+。
- 支持pod bin install/update 命令,实现无入侵修改Podfile内容,避免直接修改工程的Podfile文件而导致提交冲突、误提交。
- 支持pod bin code命令,实现二进制库不切换源码库、程序无需重新运行的调试能力
原文地址:https://juejin.cn/post/6903407900006449160