在mac较为高配的情况增量编译TIYA项目需要2分钟30s左右,这严重影响开发效率。在常规优化手段使用完后只能堆砌硬件进行提升效率是及其不可取,于是和一位同事决心做一次优化。
以下是benchmark统计出的增量数据
优化前:
结果:2分30秒
优化后:
27秒左右增量时间
在原始的AGP构建流程中,为了构建的稳定如果依赖的模块被改动那么上层模块会触发增量更新,如下图所示:
可能会问为什么模块b编译会触发模块A编译?
我们来看一个小荔枝:
//MyModuleB.java
package org.fmy;
public class MyModuleB {
public final int MY_B_VAL = 20;
}
//MyModuleA.java
package org.fmy;
public class MyModuleA {
static public int test(){
return new MyModuleB().MY_B_VAL;
}
}
我们通过字节码看看test函数编译成字节码后的表现:
在编译后被常量被内联到test中而不是通过类去获取。上面的字节码等价如下代码
package org.fmy;
public class MyModuleA {
public MyModuleA() {
}
public static int test() {
(new MyModuleB()).getClass();
return 20;
}
}
类比到开发中module依赖也是如此,在TIYA项目中很多时候不需要考虑这一情况,但是却严重拖累了整体开发效率。
在优化初期曾考虑过如果module如果没有改动话,那么将其封装aar在利用插件动态替换依赖即可。期间遇到很多问题与兼容性,最后这种方案技术实现麻烦且最后效益得不偿失。
动态切换依赖源会导致app模块的merge任务增量大量丢失,甚至不如不替换。这个动态依赖源插件在解决所有问题后发现一无是处,但是却为给未来提供了很多理论实现基础。
既然动态替换AAR无法有效的提升效率,那么换一种思想即可实现。我们检测模块B如果没有改动过,利用gradle相关api跳过模块所有任务即可达到同样的效果避免模块联动编译。
如此操作后你可以看到编译图有多个task被跳过。
最后完成禁止联动编译,但是编译速度提升的还是不够快。因为如果我们修改了R文件(修改布局,而R文件被多个文件引用)那么kapt和compile任务依旧很耗时
我们通过构件扫描图发现kapt和compile任务时间在增量情况下依然引起了效率问题
JSR269提供了编译时注解的功能,在TIYA中也使用了。在kotlin的启用的情况下,会有一个叫kapt插件去完成,我们首先看看kapt原理流程。
生产的java文件会在build/tmp/kapt3/stub/xxx下,并且包含metaclass信息,可以让你通过特殊方法读取原始的kotlin信息。
这里为什么kapt采用这种方式就不展开叙述,在新版中ksp将替换kapt这种dirty方式。
这里我们就需要替换原始kapt和compile的task为我们自己的task,自己实现增量。
为了更好的替换task我们就必须明白kapt和compile源码实现
kotlin源码仓库地址
相关task源码目录
关于如何编译kotlin-gradle-plugin插件可以参阅官方README.md.
Kotlin相关编译任务基本都会继承AbstractKotlinCompile
而其根本核心实现也在AbstractKotlinCompileTool的action函数中
abstract class AbstractKotlinCompileTool: AbstractCompile(), CompilerArgumentAwareWithInput , TaskWithLocalState, IHackCompilerFlag { //...省略代码 @TaskAction fun execute(inputs: IncrementalTaskInputs) { //传入增量信息在执行编译 executeImpl(inputs) } //...省略代码 }
但是问题来了我们如何进行相关Hack操作呢?
gradle官方提供了replace
public interface TaskContainer extends TaskCollection, PolymorphicDomainObjectContainer { T replace(String var1, Class var2); }
这个方法在多次实践后发现存在兼容问题且不能存在多个action传入IncrementalTaskInputs
反射?
实践得知也失败了
直接自己完整实现一个kotlin-gradle插件提供hack入口
我们在action执行前提供了相关回调函数你可以在这个函数中替换增量信息
abstract class AbstractKotlinCompileTool{
//提供了属性注入类
@Internal
@get:Internal
var hackCompilerIntermediary: HackCompilerIntermediary = HackCompilerIntermediary(this)
@TaskAction
fun execute(inputs: IncrementalTaskInputs) {
//hackCompilerIntermediary函数可以替换增量
val inputs = hackCompilerIntermediary.changeIncrementalTaskInputs(inputs)
//hackTaskAction提供了hack操作
if (hackCompilerIntermediary.hackTaskAction(inputs)) {
return
}
try {
executeImpl(inputs)
} catch (t: Throwable) {
}
}
}
在自己编译一个kotlin-gradle插件后我们有了一个hack入口可以自主掌握增量。那么只需要实现HackCompilerIntermediary类即可,在替换task内部属性。
这里给出一个示例代码
class AppFastHack constructor(task: Task) : HackCompilerIntermediary(task) {
val modified = ArrayList()
val removed = ArrayList()
override fun changeIncrementalTaskInputs(input: IncrementalTaskInputs): IncrementalTaskInputs {
if (!input.isIncremental) {
return super.changeIncrementalTaskInputs(input)
}
modified.clear()
removed.clear()
input.outOfDate { modified.add(it) }
input.removed { removed.add(it) }
val iterator = modified.iterator()
while (iterator.hasNext()) {
val next = iterator.next()
val file = next.file
//
if (file是其他模块java文件过滤掉牺牲内联) {
iterator.remove()
}
if (jar 文件变动 跳过) {
iterator.remove()
}
//....
}
return FastIncrementalTaskInputs(input.isIncremental, modified, removed)
}
}
参考文献
kapt原理1
kapt原理2
ksp介绍



