LazyColumn 快速滚动时 AsyncImage 触发节点停用异常,如何排查?
结论
现有日志说明,Compose 在应用组合变更时尝试更新一个已经停用的 LayoutNode。AsyncImage 是当前复现条件中的重要因素,但这些信息还不足以确认 Coil 内部测量逻辑存在竞态问题,也不能据此指定某个版本作为确定的修复方案。
建议先核对实际解析的依赖版本,再用固定列表、稳定 key 和不同图片加载方式做对照测试,以区分依赖组合、列表更新,以及图片加载与节点复用之间的交互问题。
日志能说明什么
外层异常为:
ComposePausableCompositionException: Failed to execute op number 24
更直接的失败原因是:
IllegalStateException: Apply is called on deactivated node
操作记录中,AsyncImage 对应的节点显示 deactivated: false,后面的 Spacer 节点则显示 deactivated: true,随后发生了 apply 操作。底层异常指向的 measurePolicy 是 SpacerMeasurePolicy。
应检查组合变更与节点停用、复用之间的关系。日志不足以说明 Spacer 写错了,也不能仅凭 coil.compose.AsyncImageKt$Content$1 出现在日志中,就断定根因在 Coil。
将 AsyncImage 换成普通 Image 后不再复现,是一项有价值的对照结果。不过,这种替换通常也会改变异步请求、加载状态更新和布局行为,还需进一步缩小差异。
1. 检查实际依赖解析结果
先查看应用最终使用的 Compose 和 Coil 版本。不能只依据声明的 Compose BOM 判断运行环境。
以模块名为 app、配置名为 debugRuntimeClasspath 的项目为例:
./gradlew :app:dependencyInsight --configuration debugRuntimeClasspath --dependency androidx.compose.runtime
./gradlew :app:dependencyInsight --configuration debugRuntimeClasspath --dependency androidx.compose.ui
./gradlew :app:dependencyInsight --configuration debugRuntimeClasspath --dependency androidx.compose.foundation
./gradlew :app:dependencyInsight --configuration debugRuntimeClasspath --dependency coil-compose
如果项目使用其他模块名或 product flavor,请替换相应名称。检查是否存在显式版本覆盖、传递依赖如何影响版本选择,以及相关 Coil 模块是否按预期解析。同时保留 Kotlin 和 Compose Compiler 插件的实际配置。
所提供的材料没有依赖树或完整构建配置,无法确认这组版本是否存在已知问题。验证版本调整时,每轮只改一组相关依赖,再用相同的滚动操作复测。否则,同时升级多个组件后会难以定位问题。
2. 隔离 Flow 数据更新和列表身份
原代码只展示了列表项,未展示 LazyColumn 的 items、key 或 Flow 收集方式,需要补查。
先将远程 Flow 暂时替换为一份固定列表,保留 AsyncImage 并重复高速滚动:
- 固定列表仍崩溃:继续检查图片加载、布局及依赖组合。
- 仅持续更新的列表崩溃:优先检查数据替换、排序、增删和条目身份。
为每条数据提供稳定且唯一的 key。下面的 countryKey 需要由业务实现,原始数据中没有给出这个字段:
LazyColumn {
items(
items = countries,
key = { country -> countryKey(country) }
) { country ->
CountryCardItem(
country = country,
onCountryClick = onCountryClick
)
}
}
countryKey(country) 应在同一国家的数据更新后保持不变,且在列表内唯一。列表存在插入或重排时,不要用位置作为业务身份,也不要使用随机值。只有业务明确保证 country.name 唯一且稳定时,才适合用它作为 key。
稳定 key 有助于 Compose 识别同一条数据,但不能保证修复框架内部的节点状态错误。
还要检查 Flow 发出的列表及条目是否被其他代码继续原地修改,以及每次发射是否确实对应预期的数据变化。
3. 用最小布局复现
现有代码已通过外层 Box 的 size(Dimens.dp50) 为图片区域设置固定尺寸,不能直接认定问题由图片缺少尺寸约束引起。
可以临时去掉 Card、shadow、clip、图标和次要文字,只保留固定尺寸的图片和一行文字,然后逐个恢复,判断哪些布局或状态变化是复现所必需的。
片段中没有给出 imageUrl 和 isRadius 的来源。需要确认它们与当前条目对应,尤其要检查列表重排或复用后,图片 URL 是否仍属于同一条数据。
4. 验证另一条图片显示路径
如果普通 Image 的对照测试使用本地静态资源,那么异步加载因素也被移除了。可以再用 rememberAsyncImagePainter 配合 Image,通过 Coil 加载同一 URL,观察是否复现。
针对当前 Coil 2 的包名,可以使用以下诊断代码:
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
@Composable
fun DiagnosticCountryImage(imageUrl: String?) {
val painter = rememberAsyncImagePainter(model = imageUrl)
Image(
painter = painter,
contentDescription = null,
modifier = Modifier.size(50.dp),
contentScale = ContentScale.Crop
)
}
这段代码用于对照,不能视为已确认有效的修复。它改变了图片显示组件的实现路径,图片请求的尺寸策略也需要单独评估。若验证后用于正式列表,还应检查图片内存占用、加载失败表现和显示尺寸。
测试时分别记录固定列表与 Flow 列表、冷缓存与热缓存、AsyncImage 与替代实现是否触发异常。一次未复现只能说明该轮测试通过,不能证明竞态问题已经消失。
临时处理与进一步定位
如果需要先恢复业务可用性,可以暂时使用已在相同压力条件下验证过的图片显示方案,或显示本地占位图。不要捕获并忽略这个 IllegalStateException 后继续运行。异常发生在 Compose 应用内部变更的过程中,吞掉异常无法证明界面状态仍然正确。
若在固定数据、稳定 key 和最小布局下仍能复现,应整理完整异常链、实际依赖树、最小项目和滚动复现步骤,再判断由 Compose 或 Coil 哪一侧继续跟进。提交问题时可以说明“替换 AsyncImage 后未复现”,但“Coil 内部竞态”仍是假设,需要验证。
备注:内容仅供参考。
