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

在viewModelScope.launch中执行代码致App崩溃,求CameraX计时方案

CameraX录像计时器崩溃问题解决

问题描述

我正尝试为CameraX录像应用实现一个计时器,要求录像时每秒更新TextView。以下是Activity调用的ViewModel中的启动和停止方法:

fun startTimer(timerText: TextView) {
        timer = Timer()
        viewModelScope.launch(Dispatchers.IO) {
            timer?.scheduleAtFixedRate(timerTask {
                timeCount += 1u
                val tempTimerText = (timeCount / 60u).toString().padStart(2, '0') + ":" +
                        (timeCount % 60u).toString().padStart(2, '0')
                timerText.text = tempTimerText
            }, 1000, 1000)
        }

    }

    fun stopTimer(timerText: TextView) {
        if (timer != null) {
            timer?.cancel()
            timer?.purge()
            timerText.text = application.getString(R.string.video_timer)
        }
    }

每次调用该方法应用都会崩溃,我哪里出错了?如何在不阻塞UI和录像的情况下在后台执行计时器并更新TextView?

更新内容

按照建议实现了LiveData观察者,但问题依旧,应用崩溃且Logcat无信息。

ViewModel代码:

fun startTimer(initial: String): MutableLiveData<String> {
        timer = Timer()
        timerCounterText.value = initial
        viewModelScope.launch(Dispatchers.IO) {
            timer?.scheduleAtFixedRate(timerTask {
                timerCounter += 1u
                timerCounterText.value = (timerCounter / 60u).toString().padStart(2, '0') + ":" +
                        (timerCounter % 60u).toString().padStart(2, '0')
            }, 1000, 1000)
        }
        return timerCounterText
    }

Activity订阅代码:

cameraViewModel.startTimer().observe(this) { timeCount -> timerText.text = timeCount }

问题分析与解决方案

原始代码崩溃原因

  1. 后台线程操作UI控件:timerText.text = tempTimerText在timerTask的后台线程执行,Android要求所有UI操作必须在主线程完成,直接触发CalledFromWrongThreadException导致崩溃。
  2. ViewModel持有UI引用:将TextView传入ViewModel违反MVVM架构原则,ViewModel不应持有UI层引用,会引发内存泄漏和生命周期异常。

更新后代码仍崩溃的原因

TimerTask运行在后台线程,而MutableLiveData.setValue()必须在主线程调用,后台线程调用该方法会抛出未捕获异常,导致应用崩溃。需改用postValue(),它能在后台线程安全地将更新任务投递到主线程执行。

正确实现方案

1. ViewModel层实现

class CameraViewModel(application: Application) : AndroidViewModel(application) {
    private val _timerCounterText = MutableLiveData<String>()
    val timerCounterText: LiveData<String> = _timerCounterText // 对外暴露不可变LiveData
    private var timer: Timer? = null
    private var timerCounter: UInt = 0u

    fun startTimer(initialText: String = "00:00") {
        timerCounter = 0u
        _timerCounterText.postValue(initialText)
        timer?.cancel() // 先取消已有定时器,避免重复启动
        timer = Timer()
        timer?.scheduleAtFixedRate(object : TimerTask() {
            override fun run() {
                timerCounter += 1u
                val timeStr = String.format("%02d:%02d", 
                    timerCounter / 60u, 
                    timerCounter % 60u
                )
                _timerCounterText.postValue(timeStr) // 后台线程用postValue更新LiveData
            }
        }, 1000, 1000)
    }

    fun stopTimer() {
        timer?.cancel()
        timer?.purge()
        timer = null
        _timerCounterText.postValue(getApplication<Application>().getString(R.string.video_timer))
    }

    override fun onCleared() {
        super.onCleared()
        timer?.cancel() // ViewModel销毁时取消定时器,防止内存泄漏
    }
}

2. Activity层订阅

// 在Activity生命周期方法(如onCreate)中订阅
cameraViewModel.timerCounterText.observe(this) { timeStr ->
    timerText.text = timeStr
}

// 启动录像时调用
cameraViewModel.startTimer()

// 停止录像时调用
cameraViewModel.stopTimer()

关键优化点

  • 使用postValue()更新LiveData:后台线程中必须通过postValue()通知UI更新,确保操作在主线程执行。
  • ViewModel与UI解耦:通过LiveData传递数据,ViewModel不持有任何UI控件引用,符合MVVM架构规范。
  • 定时器生命周期管理:启动新定时器前取消旧实例,ViewModel销毁时清理定时器,避免内存泄漏和重复任务。
  • 简化时间格式化:用String.format替代字符串拼接,代码更简洁易维护。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 10:55:26