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

Jetpack Compose Timer周期调用nativeCanvas.drawText崩溃问题

问题描述

需要在Jetpack Compose中周期性调用nativeCanvas.drawText方法绘制文本,运行代码A时抛出结果A所示的异常,需要定位问题并给出正确实现方案。

代码A

@Composable
fun ScreenHome_Watch(

){
    Box(        
    ) {        
        Canvas(             
        ) {    
            startTimer{
                drawIntoCanvas {
                    it.nativeCanvas.drawText("Hello "+Calendar.getInstance().time.toString() ,10f,10f, paintText)
                }
            }
        }
    }
}

private lateinit var timer: Timer

fun startTimer(block: ()->Unit) {
    timer = Timer()
    timer.scheduleAtFixedRate(timerTask {
        block()
    }, 0, 1000)
}

报错结果A

java.lang.ClassCastException: androidx.compose.ui.graphics.drawscope.EmptyCanvas cannot be cast to androidx.compose.ui.graphics.AndroidCanvas
问题原因
  • 核心错误是违反了Compose的绘制规则:Canvas的DrawScope作用域仅在单次绘制流程执行期间持有有效的Android画布实例,绘制流程结束后DrawScope会被回收,替换为EmptyCanvas占位对象。你把drawIntoCanvas绘制逻辑放到Timer的异步定时回调里延迟执行,回调触发时绘制流程早就结束,此时访问的画布是无效的EmptyCanvas,强转AndroidCanvas自然抛出类型转换异常。
  • 实现逻辑不符合Compose设计范式:Compose是状态驱动UI的框架,不支持在异步线程/延迟回调里主动触发绘制操作,所有UI更新都应该通过「修改状态→触发重组/重绘」的链路完成。
  • 存在生命周期泄漏风险:全局Timer没有和Composable的生命周期绑定,组件销毁后定时任务仍会持续执行,既浪费资源也会引发额外异常。
  • 存在线程违规问题:Timer的任务默认运行在独立后台线程,Compose不允许在非UI线程操作UI相关对象。
正确实现方案

核心思路:用可观察状态存储需要绘制的文本内容,定时任务只负责更新状态,状态变化会自动触发Canvas重绘,所有绘制逻辑直接写在Canvas的DrawScope作用域内即可。

import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Canvas
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.layout.matchParentSize
import kotlinx.coroutines.delay
import java.util.Calendar

@Composable
fun ScreenHome_Watch() {
    Box {
        // 定义状态存储待绘制的时间文本
        var showText by remember { mutableStateOf("Hello ${Calendar.getInstance().time}") }
        // 启动生命周期安全的定时任务,仅更新状态
        LaunchedEffect(Unit) {
            while (true) {
                delay(1000)
                showText = "Hello ${Calendar.getInstance().time}"
            }
        }
        // 初始化绘制用的画笔,避免每次重绘重复创建
        val textPaint = remember {
            Paint().apply {
                color = Color.Black
                textSize = 48f
            }
        }
        // 绘制逻辑直接写在Canvas作用域内,重绘时自动拿到有效画布
        Canvas(modifier = Modifier.matchParentSize()) {
            drawIntoCanvas { canvas ->
                canvas.nativeCanvas.drawText(
                    showText,
                    10f,
                    60f, // y坐标偏移避免文字被屏幕顶部截断
                    textPaint.asFrameworkPaint()
                )
            }
        }
    }
}

方案说明

  • 用LaunchedEffect启动协程实现定时,Composable被移出视图树时协程会自动取消,不存在内存泄漏问题。
  • 绘制逻辑始终在DrawScope有效生命周期内执行,不会出现空画布、类型转换异常。
  • 完全遵循Compose状态驱动更新的范式,不需要手动调用重绘方法,状态变化后框架会自动触发最优重绘。
  • 协程默认运行在Compose的UI上下文,不存在线程违规问题。

内容的提问来源于stack exchange,提问作者HelloCW

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.02 08:27:42