Android构建优化:基于Git Diff+TaskGraph

文章目录

基于Git Diff(Git状态感知)和TaskGraph(任务依赖图)实现的一个基于Git状态感知的构建优化方案,让Gradle只编译变更的模块,减少一部分执行阶段成本时间。

原因分析

在大型Android项目中,构建效率直接影响开发体验和迭代速度。传统构建流程存在以下的问题:

即使只修改了少量代码,Gradle也需要扫描所有任务。模块module 内代码没有发生变化,编译时task的状态显示为UP-TO-DATE,Gradle判断是否UP-TO-DATE 的task内部仍然会执行输入输出缓存对比 (检查文件指纹如哈希值或时间戳、与上一次构建的快照进行对比),不同的task缓存对比开销不同,发现部分task的UP-TO-DATE检查耗费需要数百毫秒 ,当module数量越多,这些代码没有变更的module的task累积起来就会造成数秒或分钟的无效等待

因此,若能提前识别出完全不受变更影响的模块,并在其对应 task 被调度前直接跳过(skip),即可避免不必要的 UP-TO-DATE 检查开销。

方案设计

大致执行流程

技术实现

主要分为四层

1.Git状态监控层

职责:检查Git环境是否发生结构性变化(如切换分支、拉取新commit),决定是否应禁用裁剪以保证构建正确性。

代码实现

groovy 复制代码
//全局状态管理
ext._gitState=[
    headChanged :false,//Git HEAD是否发生变化
    branchChanged:false,//当前分支是否发生变化
    lastHead:null,//上次记录的Git HEAD的值
    lastBranch:null, //上次记录的分支名称
    stateInited:false,//git状态是否已初始化
    taskGraphInstalled:false,//Task Graph监听器是否已安装
    trimFailed:false //上次裁剪是否失败
] as Map//全局状态管理


//初始化Git状态
def initGitState = {
    //防止在单次构建中被多次调用(例如多个回调误触发
    if (ext._gitState.stateInited) return
    ext._gitState.stateInited = true
    def root = project.rootProject

    // 使用 :app 模块的 build 目录
    def appProject = root.subprojects.find { it.name == 'app' }
    if (!appProject) {
        throw new GradleException(":app module not found for git-state!")
    }
    def stateDir = new File(appProject.buildDir, 'git-state')
    stateDir.mkdirs()

    //读取上一次记录
    def headFile = new File(stateDir, 'last_head.txt')
    def branchFile = new File(stateDir, 'last_branch.txt')
    ext._gitState.lastHead = headFile.exists() ? headFile.text.trim() : null
    ext._gitState.lastBranch = branchFile.exists() ? branchFile.text.trim() : null
    //获取当前状态
    try {
        def r1 = execGit(['rev-parse', 'HEAD'], root.rootDir)
        def r2 = execGit(['rev-parse', '--abbrev-ref', 'HEAD'], root.rootDir)
        def curHead = r1.exit == 0 ? r1.out : null
        def curBranch = r2.exit == 0 ? r2.out : null

        ext._gitState.headChanged = curHead != null && ext._gitState.lastHead != curHead
        ext._gitState.branchChanged = curBranch != null && ext._gitState.lastBranch != curBranch
        // 保存当前状态
        headFile.text = curHead ?: ''
        branchFile.text = curBranch ?: ''


    } catch (Exception e) {
        ext._gitState.headChanged = true
        ext._gitState.branchChanged = true
    }

    //失败标记也放在 app/build/
    def failFlag = new File(appProject.buildDir, OPT_CONFIG.trimFailFlagFile)
    ext._gitState.trimFailed = failFlag.exists()
}

def execGit(List<String> args, File workDir) {
    def cmd = ['git'] + args
    def proc = cmd.execute(null, workDir)
    def out = new StringBuilder()
    def err = new StringBuilder()

    // 等待进程完成并捕获输出
    proc.waitForProcessOutput(out, err)

    return [
            exit: proc.exitValue(),
            out: out.toString().trim(),
            err: err.toString().trim()
    ]
}
  • HEAD变更检测 → 通过 git rev-parse HEAD 对比;

  • 分支变更检测 → 通过 git rev-parse --abbrev-ref HEAD 对比;

  • 状态持久化,防止重复全量构建。

2.任务裁剪层

职责:基于代码变更和依赖关系,精准裁剪构建任务图

