【Android】ViewModelScope 与协程生命周期管理:告别内存泄漏,掌控异步边界
2026/6/14 7:22:01 网站建设 项目流程

ViewModelScope 与协程生命周期管理:告别内存泄漏,掌控异步边界

>一句话收益:彻底理解viewModelScopelifecycleScoperepeatOnLifecycle的边界差异,写出不泄漏、不崩溃的协程代码。

>适用版本:Lifecycle 2.4+,Kotlin Coroutines 1.6+,Android 12+

>阅读时长:约 18 分钟

---

场景切入:一个隐蔽的内存泄漏

你在 ViewModel 里启动了一个协程请求网络,用户旋转屏幕,旧 Activity 销毁,新 Activity 创建——但那个网络请求还在后台跑,并且持有了旧 Activity 的 Context 引用。LeakCanary 报警了,你盯着日志一脸茫然:协程不是应该自动取消的吗?

问题在于:你用错了 scope

---

一、协程 Scope 全景:三个核心入口

CoroutineScope 体系

├── viewModelScope // 绑定 ViewModel 生命周期

│ └── 取消时机: ViewModel.onCleared()

├── lifecycleScope // 绑定 Lifecycle Owner(Activity/Fragment)

│ └── 取消时机: Lifecycle.DESTROYED

└── repeatOnLifecycle // 在 lifecycleScope 内,按 State 暂停/恢复

└── 暂停时机: 低于指定 State(如 STARTED)

三者定位不同,混用会出问题:

| Scope | 持有者 | 取消时机 | 典型用途 |

|---|---|---|---|

| viewModelScope | ViewModel | onCleared() | 业务逻辑、数据请求 |

| lifecycleScope | Activity/Fragment | onDestroy() | UI 操作、单次任务 |

| repeatOnLifecycle | 依附于 lifecycleScope | 低于指定 State | 持续收集 Flow |

---

二、viewModelScope 原理深挖

2.1 它从哪里来?

viewModelScopeViewModel的扩展属性,定义在androidx.lifecycle:lifecycle-viewmodel-ktx
// androidx/lifecycle/ViewModel.kt (简化)

val ViewModel.viewModelScope: CoroutineScope

get() {

val scope: CoroutineScope? = this.getTag(JOB_KEY)

if (scope != null) return scope

return setTagIfAbsent(

JOB_KEY,

CloseableCoroutineScope(

SupervisorJob() + Dispatchers.Main.immediate

)

)

}

关键点:

-SupervisorJob():子协程失败不会取消兄弟协程

-Dispatchers.Main.immediate:默认在主线程调度,避免不必要的切换

-CloseableCoroutineScope:实现了CloseableViewModel.clear()时自动调用close()

2.2 取消链路追踪(AOSP 路径)

// 调用链(AOSP Lifecycle 2.4+)

ViewModelStore.clear()

└─ ViewModel.clear() // androidx/lifecycle/ViewModel.java

└─ ViewModel.onCleared() // 用户可 override

└─ mBagOfTags.values // 遍历所有 CloseableCoroutineScope

└─ Closeable.close() // 触发协程取消

AOSP 源码路径:frameworks/support/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java

2.3 何时触发 onCleared?

用户按返回键 → Activity.finish() → ViewModelStore.clear() → ✅ 正常取消

旋转屏幕 → Activity 重建,ViewModel 保留 → ❌ 不取消(正确行为)

进程被系统杀死 → ViewModel 直接消失,协程同样消失 → ✅

Activity 被替换进 BackStack → ViewModel 仍存活 → ❌ 不取消(注意!)

---

三、错误写法 → 问题 → 正确写法

案例 1:在 Fragment 中用 lifecycleScope 收集 Flow

// ❌ 错误写法:Fragment onStop 后仍然收集,浪费资源甚至 NPE

class MyFragment : Fragment() {

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

lifecycleScope.launch {

viewModel.uiState.collect { state ->

updateUI(state) // Fragment 已 STOPPED,updateUI 可能崩溃

}

}

}

}

// ✅ 正确写法:用 repeatOnLifecycle 绑定到 STARTED

class MyFragment : Fragment() {

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

viewLifecycleOwner.lifecycleScope.launch {

viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {

viewModel.uiState.collect { state ->

updateUI(state) // 只在 STARTED 到 STOPPED 期间执行

}

}

}

}

}

