JaCoCo 0.8.7适配Android API 32编译时无覆盖率结果问题
问题排查与解决
核心原因
compileSdkVersion 升级到32时,配套要求的Android Gradle Plugin(AGP)最低版本为7.2,该版本AGP默认将编译输出字节码版本升级至Java 11,同时调整了编译产物的输出路径;旧多模块JaCoCo聚合脚本普遍存在三个兼容问题:一是使用的JaCoCo版本低于0.8.8,无法解析Java 11字节码,不能正确识别插桩后的类文件与测试执行数据;二是脚本硬编码了旧版AGP的类文件输出路径,升级后扫描不到有效编译产物;三是默认聚合任务不会校验子模块是否产出有效覆盖率数据,只要任务链路执行完成就会返回成功状态,最终出现无任何统计结果的现象。
排查步骤
- 校验JaCoCo版本:查看根目录构建脚本中配置的JaCoCo版本,确认是否低于0.8.8,低版本JaCoCo完全不支持AGP 7.2+与Java 11字节码解析。
- 检查子模块执行数据:逐个进入子模块的
build/jacoco/目录,确认测试任务执行后是否生成对应变体的.exec文件,若文件大小仅为几KB,说明插桩阶段未采集到任何有效执行数据。 - 校验路径配置:检查聚合脚本中配置的类文件、源码文件扫描路径,确认是否硬编码了旧版AGP的
intermediates/classes/路径,该路径在AGP 7.2+中已被调整为jar包输出格式,旧路径无法扫描到有效类文件。 - 开启详细日志定位具体报错:执行覆盖率任务时添加
--info参数,过滤日志中JaCoCo关键字,默认日志等级下被隐藏的无法读取执行数据、类文件缺失调试信息等警告会被打印,可直接定位具体失败点。
修复方案
- 升级JaCoCo到兼容版本:在根目录build.gradle中固定JaCoCo版本为0.8.8及以上,不要使用AGP内置的JaCoCo版本避免版本不一致,配置参考:
buildscript { dependencies { classpath "org.jacoco:org.jacoco.core:0.8.11" } } // 所有模块应用JaCoCo时强制指定版本 jacoco { toolVersion = "0.8.11" }
- 重构聚合脚本的路径扫描逻辑,摒弃硬编码路径,通过AGP官方提供的Variant API获取各模块编译产物、源码、执行数据路径,适配不同AGP与compileSdk版本的路径变更,核心配置参考:
def coverageSourceDirs = [] def coverageClassCollection = files() def coverageExecCollection = files() subprojects { // 处理应用模块 plugins.withId('com.android.application') { android.applicationVariants.all { variant -> if (variant.name == "debug") { // 收集Java、Kotlin源码路径 variant.sourceSets.each { sourceSet -> coverageSourceDirs.addAll(sourceSet.java.srcDirs) coverageSourceDirs.addAll(sourceSet.kotlin.srcDirs) } // 通过AGP工件API获取编译后所有类文件(含jar格式产物) def classArtifact = variant.artifacts.get(com.android.build.api.artifact.SingleArtifact.ALL_CLASSES_JARS) coverageClassCollection.from(classArtifact) // 收集所有exec执行数据 coverageExecCollection.from(fileTree(dir: buildDir, includes: ["**/jacoco/*.exec"])) } } } // 处理库模块 plugins.withId('com.android.library') { android.libraryVariants.all { variant -> if (variant.name == "debug") { variant.sourceSets.each { sourceSet -> coverageSourceDirs.addAll(sourceSet.java.srcDirs) coverageSourceDirs.addAll(sourceSet.kotlin.srcDirs) } def classArtifact = variant.artifacts.get(com.android.build.api.artifact.SingleArtifact.ALL_CLASSES_JARS) coverageClassCollection.from(classArtifact) coverageExecCollection.from(fileTree(dir: buildDir, includes: ["**/jacoco/*.exec"])) } } } } // 注册聚合报告任务 tasks.register("jacocoMergeDebugReport", JacocoReport) { dependsOn coverageExecCollection.builtBy sourceDirectories.setFrom(files(coverageSourceDirs)) classDirectories.setFrom(coverageClassCollection) executionData.setFrom(coverageExecCollection) // 排除无需统计的自动生成类、R类、BuildConfig类 classDirectories.setFrom(classDirectories.files.collect { fileTree(dir: it, excludes: [ '**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*' ]) }) reports { html.required.set(true) xml.required.set(true) csv.required.set(false) } }
- 配置测试任务兼容参数,在各模块的android配置块中添加单元测试配置,解决JaCoCo插桩与JDK内置类、Android测试框架的字节码冲突:
android { testOptions { unitTests.all { jvmArgs "-noverify" jacoco.includeNoLocationClasses = true jacoco.excludes = ['jdk.internal.*'] } } }
- 清理缓存后重新执行任务:配置修改完成后先执行
./gradlew clean清空所有旧构建产物,再执行聚合报告任务,避免旧缓存的无效类文件、exec数据干扰结果生成。
内容的提问来源于stack exchange,提问作者Ralf Wickum
相关产品推荐
相关产品推荐

