文章目录
基于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 |