You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose Canvas空指针异常:drawForPersistence调用空对象引用

Compose Canvas 空指针崩溃排查与修复

问题背景

  • 生产环境出现Crashlytics上报的空指针异常,开发阶段无法复现,无完全匹配的公开案例
  • 触发场景:用户将位置权限从始终允许降级为仅在使用中允许时(系统会杀死应用进程)
  • 已排除数据问题:所有从ViewModel获取的数据均设置默认值,空指针与业务数据无关

崩溃日志

Fatal Exception: java.lang.NullPointerException: Attempt to invoke virtual method 'void androidx.compose.ui.graphics.layer.GraphicsLayer.drawForPersistence$ui_graphics_release(androidx.compose.ui.graphics.Canvas)' on a null object reference

at androidx.compose.ui.graphics.layer.LayerManager.persistLayers(LayerManager.android.kt:116)
at androidx.compose.ui.graphics.layer.LayerManager.updateLayerPersistence(LayerManager.android.kt:136)
at androidx.compose.ui.graphics.AndroidGraphicsContext$componentCallback$1$onTrimMemory$1.onPreDraw(AndroidGraphicsContext.android.kt:79)
at android.view.ViewTreeObserver.dispatchOnPreDraw(ViewTreeObserver.java:1204)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:4739)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:3302)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:11362)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1689)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1698)
at android.view.Choreographer.doCallbacks(Choreographer.java:1153)
at android.view.Choreographer.doFrame(Choreographer.java:1079)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1646)
at android.os.Handler.handleCallback(Handler.java:958)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:230)
at android.os.Looper.loop(Looper.java:319)
at android.app.ActivityThread.main(ActivityThread.java:8919)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:578)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1103)

涉事Composable代码

@Composable
private fun DrawArc(
    progress: ArcScoreConfig,
    progressColor: Color,
    strokeWidth: Dp,
    endCircleColor: Color = DesignTokenColor.colorSemanticDatavizIndicator,
    circularEnd: Boolean = false,
    animate: Boolean = true,
) {
    val target: MutableFloatState = rememberSaveable(progress.score) { mutableFloatStateOf(0f) }
    val animation: Float by animateFloatAsState(
        targetValue = target.floatValue,
        animationSpec = tween(
            durationMillis = DiscountArcProgressConfig.animationDuration,
            delayMillis = DiscountArcProgressConfig.animationDelay,
            easing = LinearEasing
        ),
        label = "Arc animation"
    )

    LaunchedEffect(progress.score) {
        target.floatValue = progress.score.toFloat()
    }
    Column {
        Canvas(
            modifier = Modifier.aspectRatio(DiscountArcProgressConfig.aspectRatio)
        ) {
            val centerX = size.width / 2
            val centerY = size.height
            val path = Path().apply {
                arcTo(
                    rect = Rect(
                        centerX - centerY,
                        centerY - centerY,
                        centerX + centerY,
                        centerY + centerY
                    ),
                    startAngleDegrees = DiscountArcProgressConfig.startAngle,
                    sweepAngleDegrees = DiscountArcProgressConfig.sweepAngle(
                        if (animate)
                            animation else progress.score.toFloat()
                    ),
                    forceMoveTo = false
                )
            }
            drawPath(
                path = path,
                color = progressColor,
                style = Stroke(
                    width = strokeWidth.toPx(),
                    cap = StrokeCap.Round
                )
            )
            if (circularEnd) {
                drawCircle(
                    color = endCircleColor,
                    radius = strokeWidth.toPx() / 2,
                    center = DiscountArcProgressConfig.circleOffset(
                        centerX,
                        centerY,
                        animation
                    )
                )
            }
        }
    }
}

问题分析

崩溃根源是Compose的LayerManager在处理图层持久化时,GraphicsLayer实例为null。结合触发场景(系统杀死应用后重启),推测是:

  1. rememberSaveable持久化的动画状态(target)在应用重启后恢复,但Canvas的图层实例未正确重建,导致状态与图层不同步
  2. 系统内存回收/进程重启时,Compose的AndroidGraphicsContext图层管理逻辑出现异常,未正确初始化GraphicsLayer

修复方案

方案1:清理组件销毁时的动画状态

添加DisposableEffect,在Compose组件销毁时重置动画状态,避免重启后残留状态导致图层异常:

@Composable
private fun DrawArc(/* 参数不变 */) {
    val target: MutableFloatState = rememberSaveable(progress.score) { mutableFloatStateOf(0f) }
    // ... 原有动画代码

    // 添加这部分代码
    DisposableEffect(Unit) {
        onDispose {
            target.floatValue = 0f
        }
    }

    // ... 原有Canvas代码
}

方案2:不持久化动画状态

如果不需要跨进程保留动画进度,将rememberSaveable改为remember,避免状态持久化带来的不一致:

val target: MutableFloatState = remember(progress.score) { mutableFloatStateOf(0f) }

方案3:强制Canvas随状态重建

给Canvas的Modifier添加key,当分数变化时强制重建Canvas,确保图层与状态同步:

Canvas(
    modifier = Modifier
        .aspectRatio(DiscountArcProgressConfig.aspectRatio)
        .key(progress.score) // 添加key
) {
    // ... 原有Canvas代码
}

方案4:升级Compose UI版本

该问题可能是Compose的已知底层bug,升级到最新稳定版(如1.6.x及以上)可修复部分图层管理相关的空指针问题。

内容的提问来源于stack exchange,提问作者Semir Suljević

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 11:44:56