经过一段时间的工作(摸鱼划水),从几个很小的地方给大家介绍下我是如何提升编译构建速度的,但是本次分享内容还是主要针对当前阿逼的工程架构,不一定对你们有帮助哦。
我们工程内会在编译和同步阶段首先获取到整个工程的模型,之后计算出每个模块的version
版本。之前我们通过java.nio.file.FileVisitor
来进行工程文件遍历的操作。同样文件展开的api也是可以进行剪枝的,但是由于是用groovy
写的,我还是不太喜欢。
本次优化我们采用了kotlin
的file相关的walkTopDown
(可以快速的从上到下的遍历一个文件树)语法糖。然后通过其中的onEnter
dsl进行剪枝,然后我们可以通过filter
进行第二波过滤,筛选出我们实际要访问的目录。最后再进行一次foreach。
fun File.walkFileTree(action: (File) -> Unit) {
walkTopDown().onEnter {
# 对于不需要的目录进行剪枝
val value = if (!(it.isDirectory && (it.name == "build" || it.name == "src"))) {
!it.isHidden
} else {
false
}
value
}.filter {
val value = if (this == it) {
false
} else {
if (it.isDirectory) {
val build = File(it, "build.gradle")
build.exists()
} else {
false
}
}
value
}.forEachIndexed { _, file ->
action.invoke(file)
}
}
优化效果如下,原本没有剪枝的版本,我们本机进行一次FileWalker
需要1分钟左右的时间。使用剪枝的版本之后,我们可以把时间优化到2s左右的时间。我们跳过了些什么?
格式 | 是否跳过 |
---|---|
隐藏文件夹 | 是 |
build | 是 |
src | 是 |
文件 | 是 |
其他文件夹 | 否 |
通过上述的剪枝,我们可以减少非常非常多的文件遍历操作,在完成同样的能力的情况下,可以大大的加快文件的遍历操作。
另外,我们会获取对应文件下的git commit sha
值,然后作为该模块的version版本,而这个操作也是有几百毫秒的耗时,而我们工程大概有800+这样的模块,所以如果按照同步的方式去执行,就会变得耗时了。
而优化方案就比较简单了,我们通过线程池提供的invokeAll
方法,并发执行完之后再继续向下执行就可以完成该优化了。
Executors.newCachedThreadPool().invokeAll(callableList)
整体优化下来,原来在CI上一次FileWalker
需要1mins,因为CI的机器足够牛逼,所以优化完仅仅只需要3.5s就可以完成整个工程的遍历以及获取对应git commit sha
值的操作。
修改前:
修改后:
在AGP的打包流程中,会插入很多预检查的任务,比如类似kotlin版本检查, compileSdk版本检查等等任务。而这些任务即时不执行,也并不会影响本次打包任务,还是可以打出对应的apk产物的。当然前提是编译没有啥问题的情况下。
我们仔细观察了下apk打包下的所有的task,并观察了下task任务耗时情况,找到了几个看起来并没有什么实际用处的任务。
接下来就是如何去关闭这个任务了,其实方式还是比较简单的。我们主要用字符串的形式去获取到这个Task,然后把enable设置成false就可以了。但是前提是这个Task的输出并不会影响到后续的Task就行了。另外这几个应该是高apg新增的task,7.x才出现的低版本的是没有的。
afterEvaluate {
def metaDebugTask = tasks.findByName("checkApinkDebugAarMetadata")
def debugCheck = System.getenv().containsKey("ENABLE_APINK_DEBUG_CHECK")
if( metaDebugTask != null && !debugCheck) {
metaDebugTask.setEnabled(false)
}
def preCheck = tasks.findByName("checkApinkDebugDuplicateClasses")
if( preCheck != null && !debugCheck) {
preCheck.setEnabled(false)
}
}
这样我们就可以在一个构建中优化掉大概1min30s的时间了。这些手段相对来说都比较简单,另外我们还可以考虑把一些不互相依赖的任务从线性执行变成并行执行。可以参考gradle的Worker
相关api。
原始的baseversion
是基于文件内容的md5 生成的一个全局统一版本号,然后再结合仓库内的gitsha生成二进制缓存。但是由于大仓内的代码量越来越大,所以一旦变更baseversion
,需要消耗大概80min
左右的时间重新生成所有的二进制缓存。
虽然但是,其实并没有这个必要全部模块都进行一次发布。方法签名出现问题,我们只需要让对应的模块重编即可。所以这种全局性的baseversion
就需要进行迭代了。需要让baseversion
被拆解成多个,然后进行混合生成一个新值,底层改动可以影响到上层,而最上层模块也可以具备独立更新的能力。
# 后续该文件改动不会导致整个baseVersion变更
# 基于文件路径匹配的规则,可以给每个文件路径设置一个version,但是由于工程之间存在依赖,所以可以合并多个baseVersion到一起
# 测试结果如下,framework变更80min comm 变更 50min app上变更10min
# 后续会配合a8检查,直接通知各位那些模块的方法签名检查不通过,之后直接修改局部version版本就好了
# 全局默认混入所有 如果非必要情况下 别改这个版本号 !!!!!!!
- name: global
path:
version: 1
# app目录缓存
- name: app
path: app
version: 1
mixin: [ framework,common ]
# framework基础,该层目录变更之后所有向上的全部需要变更 非必要也最好不要改
- name: framework
path: framework
version: 2
mixin:
# comm 模块,该层目录变更之后所有向上的全部需要变更 非必要也最好不要改
- name: common
path: common
version: 2
mixin: [ framework ]
通过定义出一个新的yaml文件,我们可以定义出name
,path
代表相对路径,version
表示版本号,mixin
代表混合入别的name,而相对底层改动情况下会影响到上层模块重编。
这样,我们就可以在app下进行独立的版本号增加,让app目录下的模块进行一次重编,从而解决一部分方法签名问题导致的上层库缓存刷新。
测试结果大概如下:
目录 | 耗时情况 |
---|---|
global | 80min |
common | 50min |
app | 10min |
以下仅代表个人看法哦,我觉得编译优化还是要从本工程实际情况出发,你的工具箱内的工具要足够多,要先知道哪些东西是慢的你才会有思考的去进行一些优化,而不是很盲目的进行尝试。
另外优化不应该破坏整个工程的情况,我们不应该魔改整个编译流程,最好就是通过最小的手段去进行一些微量的优化,小步慢跑的进行一些对应的优化。