代码实现

groovy 复制代码
 //  Step 1: 跳过非必需任务(lint/test等)
        graph.allTasks.each { task ->
            cfg.skipTaskNamePatterns.each { pattern ->
                if (task.name.contains(pattern)) {
                    task.enabled = false
                    println "⏭️  Skipped: ${task.path} (matches '${pattern}')"
                }
            }
        }

        // Step 2: 分析 Git 变更文件 → 识别变更模块
        def changedModules = [] as Set<String>
        cfg.gitDiffArgs.each { List<String> args ->
            try {
                def r = execGit(args, root.rootDir)
                if (r.exit == 0) {
                    r.out.split('\n').each { String filePath ->
                        def mod = pathToModule(filePath)
                        if (mod) changedModules << mod
                    }
                }
            } catch (Exception ignored) {}
        }

        //  Step 3: 依赖传递扩展
        if (changedModules) {
            def androidProjs = collectAndroidProjects(root)
          //被依赖模块->所有直接依赖他的模块
            def depMap = buildDependentsMap(androidProjs, cfg.dependentConfigs)
          //递归找出直接或间接依赖它的模块
            def expanded = expandWithDependents(changedModules, depMap)
            if (expanded.size() > changedModules.size()) {
                println "🔗 Expanded by dependencies: ${expanded - changedModules}"//-是集合差集
            }
            changedModules = expanded
        }

        //  Step 4: 裁剪未变更模块的编译任务
        graph.allTasks.each { task ->
            // 仅处理编译类任务(避免误伤其他任务)
            if (!cfg.compileTaskPatterns.any { task.path.contains(it) }) return

            // 提取所属 module
            def modulePath = task.path.substring(0, task.path.lastIndexOf(':'))
            if (!changedModules.contains(modulePath)) {
                task.enabled = false
                println "🚫 Disabled: ${task.path} (module unchanged)"
            } else {
                println "✅ Compiling: ${task.path}"
            }
        }


// 解析文件路径:Module路径(为了和后面的buildDepentsMaps的key匹配)
String pathToModule(String filePath) {
    if (!filePath) return null
    def parts = filePath.replace('\\', '/').split('/')
    def moduleParts = []
    for (String part : parts) {
        if (part == rootProject.ext.OPT_CONFIG.modulePathStopSegment) break
        moduleParts << part
    }
    return moduleParts.isEmpty() ? null : ':' + moduleParts.join(':')
}
// 收集所有 Android 模块
Set<Project> collectAndroidProjects(Project root) {
    return root.allprojects.findAll { p ->
        p.plugins.hasPlugin('com.android.application') ||
                p.plugins.hasPlugin('com.android.library')
    } as Set<Project>
}

// 构建依赖图:depPath → [consumerPaths]
Map<String, Set<String>> buildDependentsMap(Iterable<Project> androidProjects, List<String> configs) {
    def dependents = [:] as Map<String, Set<String>>
    androidProjects.each { Project consumer ->
        configs.each { String confName ->
            def conf = consumer.configurations.findByName(confName)
            if (!conf) return
            conf.dependencies.withType(ProjectDependency).each { ProjectDependency pd ->
                def depPath = pd.dependencyProject.path
                dependents.putIfAbsent(depPath, [] as Set)
                dependents[depPath].add(consumer.path)
            }
        }
    }
    return dependents
}

// 依赖传递扩展
Set<String> expandWithDependents(Set<String> changed, Map<String, Set<String>> dependents) {
    def expanded = new LinkedHashSet<>(changed)
    def queue = new LinkedHashSet<>(changed)
    while (!queue.isEmpty()) {
        def m = queue.iterator().next()
        queue.remove(m)
        dependents[m]?.each { String consumer ->
            if (!expanded.contains(consumer)) {
                expanded.add(consumer)
                queue.add(consumer)
            }
        }
    }
    return expanded
}
  • 非必须任务过滤 → 显式跳过 lint/test/kaptTest 等
  • 变更模块识别→ 通过 git diff --name-only + pathToModule() 解析模块路径
  • 依赖传递扩展→ 构建反向依赖图,递归扩展受影响模块
  • 编译任务裁剪→ 仅启用变更模块及其依赖的编译任务

buildDependentsMap(...) 会生成一个 "被依赖者 → 依赖者" 的映射,例如:

