随着android的组件化的到来,一个项目后期功能越来越多,模块拆分的越来越多,作为android的开发的小伙伴就不得不面对运行一下android项目可能需要5,6分钟甚至10几分钟的等待期,开发时间都浪费在编译上了,你说烦不烦呢!那么怎么解决这个困境,总不能就这么一直凑合着吧,反正也没太大影响开发,只是速度慢了一点而已。
那么怎么让编译加快呢,自从把eclipse改用android studio之后后,出现了一种新的引用格式aar,aar格式是编译好的资源和类,那么有没有一种可能将主模块依赖的的所有模块库转化为aar依赖,在某个模块库变化的时候将它转化为aar依赖,不变化的时候转化为库依赖,那么编译速度会不会大大提高,答案是肯定的了。有人为说了,把库都部署到maven不就行了,当然,maven远程库确实有这个功效,但是,每写一个新的模块库是不是都得把它部署到maven上,有没有一种可能就是写一个脚本插件上这么新的模块自动转化为Maven的库文件,这种方案就就非常ok了。
要实现这个方案,首先咱们要面对以下这些问题:
- 需要通过
gradle plugin
的形式动态修改没有改动过的module
依赖为 相对应的aar
依赖,如果module
改动,退化成project
工程依赖,这样每次只有改动的module
和app
两个模块编译。 - 需要把
implement/api moduleB
,修改为implement/api aarB
,并且需要知道插件中如何加入aar
依赖和剔除原有依赖 - 需要构建
local maven
存储未被修改的module
对应的aar
(也可以通过flatDir
代替速度更快) - 编译流程启动,需要找到哪一个
module
做了修改 - 需要遍历每一个
module
的依赖关系进行置换,module
依赖怎么获取?一次性能获取到所有模块依赖,还是分模块各自回调?修改其中一个模块依赖关系会阻断后面模块依赖回调? - 每一个
module
换变成aar
之后,自身依赖的child
依赖 (网络依赖,aar
),给到parent module
(如何找到所有parent module
) ? 还是直接给app module
? 有没有app
到module
依赖断掉的风险? 这里需要出一个技术方案。 - 需要
hook
编译流程,完成后置换loacal maven
中被修改的aar
1、如何手动添加 aar
依赖,分析implement
源码实现入口在 DynamicAddDependencyMethods
中的 tryInvokeMethod
方法。他是一个动态语言的 methodMissing
功能
java
public DynamicInvokeResult tryInvokeMethod(String name, Object... arguments) {
//省略部分代码 ...
return DynamicInvokeResult.found(this.dependencyAdder.add(configuration, normalizedArgs.get(0), (Closure)null));
}
private class DirectDependencyAdder implements DependencyAdder<Dependency> {
private DirectDependencyAdder() {
}
public Dependency add(Configuration configuration, Object dependencyNotation, @Nullable Closure configureAction) {
return DefaultDependencyHandler.this.doAdd(configuration, dependencyNotation, configureAction);
}
}
public Dependency add(String configurationName, Object dependencyNotation) {
return this.add(configurationName, dependencyNotation, (Closure)null);
}
public Dependency add(String configurationName, Object dependencyNotation, Closure configureClosure) {
//这里直接调用到了 doAdd
return this.doAdd(this.configurationContainer.getByName(configurationName), dependencyNotation, configureClosure);
}
而 doAdd
方法三个参数通过 debug
源码发现,configuration
就是 "
implementation", "
api", "
compileOnly"
这三个字符串生成的对象,dependencyNotation
是一个 LinkHashMap
有两个键值对,分别是 name:aarName
, ext:aar
,最后一个configureAction
传 null
就可以了,调用 project.dependencies.add
最终会调到 doAdd
方法,也就是说直接调用 add 即可。
2、localMave
优先使用 flatDir
实现通过指定一个缓存目录 getLocalMavenCacheDir
把生成 aar/jar
包丢进去,依赖修改时候通过 上面的 4.1 添加对应的 aar
即可
java
fun flatDirs() {
val map = mutableMapOf<String, File>()
map.put("dirs", File(getLocalMavenCacheDir()))
appProject.rootProject.allprojects {
it.repositories.flatDir(map)
}
}
3、编译流程启动,需要找到哪一个 module
做了修改
- 使用遍历整个项目的文件的
lastModifyTime
去做实现 - 已每一个
module
为一个粒度,递归遍历当前module
的文件,把每个文件的lastModifyTime
整合计算得出一个唯一标识countTime
- 通过
countTime
与上一次的作对比,相同说明没改动,不同则改动. 并需要同步计算后的countTime
到本地缓存中
计算每个module文件改变的时间代码如下:
java
project.rootProject.allprojects.onEach {
if (it == project.rootProject || it.childProjects.isNotEmpty()) {
return@onEach
}
var countTime = 0L
it.projectDir.eachFileRecurse { file ->
// 过滤掉build目录及该目录下的所有文件
isCodeFile = !(file.isDirectory && Contants.BUILD == file.name)
if (isCodeFile) {
countTime += file.lastModified()
count++
}
return@eachFileRecurse isCodeFile
}
newModuleList.add(ModuleChangeTime(it.path, countTime))
}
原理就是将所有moudle下的代码和资源的最后修改时间累加,在编译的时候对比前后时间,如果一只就用aar依赖,如果不一致就用项目依赖。
4、module
依赖关系 project
替换成 aar
技术方案
- 每一个
module
依赖关系替换的遍历顺序是无序的,所以技术方案需要支持无序的替换 - 目前使用的方案是:如果当前模块
A
未改动,需要把A
通过localMaven
置换成A.aar
,并把A.aar
以及A
的child
依赖,给到第一层的parent module
即可
5、hook
编译流程,完成后置换 loacal maven
中被修改的 aar
java
//如果当前模块是改动模块,需要打 aar
if (mAllChangedProject?.contains(childProject.path) == true) {
//打包aar
val bundleTask = getBundleTask(childProject, buildType.capitalize())?.apply {
task.configure {
it.finalizedBy(this)
}
}
if (enableLocalMaven) {
// publish local maven
bundleTask?.let { bTask ->
LogUtil.d("bTask=$bTask")
val buildType = if (bTask.name.contains("release")) {
"Release"
} else {
"Debug"
}
try {
val publishMavenTask =
childProject.project.tasks.named("publishMaven${buildType}PublicationToLocalRepository").orNull
publishMavenTask?.let {
bTask.finalizedBy(it)
}
} catch (e: Throwable) {
e.printStackTrace()
}
}
} else {
//copy aar
val localMavenTask =
childProject.tasks.maybeCreate("uploadLocalMaven" + buildType.capitalize(),
FlatTask::class.java)
localMavenTask.localMaven = this@AarFlatLocalMaven
bundleTask?.finalizedBy(localMavenTask)
}
}
这些代码的意思就是获取将每个moudle转化为aar的任务,然后在这个任务结束后创建新的任务FlatTask将这些aar放到localMaven中。FlatTask源码如下:
Groovy
open class FlatTask : DefaultTask() {
@Internal
var inputPath: String? = null
@Internal
var inputFile: File? = null
@Internal
var outputPath: String? = null
@Internal
var outputDir: File? = null
@Internal
lateinit var localMaven: AarFlatLocalMaven
@TaskAction
fun uploadLocalMaven() {
val flatAarName = getFlatAarName(project)
this.inputPath = FileUtil.findFirstLevelAarPath(project)
this.outputPath = FileUtil.getLocalMavenCacheDir()
inputFile = inputPath?.let { File(it) }
outputDir = File(this.outputPath)
inputFile?.let {
File(outputDir, flatAarName + ".aar").let { file ->
if (file.exists()) {
file.delete()
}
}
it.copyTo(File(outputDir, flatAarName + ".aar"), true)
localMaven.putIntoLocalMaven(flatAarName, flatAarName + ".aar")
}
}
}
}