Android Gradle 开发与应用 (七) : 实战buildSrc插件优化与命令行插件进阶调试

Android Gradle 开发与应用 (七) : 实战buildSrc插件优化与命令行插件进阶调试
1. buildSrc插件深度优化实战第一次用buildSrc插件时你可能觉得它就是个放在项目根目录的普通文件夹。但当你真正在大型多模块项目中应用时很快就会遇到依赖混乱、版本冲突、代码难以维护的问题。我经历过一个包含30模块的电商项目buildSrc目录膨胀到2000多行代码后每次同步项目都要等3分钟以上。1.1 项目结构优化技巧先看一个典型的反例——把所有插件代码都堆在buildSrc/src/main/java目录下。这种结构在小型项目中勉强能用但当你有十几个自定义Task、多个Plugin实现时找代码就像大海捞针。我推荐的模块化结构是这样的buildSrc ├── build.gradle.kts ├── settings.gradle.kts └── src ├── main │ ├── java │ │ └── com │ │ └── yourcompany │ │ ├── android │ │ │ ├── AndroidManifestPlugin.kt │ │ │ └── ResOptimizeTask.kt │ │ └── common │ │ ├── Dependencies.kt │ │ └── VersionCatalog.kt │ └── resources │ └── META-INF │ └── gradle-plugins │ └── com.yourcompany.android.properties └── test └── java └── com └── yourcompany └── android └── AndroidManifestPluginTest.kt关键改进点按功能领域分包android/common等将版本常量抽离到Dependencies.kt测试代码与主代码保持相同包结构使用Kotlin DSL替代Groovy配置版本集中管理的Dependencies.kt可以这样写object Libs { const val kotlinVersion 1.9.0 const val androidGradleVersion 8.1.0 object AndroidX { const val coreKtx 1.10.1 const val appcompat 1.6.1 } }1.2 依赖管理的正确姿势很多开发者直接在buildSrc/build.gradle里写死依赖版本这会导致两个严重问题与主项目依赖版本冲突无法在buildSrc内部使用版本目录Version Catalog实测有效的解决方案是在gradle.properties定义版本号kotlinVersion1.9.0 androidGradlePluginVersion8.1.0在buildSrc/build.gradle.kts中引用val kotlinVersion by extra(providers.gradleProperty(kotlinVersion)) val androidGradleVersion by extra(providers.gradleProperty(androidGradlePluginVersion)) dependencies { implementation(org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion) implementation(com.android.tools.build:gradle:$androidGradleVersion) }主项目的settings.gradle.kts添加dependencyResolutionManagement { versionCatalogs { create(libs) { from(files(../gradle/libs.versions.toml)) } } }1.3 版本同步的自动化方案在多模块项目中最头疼的就是各个模块的插件版本不一致。我开发过一个自动同步插件核心逻辑是通过Project.afterEvaluate钩子project.afterEvaluate { project.allprojects { subproject - subproject.plugins.withTypeJavaPlugin { subproject.dependencies.constraints { add(implementation, org.jetbrains.kotlin:kotlin-stdlib) { version { strictly(Libs.kotlinVersion) } } } } } }这个插件会自动检查所有子模块的Kotlin版本强制统一为buildSrc中定义的版本。你还可以扩展它来检查Android Gradle插件版本、Java版本等关键配置。2. 命令行插件进阶调试技巧用命令行创建的独立插件虽然灵活但调试起来简直是一场噩梦。我最痛苦的一次经历是调试一个Transform插件每次修改代码后都要经历打包-发布到本地Maven-在主项目引用-运行构建这个循环至少需要2分钟。2.1 本地直连调试方案后来发现Gradle其实提供了更高效的调试方式——插件项目直接作为源码依赖引入。具体操作在主项目的settings.gradle.kts中添加includeBuild(../my-gradle-plugin) // 指向你的插件项目目录在主项目build.gradle.kts中直接引用plugins { id(com.yourcompany.plugin) version 1.0.0 // 版本号随意 }在Android Studio中同时打开两个项目修改插件代码后右键插件项目 - Gradle - publishToMavenLocal主项目立即就能使用最新代码这种方式比传统调试快10倍以上因为省去了上传到远程仓库的时间。我在MacBook Pro上实测代码修改到生效平均只需8秒。2.2 单元测试最佳实践好的Gradle插件必须要有完善的单元测试。分享几个实用技巧使用Gradle TestKit测试插件逻辑class MyPluginTest { Test fun plugin should add tasks() { val project ProjectBuilder.builder().build() project.pluginManager.apply(com.yourcompany.plugin) assertTrue(project.tasks.getByName(customTask) is CustomTask) } }测试Task行为时模拟文件系统Test fun task should process files correctly() { val testDir temporaryFolder.root testDir.resolve(input.txt).writeText(test) val task project.tasks.create(testTask, CustomTask::class.java).apply { inputDir.set(testDir) outputDir.set(testDir.resolve(output)) } task.execute() assertTrue(testDir.resolve(output/input.txt).exists()) }集成测试使用GradleRunnerTest fun should build successfully() { val result GradleRunner.create() .withProjectDir(testProjectDir) .withArguments(build) .withPluginClasspath() .build() assertTrue(result.output.contains(BUILD SUCCESSFUL)) }2.3 集成测试的进阶玩法当你的插件需要与Android插件交互时常规测试方法就不够用了。这时候需要Gradle Tooling API首先在插件项目的build.gradle.kts中添加依赖dependencies { testImplementation(com.android.tools.build:gradle:8.1.0) testImplementation(org.gradle:gradle-tooling-api:8.1.1) }编写集成测试Test fun should work with android plugin() { val connector GradleConnector.newConnector() .forProjectDirectory(androidTestProjectDir) .connect() try { val build connector.newBuild() .forTasks(assembleDebug) .setStandardOutput(System.out) .run() assertEquals(BuildResult.SUCCESS, build.result) } finally { connector.close() } }这个测试会真实启动一个Gradle守护进程完全模拟实际构建环境。我在测试一个代码混淆插件时用这种方法发现了3个在单元测试中无法复现的边界条件问题。3. 性能优化实战案例曾经优化过一个编译耗时从12分钟降到2分钟的案例关键点在于buildSrc插件的性能调优。3.1 增量构建配置很多自定义Task没有正确实现增量构建导致每次都要全量执行。正确的做法CacheableTask abstract class OptimizeTask : DefaultTask() { get:InputFiles get:PathSensitive(PathSensitivity.RELATIVE) abstract val inputFiles: ConfigurableFileCollection get:OutputDirectory abstract val outputDir: DirectoryProperty TaskAction fun execute() { // 只有变化的输入文件才会触发处理 } }关键注解CacheableTask启用任务缓存InputFiles标记输入文件OutputDirectory标记输出目录PathSensitive控制路径变化检测策略3.2 并行执行优化默认情况下Gradle会顺序执行任务通过以下配置可以启用并行tasks.withTypeJavaCompile().configureEach { options.isIncremental true inputs.property(java.version, JavaVersion.current()) doFirst { logger.lifecycle(Compiling with ${options.forkOptions.javaHome}) } }在gradle.properties中添加org.gradle.paralleltrue org.gradle.cachingtrue org.gradle.daemontrue3.3 依赖分析工具使用Gradle Build Scan可以直观看到构建耗时./gradlew build --scan这个命令会生成详细的构建报告显示各任务执行时间依赖下载耗时配置阶段瓶颈并行化效率我在一个项目中通过分析发现30%的时间花在了不必要的配置解析上通过延迟配置优化后整体构建时间减少了28%。4. 疑难问题解决方案4.1 类加载冲突处理当插件依赖的库版本与主项目冲突时常见的报错是NoSuchMethodError或ClassNotFoundException。解决方案在插件build.gradle.kts中重定位冲突包shadowJar { relocate(com.google.gson, com.yourcompany.shadow.gson) }使用gradle-dependency-analyze插件检测冲突plugins { id(ca.cutterslade.analyze) version 1.9.1 }在settings.gradle.kts中强制统一版本dependencyResolutionManagement { resolutionStrategy { force(com.google.code.gson:gson:2.10.1) } }4.2 跨版本兼容技巧为了让插件同时支持不同Gradle版本可以使用条件逻辑val isGradle7Plus project.gradle.gradleVersion 7.0 tasks.register(customTask) { if (isGradle7Plus) { dependsOn(:app:assembleDebug) } else { dependsOn(:app:packageDebug) } }对于Android插件兼容val androidExtension project.extensions.findByType(ApplicationExtension::class) ?: project.extensions.findByType(LibraryExtension::class) ?: throw GradleException(Android plugin not applied)4.3 调试日志增强常规的println在复杂插件中根本不够用建议使用logger.debug(Detailed debug info) logger.lifecycle(Visible build progress) logger.quiet(Important warnings) logger.error(Critical failures)配合gradle.properties配置日志级别org.gradle.logging.leveldebug对于耗时操作可以添加性能日志fun T logTime(tag: String, block: () - T): T { val start System.currentTimeMillis() try { return block() } finally { logger.lifecycle($tag took ${System.currentTimeMillis() - start}ms) } }