ComposePausableCompositionException与 IllegalStateException:在快速滚动带Coil AsyncImage的 LazyColumn时,对已停用的节点调用了Apply
大家好,
我在Jetpack Compose项目中使用 Coil (AsyncImage) 时,在一个 LazyColumn 里面遇到了持续崩溃的问题。数据是通过 Flow 从远程服务观察到的。
崩溃特别发生在 高速滚动(压力测试)过程中。经过多次尝试,我将问题定位并确认只有在 AsyncImage 存在时才会发生崩溃。将其替换为常规的 Image 就能解决问题,这指向Coil内部度量逻辑中的一个竞态条件。
Environment:
- Compose BOM:
2026.02.01 - Coil Version:
2.2.2 - Kotlin Version:
2.0.21 - Compiler:
Kotlin 2.0 Compose Compiler
代码:
@Composable
fun CountryCardItem(
country: CountryItem,
onCountryClick: (String) -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = Dimens.dp16, vertical = Dimens.dp6)
.clip(shape = RoundedCornerShape(Dimens.dp12))
.clickable { onCountryClick(country.name.toString()) },
shape = RoundedCornerShape(Dimens.dp12),
elevation = CardDefaults.cardElevation(defaultElevation = Dimens.dp2),
colors = CardDefaults.cardColors(containerColor = Color.White)
) {
Row(
modifier = Modifier
.padding(Dimens.dp12)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(Dimens.dp50)
.shadow(
elevation = Dimens.dp2,
shape =
if (isRadius)
RoundedCornerShape(Dimens.dp16)
else
CircleShape
)
.clip(
if (isRadius)
RoundedCornerShape(Dimens.dp16)
else
CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant)
) {
AsyncImage(
model = imageUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
Spacer(modifier = Modifier.width(Dimens.dp16))
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = country.name ?: "Unknown",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
)
Text(
text = country.capital ?: "No Capital",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.secondary,
)
}
Icon(
painter = painterResource(R.drawable.ic_arrow_right),
contentDescription = "Go to details",
tint = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier.size(Dimens.dp24)
)
}
}
}
Full Exception Code :
androidx.compose.runtime.ComposePausableCompositionException: Failed to execute op number 24
0: down LayoutNode@ec918ec children: 1 measurePolicy: BoxMeasurePolicy(alignment=BiasAlignment(horizontalBias=-1.0, verticalBias=-1.0), propagateMinConstraints=true) deactivated: false
1: reuse LayoutNode@ec918ec children: 1 measurePolicy: BoxMeasurePolicy(alignment=BiasAlignment(horizontalBias=-1.0, verticalBias=-1.0), propagateMinConstraints=true) deactivated: false
2: apply Function2<androidx.compose.ui.node.ComposeUiNode, androidx.compose.ui.layout.MeasurePolicy, kotlin.Unit>
3: apply Function2<androidx.compose.ui.node.ComposeUiNode, androidx.compose.runtime.CompositionLocalMap, kotlin.Unit>
4: apply Function2<androidx.compose.ui.node.ComposeUiNode, java.lang.Integer, kotlin.Unit>
5: apply Function2<androidx.compose.ui.node.ComposeUiNode, androidx.compose.ui.Modifier, kotlin.Unit>
6: down LayoutNode@aad474a children: 1 measurePolicy: ColumnMeasurePolicy(verticalArrangement=Arrangement#Top, horizontalAlignment=Horizontal(bias=-1.0)) deactivated: false
7: reuse LayoutNode@aad474a children: 1 measurePolicy: ColumnMeasurePolicy(verticalArrangement=Arrangement#Top, horizontalAlignment=Horizontal(bias=-1.0)) deactivated: false
8: apply Function2<androidx.compose.ui.node.ComposeUiNode, androidx.compose.ui.layout.MeasurePolicy, kotlin.Unit>
9: apply Function2<androidx.compose.ui.node.ComposeUiNode, androidx.compose.runtime.CompositionLocalMap, kotlin.Unit>
10: apply Function2<androidx.compose.ui.node.ComposeUiNode, java.lang.Integer, kotlin.Unit>
11: apply Function2<androidx.compose.ui.node.ComposeUiNode, androidx.compose.ui.Modifier, kotlin.Unit>
12: down LayoutNode@4bc16bb children: 4 measurePolicy: RowMeasurePolicy(horizontalArrangement=Arrangement#Start, verticalAlignment=Vertical(bias=0.0)) deactivated: false
13: reuse LayoutNode@4bc16bb children: 4 measurePolicy: RowMeasurePolicy(horizontalArrangement=Arrangement#Start, verticalAlignment=Vertical(bias=0.0)) deactivated: false
14: apply Function2<androidx.compose.ui.node.ComposeUiNode, androidx.compose.ui.layout.MeasurePolicy, kotlin.Unit>
15: apply Function2<androidx.compose.ui.node.ComposeUiNode, androidx.compose.runtime.CompositionLocalMap, kotlin.Unit>
16: apply androidx.compose.runtime.Updater$$ExternalSyntheticLambda1@22aca1
17: apply Function2<androidx.compose.ui.node.ComposeUiNode, androidx.compose.ui.Modifier, kotlin.Unit>
18: down LayoutNode@b99b0c0 children: 0 measurePolicy: coil.compose.AsyncImageKt$Content$1@4e2bec6 deactivated: false
19: reuse LayoutNode@b99b0c0 children: 0 measurePolicy: coil.compose.AsyncImageKt$Content$1@4e2bec6 deactivated: false
20: apply Function2<androidx.compose.ui.node.ComposeUiNode, androidx.compose.ui.Modifier, kotlin.Unit> 21: up
22: down LayoutNode@2a4d7d8 children: 0 measurePolicy: androidx.compose.foundation.layout.SpacerMeasurePolicy@b74cc8f deactivated: true 23: apply Function2<androidx.compose.ui.node.ComposeUiNode, androidx.compose.runtime.CompositionLocalMap, kotlin.Unit> 24: apply androidx.compose.runtime.Updater$$ExternalSyntheticLambda1@dcfbe87
25: up
26: down LayoutNode@a008e16 children: 2 measurePolicy: ColumnMeasurePolicy(verticalArrangement=Arrangement#Top, horizontalAlignment=Horizontal(bias=-1.0)) deactivated: true
27: apply Function2<androidx.compose.ui.node.ComposeUiNode, androidx.compose.runtime.CompositionLocalMap, kotlin.Unit>
28: apply androidx.compose.runtime.Updater$$ExternalSyntheticLambda1@eb71b4
29: down LayoutNode@8caf1f9 children: 0 measurePolicy: androidx.compose.foundation.text.EmptyMeasurePolicy@61c71dd deactivated: true 30: apply Function2<androidx.compose.ui.node.ComposeUiNode, androidx.compose.runtime.CompositionLocalMap, kotlin.Unit> 31: apply androidx.compose.runtime.Updater$$ExternalSyntheticLambda1@2bc0152 32: apply Function2<androidx.compose.ui.node.ComposeUiNode, androidx.compose.ui.Modifier, kotlin.Unit>
.....
Caused by: java.lang.IllegalStateException: Apply is called on deactivated node LayoutNode@a5829de children: 0 measurePolicy: androidx.compose.foundation.layout.SpacerMeasurePolicy@fabdd40 deactivated: true at androidx.compose.ui.node.ComposeUiNode$Companion$ApplyOnDeactivatedNodeAssertion$1.invoke(ComposeUiNode.kt:54)
解决方案
解决方案:
Hello everyone,
其实,这里的根本问题在于快速滚动时,LazyColumn会停用屏幕上不可见的项,我发现造成这一点的是AsyncImage内的 ConstraintsSizeResolver() 类中的 \_constraint 变量(private val _constraints = MutableStateFlow(ZeroConstraints))。原因在于:
AsyncImage尝试把值分配给LazyColumn已经停用的元素。因此,就会出现这个错误。为了解决这个问题,你可以尝试以下方法:
- 可以使用Image而不是AsyncImage。我在测试中发现它没有报错。
- 如果你想使用AsyncImage,它必须有一个尺寸值。由于这是一个静态值,上文提到的ConstraintsSizeResolver中的
\_constraint不需要重新计算,因为它试图分配一个常量值。因此,
AsyncImage(
model = ImageRequest.Builder(context)
.data(country.flag?.png)
.crossfade(true)
.diskCacheKey(country.name)
.size(200)
.memoryCacheKey(country.name)
.build(),
)
我们可以通过.size将图像的大小设为固定值。
这样做时,AsyncImage的 updateRequest() 方法会直接使用我们提供的ImageRequest对象,根本不会进入if条件。因此,也就不会创建ConstraintsSizeResolver类。
- 如果把AsyncImage的 ContentScale状态设为None,当updateRequest() 方法内部的另一个if条件被执行时,它会自动返回一个Size值(SizeResolver(CoilSize.ORIGINAL))。因此,不再需要额外的补丁。
我把updateRequest方法附上,供你们审阅:
@Composable
internal fun updateRequest(request: ImageRequest, contentScale: ContentScale): ImageRequest {
return if (request.defined.sizeResolver == null) {
val sizeResolver = if (contentScale == ContentScale.None) {
SizeResolver(CoilSize.ORIGINAL)
} else {
remember { ConstraintsSizeResolver() }
}
request.newBuilder().size(sizeResolver).build()
} else {
request
}
}
我再附上额外的ConstraintsSizeResolver类,供你们审阅:
internal class ConstraintsSizeResolver : SizeResolver, LayoutModifier {
private val _constraints = MutableStateFlow(ZeroConstraints)
override suspend fun size() = _constraints.mapNotNull(Constraints::toSizeOrNull).first()
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
_constraints.value = constraints
val placeable = measurable.measure(constraints)
return layout(placeable.width, placeable.height) { placeable.place(0, 0) }
}
}
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。