groovy 复制代码
def depMap = [
  ":core:framework": [":feature:login", ":feature:home", ":common:ui"],
  ":common:ui":      [":feature:home"],
  ":feature:login":  [":app"],
  ":feature:home":   [":app"]
]

递归查找所有直接或间接依赖 common-network 的模块:

初始:"common-network"

第一层依赖者:feature-login, feature-home, common-ui

再找这些模块的依赖者:

feature-login → app

feature-home → app

common-ui → feature-home(已包含)

最终结果:

expanded = "common-network", "feature-login", "feature-home", "common-ui", "app"

3.编译优化层

职责:提升单模块编译效率

groovy 复制代码
allprojects { Project p ->
    afterEvaluate {
        def cfg = rootProject.ext.OPT_CONFIG

        // 🟢 Kotlin 优化
        def kotlinClasses = [
                'org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile',
                'org.jetbrains.kotlin.gradle.tasks.KotlinCompile'
        ]
        kotlinClasses.each { className ->
            try {
                Class<?> cls = Class.forName(className)
                tasks.withType(cls).configureEach { task ->
                    task.kotlinOptions {
                        jvmTarget = cfg.kotlinJvmTarget//指定JVM版本
                        freeCompilerArgs += cfg.kotlinFreeArgs //添加额外编译参数
                        suppressWarnings = true //抑制警告提升速度
                        incremental = cfg.kotlinIncremental
                    }
                }
            } catch (ClassNotFoundException ignored) {}
        }

        // 🔵 Java 优化
        tasks.withType(JavaCompile).configureEach {
            options.incremental = cfg.javaIncremental //启用java增量编译
            options.fork = cfg.javaFork //是否fork出独立进程编译
            if (cfg.javaMaxMem) options.forkOptions.memoryMaximumSize = cfg.javaMaxMem //限制fork进程内存
        }

        // 🟣 测试并行化
        tasks.withType(Test).configureEach {
            int cpus = Runtime.runtime.availableProcessors()
            int divisor = cfg.testParallelDivisor ?: 2
            maxParallelForks = Math.max(cpus.intdiv(divisor), 1)//并行测试进程数
            forkEvery = 100 // 每执行 100 个测试用例就重启 JVM(防内存泄漏)
        }

        // 🟠 Android 特定优化
/*        if (p.plugins.hasPlugin('com.android.application') ||
                p.plugins.hasPlugin('com.android.library')) {

            android {
                aaptOptions {
                    cruncherEnabled = false // 通常不需要
                }
                compileOptions {
                    coreLibraryDesugaringEnabled = true
                    targetCompatibility JavaVersion.VERSION_1_8
                    sourceCompatibility JavaVersion.VERSION_1_8
                }
            }
        }*/
    }
}
  • Kotlin编译优化 → 增量编译 + 编译参数
  • Java编译优化 → 增量编译 + fork + 内存控制
  • 测试并行化 → 多进程并行执行测试

4.容错控制层

职责:防止智能裁剪导致构建失败后陷入"永久失败"循环,支持自动回退

groovy 复制代码
//初始化initGitState时,检查失败标记
def initGitState = {
  ....
        def failFlag = new File(appProject.buildDir, rootProject.ext.OPT_CONFIG.trimFailFlagFile)
    ext._gitState.trimFailed = failFlag.exists()
}

gradle.taskGraph.whenReady { graph->
 ....
   //裁剪前判断
     boolean allowTrim = !ext._gitState.trimFailed &&
                !ext._gitState.headChanged &&
                !ext._gitState.branchChanged
  
}
// 构建完成后更新状态
    gradle.buildFinished { result ->
        def root = project.rootProject
        def appProject = root.subprojects.find { it.name == 'app' }
        if (!appProject) return // 安全兜底
        def cfg = root.ext.OPT_CONFIG
        def failFlag = new File(appProject.buildDir, cfg.trimFailFlagFile)

        if (result.failure == null) {
            if (failFlag.exists()) {
                failFlag.delete() // 成功则清除标记
                println "\n✅ Build succeeded. Trim marker cleared."
            }
        } else {
          // 失败则保留标记 → 下次走全量构建
            println "\n❌ Build failed. Trim marker preserved for safety."
        }
    }
  • 失败标记机制 → 使用文件 last_trim_failed.flag 记录上次裁剪失败
  • 自动回退策略→ 一旦失败或检测到 HEAD/分支变化,立即禁用裁剪,回归安全的全量构建,成功则删除失败标记

