谈一谈如何加快android的项目的编译速度

随着android的组件化的到来,一个项目后期功能越来越多,模块拆分的越来越多,作为android的开发的小伙伴就不得不面对运行一下android项目可能需要5,6分钟甚至10几分钟的等待期,开发时间都浪费在编译上了,你说烦不烦呢!那么怎么解决这个困境,总不能就这么一直凑合着吧,反正也没太大影响开发,只是速度慢了一点而已。

那么怎么让编译加快呢,自从把eclipse改用android studio之后后,出现了一种新的引用格式aar,aar格式是编译好的资源和类,那么有没有一种可能将主模块依赖的的所有模块库转化为aar依赖,在某个模块库变化的时候将它转化为aar依赖,不变化的时候转化为库依赖,那么编译速度会不会大大提高,答案是肯定的了。有人为说了,把库都部署到maven不就行了,当然,maven远程库确实有这个功效,但是,每写一个新的模块库是不是都得把它部署到maven上,有没有一种可能就是写一个脚本插件上这么新的模块自动转化为Maven的库文件,这种方案就就非常ok了。

要实现这个方案,首先咱们要面对以下这些问题:

  • 需要通过 gradle plugin 的形式动态修改没有改动过的 module 依赖为 相对应的 aar 依赖,如果 module 改动,退化成 project 工程依赖,这样每次只有改动的 moduleapp 两个模块编译。
  • 需要把 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 ? 有没有 appmodule 依赖断掉的风险? 这里需要出一个技术方案。
  • 需要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,最后一个configureActionnull 就可以了,调用 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 以及 Achild 依赖,给到第一层的 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")
            }
        }
    }


}
相关推荐
拭心1 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王4 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡4 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道4 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库5 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道6 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe6 小时前
Android Hook - 动态加载so库
android
居居飒7 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He10 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗10 小时前
Android笔试面试题AI答之Android基础(1)
android