// 注意:用 viewLifecycleOwner 而不是 this,防止 View 复用时泄漏

案例 2:ViewModel 中直接操作 UI

// ❌ 错误写法:ViewModel 持有 Activity 引用

class MyViewModel(private val activity: Activity) : ViewModel() {

fun loadData() {

viewModelScope.launch {

val data = repository.fetch()

activity.updateUI(data) // 泄漏!旋转屏幕后旧 Activity 无法回收

}

}

}

// ✅ 正确写法:通过 StateFlow/SharedFlow 传递数据

class MyViewModel : ViewModel() {

private val _uiState = MutableStateFlow (UiState.Loading)

val uiState: StateFlow = _uiState.asStateFlow()

fun loadData() {

viewModelScope.launch {

val data = repository.fetch()

_uiState.value = UiState.Success(data) // 无 UI 引用,安全

}

}

}

案例 3:忽略 SupervisorJob 导致全部子协程取消

// ❌ 错误写法:用普通 Job,一个子协程异常取消所有任务

class MyViewModel : ViewModel() {

private val scope = CoroutineScope(Job() + Dispatchers.Main)

fun loadAll() {

scope.launch { loadUserInfo() } // 如果这个抛异常

scope.launch { loadNewsFeed() } // 这个也会被取消!

}

}

// ✅ 正确写法:使用 viewModelScope 内置 SupervisorJob

class MyViewModel : ViewModel() {

fun loadAll() {

viewModelScope.launch {

supervisorScope {

launch { loadUserInfo() } // 失败不影响兄弟协程

launch { loadNewsFeed() }

}

}

}

}

---

四、repeatOnLifecycle 深度解析

4.1 内部状态机

Activity Lifecycle

onCreate → onStart → onResume → onPause → onStop → onDestroy

↓ ↑

[STARTED] [STOPPED]

协程块 launch 协程块 cancel

(repeatOnLifecycle 重新启动) (等待下次 STARTED)

repeatOnLifecycle(Lifecycle.State.STARTED)的行为:

1. 每次生命周期进入STARTED重新启动协程块

2. 每次生命周期低于STARTED(进入CREATED/DESTROYED),取消协程块

3. 最终在DESTROYED时永久取消

4.2 State 选择指南

| State | 含义 | 推荐场景 |

|---|---|---|

| CREATED | Activity 已创建,不可见 | 极少用,几乎不推荐 |

| STARTED | 可见但可能不在前台 |推荐:收集 UI 状态、展示数据 |

| RESUMED | 完全在前台 | 相机、传感器等需要独占资源 |

4.3 stateIn 与 repeatOnLifecycle 配合

// ViewModel 侧:将冷 Flow 转为热 StateFlow

class MyViewModel : ViewModel() {

val userProfile: StateFlow = repository.getUserFlow()

.stateIn(

scope = viewModelScope,

started = SharingStarted.WhileSubscribed(5_000), // 5秒无订阅停止上游

initialValue = null

)

}

// Fragment 侧:用 repeatOnLifecycle 收集

class MyFragment : Fragment() {

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

viewLifecycleOwner.lifecycleScope.launch {

viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {

viewModel.userProfile.collect { profile ->

profile?.let { renderProfile(it) }

}

}

}

}

}

---

五、最佳实践

5.1 异常处理:CoroutineExceptionHandler

做法:用CoroutineExceptionHandler统一处理非预期异常,结合try/catch处理可预期的业务错误。原因SupervisorJob不会传播子协程异常到父级,未捕获的异常会静默丢失,导致 UI 无响应。对比:不加 handler,崩溃被SupervisorJob吞掉,LogCat 无报错,用户只看到白屏。
private val handler = CoroutineExceptionHandler { _, e ->

_errorEvent.value = e.message ?: "未知错误"

}

fun loadData() {

viewModelScope.launch(handler) {

try {

_uiState.value = UiState.Success(repository.fetch())

} catch (e: NetworkException) {

_uiState.value = UiState.Error(e.message)

}

}

}

5.2 取消与超时控制