构建报告

每次构建会

groovy 复制代码
  Git-Aware Build Optimizer v1.0
  HEAD changed: false
  Branch changed: false
  Last trim failed: false
  → Allow trimming: true
  Final changedModules: [:build_optimization.gradle, :common:recharge, :common:user]
  Expanded by dependencies: [:app, :common:advert, :common:wear]
🚫 Disabled: :common:player:compileDebugLibraryResources (module unchanged)
🚫 Disabled: :common:quicktaskcenter:compileDebugLibraryResources (module unchanged)
✅ Compiling: :common:recharge:compileDebugLibraryResources
🚫 Disabled: :common:resouces-v10:compileDebugLibraryResources (module unchanged)
🚫 Disabled: :common:resources:compileDebugLibraryResources (module unchanged)
🚫 Disabled: :common:stat-report:compileDebugLibraryResources (module unchanged)
✅ Compiling: :common:user:compileDebugLibraryResources
✅ Compiling: :common:wear:compileDebugLibraryResources
🚫 Disabled: :media-lib-common:compileDebugJavaWithJavac (module unchanged)
🚫 Disabled: :media-lib-container:compileDebugJavaWithJavac (module unchanged)

使用方式

拷贝脚本

将build-optimization.gradle拷贝到项目根目录(与settings.gradle同级)

复制代码
project-root/

├── build-optimization.gradle

├── settings.gradle

├── app/

└── ...

在 Root build.gradle 引入

复制代码
apply from: "build-optimization.gradle"

生成缓存文件说明

文件 作用
app/build/git-state/last_branch.txt 记录上一次Git HEAD
app/build/git-state/last_head.txt 记录上一次Git分支
app/build/last_trim_failed.flag 裁剪失败标记

脚本内配置

groovy 复制代码
ext.OPT_CONFIG = [
        gitDiffArgs:[
                ['diff','--name-only','HEAD~1..HEAD'],//最近一次commit变更
                ['diff','--name-only','--cached'],//暂存区变更
                ['diff','--name-only'] //工作区变更
        ],
        modulePathStopSegment:'src',//从文件路径解析Module时的停止标记
        gitStateDir:'build/git-state',//状态文件存储目录
        //任务裁剪策略
        skipTaskNamePatterns:['lint','test','connectedAndroidTest','kaptTest','kaptAndroidTest'],
        compileTaskPatterns:['compileDebug','kaptDebug','kspDebug'],//只裁剪Debug编译任务
        assemblePrefixes:['assemble'],//促发智能裁剪的任务前缀

        //依赖传递配置
        dependentConfigs :['implementation','api','compileOnly'],
        //编译优化
        kotlinJvmTarget:'1.8',
        kotlinFreeArgs:['-Xjsr305=strict','-progressive'],
        kotlinIncremental:true,
        javaIncremental:true,
        javaFork:true,
        javaMaxMem:'1g',
        testParalleDivisor:2,
        //容错控制
        trimFailFlagFile:'last_trim_failed.flag'
] as Map<String,Object>

对比

以app单模块小改动场景对比

xx1App xx2App xx3App
使用前 1min 18s 17s
使用后 53s 13s 13s
相关推荐
沐怡旸10 小时前
深入解析 Android Performance Analyzer (APA) 底层架构与技术原理
android
李斯维17 小时前
从历史的角度看 Android 软件架构
android·架构·android jetpack
plainGeekDev20 小时前
Activity 间传值 → Navigation 参数
android·java·kotlin
用户416596736935520 小时前
Android WebView 加载 file:// 离线页面调试教程
android·前端
plainGeekDev20 小时前
onActivityResult → ActivityResult API
android·java·kotlin
随遇丿而安1 天前
第10周:Activity 基础功能与生命周期优化
android
alexhilton2 天前
Android车载OS中的Remote Compose
android·kotlin·android jetpack
落魄Android在线炒饭2 天前
Android 自定义HAL开发篇之 HIDL篇——从入门到实战(上)
android
武子康2 天前
调查研究-197 FAISS vs Elasticsearch 全面对比:从向量检索、全文搜索到 RAG 选型指南
人工智能·elasticsearch·agent
plainGeekDev2 天前
广播接收器 → Flow + Lifecycle
android·java·kotlin