2. 失败日志分类:常见回归失败日志的类型与特点
回归失败的日志,说白了就像医生的诊断报告。你拿到手第一反应不是慌,而是得先知道“这是哪类病”。我个人习惯把日志先分个类,不同类别对应不同的调试思路。今天咱们就聊聊最常见的几类,以及它们长什么样。
2.1 编译失败:大多数问题的源头
编译失败往往是最直白的。Gradle 构建跑着跑着就红了,甚至 IDE 直接给你弹个错误框。我个人习惯先看 Build Output 标签页的关键词。
- Gradle 同步失败:通常报
Could not resolve dependency或Plugin not found。这类问题多半是版本号写错了,或者某个仓库地址失效了。 - Kotlin 版本冲突:比如
Module was compiled with an incompatible version of Kotlin。这钟情况常发生在引入第三方库后,kotlin stdlib 版本不一致。 - Compose 编译器版本不匹配:报
androidx.compose.compiler requires kotlin version X。记住,Compose 编译器版本必须和 kotlin 严格对齐。
我的经验: 有一次项目升级 AGP 到 8.0,结果一堆 Compose 库直接罢工。后来发现是 compose-compiler 版本没跟着提。你可以用
./gradlew dependencies 查看依赖树,很快就能定位冲突。2.2 测试失败:UI 层最头疼的部分
测试失败日志,说白了就是预期和实际结果对不上。在 Compose 项目里,大部分失败都出现在 UI 测试上。你想想看,一次简单的文案改动,都有可能让已有测试截图比对失败。
- 单元测试断言失败:例如
expected: "提交" but was: "保存"。这种最好办,通常是文案改了但没更新测试。 - UI 截图比对失败:报
ScreenshotTest: image mismatch > 5%。我在项目中遇到过多次——明明只是改了按钮圆角半径,结果整个测试区域都红了。后来我用papaya工具本地比对 reference 和 actual 图,才发现是渲染精度问题。 - Compose 语义树不匹配:报
SemanticsMatcher could not find node with tag。这通常是因为你在Modifier.semantics里改动了 key,或者重组导致节点消失。
避坑指南: 我曾经把
contentDescription 直接塞在 Modifier.testTag 之前,导致测试用 onNodeWithTag 始终找不到。建议统一用 testTag 或者 semantics 作为测试锚点,别混用。2.3 性能异常:不崩但慢
性能问题在日志里往往不显眼,它不会把管道标红,而是藏在一堆“warning”里。但你看多了就知道哪里不对劲。
| 指标 | 典型日志特征 | 可能原因 |
|---|---|---|
| 帧率 | Choreographer: Skipped 47 frames! | 主线程耗时操作、重组范围过大 |
| 内存 | GC_FOR_ALLOC freed 8MB 伴随频繁触发 | CompositionLocal 泄露、LaunchedEffect 未取消 |
| 启动速度 | ActivityTaskManager: Displayed +3s | 过度依赖缓存或冷启动渲染过多 |
其实很多性能问题不是因为代码逻辑错了,而是你的 Compose 状态管理没做好。比如把 mutableStateListOf 写成了 mutableStateOf(listOf(...)),导致整个列表不可观察。
2.4 UI 差异:眼睛对比和机器对比的鸿沟
这类日志通常出现在跨设备、跨语言或不同主题切换的时候。举个例子:
java.lang.AssertionError:
Expected: Canvas snapshot matches baseline (delta 0.02)
Actual: Render result differs at region (120,340)-(160,380)
Diff pixels: 256 (0.3% of total)
为什么会出现这种差异?往往是字形渲染曲线在低于 600dp 宽度设备上发生了折行。记住,截图比对的容忍度要结合具体机型调整。项目里我曾经为了一个像素的阴影偏差折腾了一下午,后来发现是模拟器 GPU 渲染模式不同。
2.5 运行时崩溃:最痛但也最好定位
运行时崩溃的日志通常有完整堆栈。我最怕的不是它崩,而是它在特定条件下才崩——比如按返回键、横竖屏切换。
- NullPointerException:常出现在
State<T?>没有初始化就读取,或者LazyColumn中 item 被移出后仍持有引用。 - CompositionLocal 丢失:报
java.lang.IllegalStateException: Reading a CompositionLocal that was not provided。这基本是跨 Composable 传递时没有给对应的CompositionLocalProvider。 - 重组期间 IllegalStateException:例如
Modifier should be used only inside a Composable function。这种往往是把 Modifier 定义在了 lambda 外部。
小技巧:运行时崩溃我会第一时间看堆栈里的
com.example 包名,并且关注 Cause by 后面的几行。90% 的崩溃原因都在那个 Caused by 里。好了,这五类日志你心里有个谱就行。下一轮拿到回归结果,先看日志属于哪一类,然后直接匹配对应的调试思路。嗯,这样效率会高很多。