做法:给网络请求加withTimeout,并区分CancellationException和业务异常。原因:协程取消通过CancellationException传播,若在catch中捕获了所有异常,会阻止取消传播。对比catch (e: Exception)捕获了CancellationException,导致协程取消失效,ViewModel 销毁后请求依然执行。
viewModelScope.launch {

try {

withTimeout(10_000L) {

val data = repository.fetchData()

_uiState.value = UiState.Success(data)

}

} catch (e: TimeoutCancellationException) {

_uiState.value = UiState.Error("请求超时")

} catch (e: CancellationException) {

throw e // ✅ 必须重新抛出,让协程正常取消

} catch (e: Exception) {

_uiState.value = UiState.Error(e.message ?: "网络错误")

}

}

5.3 stateIn 黄金配置

做法:用stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), initialValue)将冷流转热流。原因:多个 collector 共享同一个上游订阅,旋转屏幕 5 秒内重新订阅不会重启上游数据源。对比:不用stateIn,旋转一次屏幕就重新查数据库,5个 collector 就查5次,性能差且体验不连贯。
val userProfile: StateFlow = repository.getUserFlow()

.stateIn(

scope = viewModelScope,

started = SharingStarted.WhileSubscribed(5_000),

initialValue = null

)

---

六、常见坑点

坑 1:launchWhenStarted的"伪取消"陷阱

-现象:切换到后台后,Flow 停止回调,但 LeakCanary 仍报内存泄漏,返回前台数据堆积爆发。

-原因launchWhenStarted只是挂起协程而非取消,协程持有 collector 引用不释放,上游持续发射的数据积压在 Channel 缓冲区。

-复现:使用lifecycleScope.launchWhenStarted { hotFlow.collect { } }+ 频繁锁屏解锁。

-解决:废弃launchWhenStarted,改用repeatOnLifecycle(STARTED),后者真正取消并重启协程块。

坑 2:collect阻塞导致后续代码不执行

-现象collect之后的代码从未执行到,逻辑看似正确但毫无响应。

-原因StateFlow.collect是挂起函数,永不返回,后续代码处于死区。

-复现launch { flowA.collect { } ; flowB.collect { } }——flowB永远不会被收集。

-解决:每个 Flow 用独立的launch块,或使用combine/merge合并多个 Flow。

坑 3:Fragment 中观察者重复注册

-现象:页面旋转后,Toast 或导航事件触发了两次。

-原因:每次 Fragment 进出 BackStack,新的 Observer 被添加,但旧的因为用了this(Fragment 存活)没有取消,导致多次触发。

-复现lifecycleScope.launch { sharedFlow.collect { navigate() } }onViewCreated中调用,但用的是this而非viewLifecycleOwner

-解决:改用viewLifecycleOwner.lifecycleScope,View 销毁时自动取消订阅。

坑 4:GlobalScope导致协程无法取消

-现象:用户退出 App 后,后台任务还在跑,Battery Historian 显示持续唤醒。

-原因GlobalScope不绑定任何生命周期,APP 进程存活期间协程一直运行,且无法通过 ViewModel/Lifecycle 取消。

-复现GlobalScope.launch { infiniteLoop() }放在 ViewModel 中。

-解决:绝对禁止在 ViewModel 中使用GlobalScope,一律替换为viewModelScope

---

七、总结

1.viewModelScope默认首选:业务逻辑、数据请求都放这里,随onCleared()自动取消,天然支持旋转屏幕。

2.收集 Flow 必用repeatOnLifecycle:替代已废弃的launchWhenStarted,真正取消协程而非挂起。

3.Fragment 永远用viewLifecycleOwner:View 生命周期 ≠ Fragment 生命周期,混用必泄漏。

4.stateIn(WhileSubscribed(5000))是旋转黄金配置:兼顾性能和 UX,5 秒窗口容纳旋转重建。

5.CancellationException必须重新抛出:捕获所有异常会阻断协程取消链路。

>核心结论:协程生命周期管理的本质是"让数据生命周期与 UI 生命周期解耦"——ViewModel 持有数据,repeatOnLifecycle桥接展示,边界清晰则泄漏无处藏身。

---

参考资料

- 官方文档:Use Kotlin coroutines with lifecycle-aware components

- 官方文档:repeatOnLifecycle API design story

- 官方文档:StateFlow and SharedFlow

- AOSP 源码:frameworks/support/lifecycle/lifecycle-viewmodel-ktx/src/main/java/androidx/lifecycle/ViewModel.kt

- AOSP 源码:frameworks/support/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/RepeatOnLifecycle.kt

- AOSP 源码:frameworks/support/lifecycle/lifecycle-runtime-ktx/src/main/java/androidx/lifecycle/FlowExt.kt

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询