PAYATHON 2026

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 内部竞态”仍是假设,需要验证。

备注:内容仅供参考。