ComposePausableCompositionException与 IllegalStateException:在快速滚动带Coil AsyncImage的 LazyColumn时,对已停用的节点调用了Apply

移动开发 2026-07-11

大家好,

我在Jetpack Compose项目中使用 Coil (AsyncImage) 时,在一个 LazyColumn 里面遇到了持续崩溃的问题。数据是通过 Flow 从远程服务观察到的。

崩溃特别发生在 高速滚动(压力测试)过程中。经过多次尝试,我将问题定位并确认只有在 AsyncImage 存在时才会发生崩溃。将其替换为常规的 Image 就能解决问题,这指向Coil内部度量逻辑中的一个竞态条件。

Environment:

  1. Compose BOM: 2026.02.01
  2. Coil Version: 2.2.2
  3. Kotlin Version: 2.0.21
  4. 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已经停用的元素。因此,就会出现这个错误。为了解决这个问题,你可以尝试以下方法:

  1. 可以使用Image而不是AsyncImage。我在测试中发现它没有报错。
  2. 如果你想使用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类。

  1. 如果把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导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章