重启应用后后台BLE扫描线程指向旧ViewModel的问题如何解决?
问题根因
后台线程持有旧ViewModel实例是问题核心。默认Koin提供的ViewModel是和页面(Activity/Fragment)生命周期绑定的,关闭应用再重新打开时,页面实例重建,Koin会注入新的ViewModel实例,而之前启动的死循环线程没有销毁,仍然持有旧ViewModel的引用,所以只能读到旧值,新页面的赋值操作是作用在新ViewModel实例上的,旧线程完全感知不到。同时你的无限循环线程会持续占用CPU资源,还会导致旧页面、旧ViewModel无法被回收,引发内存泄漏。
解决方案
- 把告警状态从页面级ViewModel迁移到全局单例管理
BLE扫描、告警属于全局后台业务,和页面生命周期无关,直接用Koin声明一个单例的AlertStateManager类,统一存储stopAlertingAt、isStopAlerting等全局状态,无论页面如何重建,所有逻辑访问的都是同一份状态,不会出现实例不一致的问题。 - 替换无限轮询线程为延时任务
无限循环轮询功耗极高,完全可以用延时任务替代。用户点击snooze按钮时,直接启动对应时长的延时任务,时间到后自动修改告警状态,无需持续轮询。可以用协程delay、AlarmManager或者WorkManager实现,其中WorkManager还支持进程被杀后任务仍然生效。 - 状态持久化防止进程重启丢失数据
把snooze的时间戳持久化存储到SharedPreferences或DataStore中,应用进程被杀后重启时,读取本地存储的时间戳计算剩余snooze时长,自动恢复状态,避免进程重启后状态丢失。 - 销毁无用线程
如果后续还有页面侧启动线程的场景,一定要在页面销毁(onDestroy)时主动终止线程,避免无效线程一直在后台运行占用资源、持有旧实例引发泄漏。
代码示例
全局单例状态管理(Koin声明为single单例)
class AlertStateManager { // 用StateFlow或LiveData都可,根据自己的技术栈选择 val stopAlertingAt = MutableStateFlow<Long?>(null) val isStopAlerting = MutableStateFlow(false) }
snooze按钮点击逻辑
binding.btnDismiss.setOnClickListener { val snoozeTime = System.currentTimeMillis() // 更新全局状态 alertStateManager.stopAlertingAt.value = snoozeTime alertStateManager.isStopAlerting.value = true // 读取配置的snooze时长 val durationMin = PreferenceManager.getDefaultSharedPreferences(this) .getString("dismiss_duration", "1")!!.toInt() // 启动延时任务 CoroutineScope(Dispatchers.IO).launch { delay(durationMin * 60 * 1000L) alertStateManager.isStopAlerting.value = false } // 持久化存储snooze时间,进程重启也能恢复 PreferenceManager.getDefaultSharedPreferences(this) .edit() .putLong("last_stop_alert_at", snoozeTime) .apply() }
应用启动/后台服务启动时状态恢复逻辑
val lastSnoozeTime = PreferenceManager.getDefaultSharedPreferences(this) .getLong("last_stop_alert_at", 0) val durationMin = PreferenceManager.getDefaultSharedPreferences(this) .getString("dismiss_duration", "1")!!.toInt() val passedTime = System.currentTimeMillis() - lastSnoozeTime if (passedTime < durationMin * 60 * 1000L) { // 还在snooze周期内,恢复状态并补剩余延时 alertStateManager.isStopAlerting.value = true val remainingTime = durationMin * 60 * 1000L - passedTime CoroutineScope(Dispatchers.IO).launch { delay(remainingTime) alertStateManager.isStopAlerting.value = false } } else { alertStateManager.isStopAlerting.value = false }
页面侧的ViewModel只需要监听AlertStateManager的状态变化同步更新UI即可,不需要再单独存储告警相关状态,原有的无限轮询线程可直接删除。
内容的提问来源于stack exchange,提问作者Amine Harbaoui
相关产品推荐
相关产品推荐

