前言
本方案适合于单仓库(Monorepo)方式管理的项目,通过二进制化的想法减少编译工作量,并通过抛弃Xcode的自带的依赖管理机制,建立自定义的依赖管理去实现开发时的编译加速。本方案可做到对原项目无侵入性,兼容两套依赖管理机制,既可用Xcode进行依赖管理,也可以使用自定义的方式进行依赖管理,任何项目均可放心使用。
(本方案基于项目已经组件化,未组件化项目可先进行组件化再实施,整体改造难点在于依赖管理改造)
项目改进后,会有飞一半的质感,让大项目的编译速度回归到小项目一样。
如图,修改了组件A、组件C情况下,原Xcode依赖管理方案进行管理时,执行build clean后会重新编译所有代码编译。依赖管理改进方案只需要执行编译主工程、组件A、组件C,即完成编译工作,大大缩短了编译时间。
对Xcode熟悉以及对自身项目熟悉的开发者两天内可以完成改造,最长不超过一周时间,可以说成本很低,而且不影响原有项目的构建方式。
思路与原理
-
二进制组件化与多仓库管理陷阱
通过对组件预先编译得到二进制文件,那么在开发时主工程只需要执行链接的过程就可以完成构建工作。
如图2所示,通过将主工程拆分为组件后,通过对组件预先编译得到二进制文件,主工程构建时只需要对组件进行链接则可完成构建,时间可以大大缩短。理论上,主工程拆分出来越多,构建时编译时间就会越少。
通过以上的思路,很容易就会走向多仓库项目管理,Cocoapods一上,项目管理得整整齐齐。
但是,项目的组件依赖往往不如图2那么简单。如图3,组件D依赖组件A和B,修改组件A需要重新编译和发布组件A和组件D。(现实状况会比图3复杂的多得多)
虽然通过二进制化组件可以减少编译时间,但是一旦使用多仓管理这种重型项目管理方式,会使团队陷入了无止境的编译发布流程,光是Debug的工作就是噩梦般的存在。小项目,小团队使用多仓管理,等于把自己推向深渊。
-
单仓多组件全量编译工作量过大
单仓多组件没有组件发布流程,debug时也像无组件工程一样来去自如。这种组件化方式却无法缩短编译时间,只得到了分层的好处。
当然,还可以做成单多仓结合,业务组件修改频繁,放在主仓库,底层稳定组件封装,放在分仓库。这种方式能解决部分问题,但是编译时间还是冗长,毕竟业务组件基本都是大头。
如图4,当组件A修改后,理论上只需要重新编译A、D和主工程,但是实际操作上,一个Clean动作就把组件B、C的编译产物也清理了。也就是说,在单仓情况下,全量编译的情况是非常普遍的。
单仓多组件二进制化
所以,是否存在一种方案,在单仓多组件的项目管理方式下,解决如图4出现的,对组件B和组件C的冗余编译工作?
毫无疑问,理论上,这肯定是存在,因为构建的工作就是这样执行的。图4的情况下,保留组件B和组件C的编译产物就可以做到。
因此,我们需要对工程进行改进,把编译产物二进制文件缓存下来,并使用它们。(其实整体思路有点像增量编译那套,只是缓存的颗粒大一些)
如图5,通过对二进制产物缓存起来,并在项目中链接使用该产物,则可以实现。虽然看似简单,但实际上是需要将原来交由Xcode托管的依赖管理机制重新实现一遍。
4.依赖计算以及编译顺序
如图5中,修改了组件A,需要计算出需要重新编译的组件和顺序是:A、D。因此,若实施方案,则需要先梳理好所有组件的依赖关系,而不是像原来一样交个Xcode计算。确定好需要重新编译的组件后,安装依赖关系对组件按顺序重新编译。
5.增量编译兼容
实际工作中,需要执行增量编译。比如,上一次编译中,修改了组件A、D,也编译了组件A、D,后续又修改了D,则本次只需执行编译组件D,而不是组件A、D。而且,组件D执行的是增量编译,秒级的,而不是全量编译。笔者的方案是通过git diff进行hash计算修改,并使用Xcode的自身的增量编译。
实践
·基础知识
- Xcode的依赖管理
Xcode的依赖管理分为两种,第一种是显式依赖(Dependencies),第二种是隐式依赖(Implicit Dependencies)。
- 显式依赖(Dependencies)的管理放在了Build Phases下的Dependecis栏,开发者可以手动添加依赖,仅限workspace下的target
- 隐式依赖(Implicit Dependencies)是XCode在构建时进行计算得出的,具体计算范围是显式依赖基础上,叠加project下被链接的库(Link Binary With Libraries 以及 手动链接的库)
- Scheme、Target
Scheme是管理各种构建等配置的地方,其中有一项是隐式依赖的开关
Target是管理构建信息的地方,普通项目的target对应一个构建产物
Scheme和Target的概念大家可以网上找找,此处不赘叙。
·具体方案
整体构建流程:
Step0. Scheme处理
·为了不影响原构建方案,需要额外新建一个主工程的scheme,直接Xcode上new scheme,选择主工程(todo: 配图 打码)
·修改所有project的scheme,取消所有工程的Find Implicit Dependencies,从而取消Xcode的隐式依赖管理。
Step1. Target处理
·新建一个target,直接对原主工程target duplicate一个,并完善info.plist的指向
·新建一个target,选择脚本(本方案通过脚本执行组件的依赖管理以及构建工作)
·修改目标工程的依赖,新增脚本依赖
Step2. 缓存构建产物
·项目中新建文件夹对产物进行缓存,修改.gitignore
·主项目和子项目framework的链接地址(Link binary with Libraries)全部指向缓存文件夹
·通过脚本完成编译的组件二进制产物放到缓存文件夹(切记使用cp命令,不要使用mv命令,否则Xcode增量编译失效)
Step3. 依赖关系梳理(脚本实现)
Step4. 计算出需要编译的组件(脚本实现)
· 缓存文件夹中无的组件需要编译
· 被修改的组件需要编译
· 组件中依赖的组件被编译的需要重新编译
Step5. 执行构建(脚本实现)
Step6. git diff 文件hash计算
每个git diff的hash值对应一个二进制文件产物,通过hash值的比对来确定修改是否已经编译过。