2. 失败日志分类:常见回归失败日志的类型与特点

回归失败的日志,说白了就像医生的诊断报告。你拿到手第一反应不是慌,而是得先知道“这是哪类病”。我个人习惯把日志先分个类,不同类别对应不同的调试思路。今天咱们就聊聊最常见的几类,以及它们长什么样。

回归失败日志分类图谱 回归失败日志分类图谱 回归失败日志 编译失败 测试失败 性能异常 UI 差异 运行时崩溃 Gradle 同步失败 Kotlin 版本冲突 单元测试断言失败 UI 测试截图比对不匹配 帧率骤降 内存泄漏/抖动 Compose 布局偏移 主题/颜色不一致 NullPointerException CompositionLocal 丢失 分类核心原则: • 根据日志中第一个异常类型判断大类 • 同一模块多次失败优先看编译 -> 测试 -> 运行的顺序

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 里。

好了,这五类日志你心里有个谱就行。下一轮拿到回归结果,先看日志属于哪一类,然后直接匹配对应的调试思路。嗯,这样效率会高很多。