4. 常见AGP崩溃类型概览
做 Android 构建这么多年,我跟 AGP 的崩溃打了无数次交道。说实话,崩溃的套路其实就那几类。你只要摸清楚它们的脾气,百分之八十的构建失败都能在十分钟内搞定。
我习惯把这四类问题归到一起讲——依赖冲突、插件兼容、资源合并、混淆配置。为什么?因为它们出错的频率最高,而且往往互相牵扯。举个例子,你加了某个三方库,结果资源合并炸了,仔细一看其实是依赖冲突引起了编译器内部状态异常。嗯,这种连环坑我踩过不止一次。
1. 依赖冲突:最常见的「定时炸弹」
依赖冲突说白了就是同一个库出现了多个不同版本。Gradle 虽然会自动帮我们选择最高版本,但这种「自动」有时候会搞出大麻烦。
我记得有一次,项目里同时依赖了 androidx.core:core:1.9.0 和一个内购 sdk,那个 sdk 内部声明了 core:1.6.0。按说应该用 1.9.0,结果 sdk 里用了几个被废弃的 API,运行时直接 ClassNotFoundException。那是我第一次领教「编译通过、运行挂掉」的诡异场景。
检测命令(别跟我一样等到运行时才拍大腿):
// 查看完整依赖树
./gradlew app:dependencies --configuration releaseRuntimeClasspath
// 指定输出到文件方便分析
./gradlew app:dependencies > deps.txt
我的习惯:每次引入新库,先在分支上跑一次 dependencies,用眼睛扫一遍有没有冲突。尤其是那些不遵守语义化版本的国产库,经常偷偷升级 minor 版本号然后又 break API。
2. 插件兼容:新版 AGP 的「老毛病」
AGP 和 Gradle 的版本匹配是门玄学。官方虽然给了兼容性矩阵,但你想想看,一个大型项目可能有十几个 Gradle 插件——Kotlin、Hilt、Room、Protobuf……每个插件又依赖特定的 AGP 版本范围。
我曾经接手过一个项目,AGP 8.2 配合 Gradle 8.5,看上去没问题。但是项目里用了老版本的 kotlin-gradle-plugin(1.8.x),结果构建时报了一个奇怪的 Unsupported class file major version。排查了两小时,最终发现是 Kotlin 编译器版本太低,跟 AGP 8.x 的字节码生成不兼容。
| AGP 版本 | 最低 Gradle 版本 | 推荐 Kotlin 版本 |
|---|---|---|
| 8.0.x | 8.1 | 1.8.10+ |
| 8.1.x | 8.2 | 1.8.20+ |
| 8.2.x | 8.2 | 1.9.0+ |
| 8.3.x | 8.4 | 1.9.10+ |
我曾经踩过的坑:升级 AGP 后不要光改一个版本号就跑。必须同步检查所有插件的最新兼容版本。特别是 Hilt 和 Room 的 annotation processor,它们经常和 AGP 的 JavaCompile 任务有隐藏的版本绑定。
3. 资源合并:aapt2 的「无声崩溃」
资源合并的崩溃往往最让人头疼。因为它报错信息通常很含蓄——比如 resource drawable/xxx not found 或者直接一个 AAPT2 error 弹出。
我遇到过最典型的情况:两个 module 各自定义了一个同名 colors.xml,但里面的属性值不一样。aapt2 在合并时会尝试去重,但如果属性类型不一致(比如一个写 #FFF 另一个写 @color/white),它就直接炸了。
排查步骤(我个人的三板斧):
- 定位模块:看错误日志里提到的文件路径,判断是哪个 library 或 module 的问题。
- 检查命名冲突:用
grep -r "res/.*\.xml" --include="*.gradle"搜一下各个 module 里是否有同名资源。 - 打开合并报告:在
build/outputs/logs/resources-merge-debug-report.txt里能看到详细的合并冲突记录。
一个更隐蔽的坑:有些库会把资源放在 res/values/public.xml 里通过 <public> 标签声明公共资源 ID。当你的主项目也定义了相同 ID 的资源时,aapt2 不会合并而是直接报错。遇到这种情况,我一般优先考虑用 tools:override="true" 或者直接修改 library 源码里的 id。
4. 混淆配置:R8 的「静默杀手」
混淆配置引起的崩溃往往发生在发布包上,debug 包一切正常。这是因为 R8 在生产构建时才会启用完整的全树缩减。
你想想看,一个 app 在测试阶段跑得好好的,发到应用市场后用户反馈「点登录就闪退」。这种问题排查起来极其痛苦。
我记得有一次,某个网络库通过反射调用 OkHttp 的拦截器,proguard 规则里只保留了 OkHttp 类,却没保留那个反射调用的中间类。R8 压缩后那个类直接被移除,导致运行时 ClassNotFoundException。最后我把 -keep class com.example.network.** { *; } 加上才解决。
常见的混淆配置文件:
proguard-rules.pro(主项目规则)consumer-rules.pro(aar 库自带的规则,会被自动消费)--keep系列、-dontwarn、-keepclassmembers等
调试技巧:
// 在 build.gradle 中开启详细日志
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
// 生成 mapping 映射文件
proguardFiles 'mapping.txt'
}
}
// 查看 R8 输出的精简日志
./gradlew app:minifyReleaseWithR8 --info
特别注意:如果你用了 Jetpack Compose、DataBinding 或 Hilt,必须确保 proguard 规则不包括 -dontnote 或 -dontwarn 掉编译器生成的合成类。我曾经因为一个 -dontwarn android.arch.lifecycle.** 直接把 LiveData 的观察者给去掉了,导致整个 LiveData 不触发回调。那是我职业生涯中最丢人的一次线上事故。
用一张表格总结一下四种类型的关键区别
| 崩溃类型 | 报错关键词 | 常见出现时机 | 我个人的第一反应 |
|---|---|---|---|
| 依赖冲突 | Duplicate class, conflict | 编译期 | 跑 dependencies 命令 |
| 插件兼容 | Unsupported, Incompatible | 配置阶段 | 查官方兼容表 |
| 资源合并 | AAPT2 error, resource not found | 资源处理阶段 | 看合并报告 |
| 混淆配置 | ClassNotFoundException, NoClassDefFoundError | 运行时(release 包) | 检查 mapping.txt + 日志 |
好了,这四种 AGP 崩溃类型算是构建问题里的「四大天王」了。说实话,刚接触 AGP 配置时,每次遇到 crash 我都很焦虑。后来整理成这个分类之后,每次报错我都先对号入座——嗯,是依赖冲突还是混淆没配好?一张表格帮我节省了大量瞎猜的时间。