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
相关推荐
赏金术士2 小时前
第二章:Compose入门—声明式UI编程
android·ui·kotlin·compose
星间都市山脉2 小时前
Android 谷歌 VTS 完整测试
android
齊家治國平天下2 小时前
Android 14 AIDL HAL 使用指南-获取服务流程解析
android·hal·aidl·servicemanager·aidl hal·获取服务
Huazzi.3 小时前
Git本地和远程历史不一致问题解决步骤
大数据·git·elasticsearch
张二娃同学3 小时前
02_C语言数据类型_整型浮点型字符型一次讲清楚
android·java·c语言
lf2824814313 小时前
07 AD9361自发自收PL工程搭建
android
RemainderTime3 小时前
(十二)Spring Cloud Alibaba 2023.x:基于 Filebeat 构建轻量级 ELK日志追踪体系
分布式·elk·elasticsearch·微服务·架构·logback
什么都会一点儿的自动驾驶工程狮3 小时前
Jetson Orin Nano Super + Ubuntu 22.04 + ROS2 Humble + Autoware Universe
linux·ubuntu·elasticsearch
我命由我123453 小时前
Android 开发:Unable to start service Intent { ... } U=0: not found
android·开发语言·android studio·android jetpack·android-studio·android runtime