AndroidX Lifecycle v2.1.0 在 ViewModel 中引入 viewModelScope
,当 ViewModel 被销毁时它会自动取消协程任务,这个特性真的好用。本文介绍 viewModelScope
使用和内部实现方式,分析 ViewModel 是如何自动取消协程的。
ViewModel 引入协程
当我们在 ViewModel 里面需要引入协程,首先要在 ViewModel 中新建一个 CoroutineScope, 用来管理所有协程任务,同时需要 onCleared()
方法里面取消协程任务,模板代码实现如下:
- class MyViewModel : ViewModel() {
-
- private val viewModelJob = SupervisorJob()
-
- private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
-
- override fun onCleared() {
- super.onCleared()
- viewModelJob.cancel() // Cancel all coroutines
- }
-
- fun launchDataLoad() {
- uiScope.launch {
- sortList()
- // Modify UI
- }
- }
-
- suspend fun sortList() = withContext(Dispatchers.Default) {
- // Heavy work
- }
- }
- 复制代码
然而,很多情况我们会经常忘记取消协程,导致出现内存泄漏等各种问题。 遇到这种场景,可以使用 ViewModel 扩展属性 viewModelScope
来优化代码。
viewModelScope 方式
注意 lifecycle-viewmodel-ktx 版本号: 2.1.0-beta01
viewModelScope
管理协程的方式与我们在 ViewModel 引入协程的方式一样,使用非常简单,模板代码实现如下:
- class MyViewModel : ViewModel() {
-
- fun launchDataLoad() {
- viewModelScope.launch {
- sortList()
- // Modify UI
- }
- }
-
- suspend fun sortList() = withContext(Dispatchers.Default) {
- // Heavy work
- }
- }
- 复制代码
viewModelScope 内部实现
- // androidx.lifecycle.ViewModel.viewModelScope
-
- private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"
-
- 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))
- }
-
- internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
- override val coroutineContext: CoroutineContext = context
-
- override fun close() {
- coroutineContext.cancel()
- }
- }
- 复制代码
分析 viewModelScope
源码有 3 点需要关注:
- 注意使用
SupervisorJob
而不是用Job
- 为了 ViewModel 能够取消协程,需要实现
Closeable
接口 viewModelScope
默认使用Dispatchers.Main
, 方便 Activity 和 Fragment 更新 UI
ViewModel 内部取消协程
ViewModel 类通过 HashMap 存储 CoroutineScope 对象,当使用 getTag(JOB_KEY)
方法获取对象不存在时,创建一个新的 CoroutineScope 并调用 setTagIfAbsent(JOB_KEY, scope)
方法存储新建的 CoroutineScope 对象。ViewModel 被销毁时内部会执行 clear()
方法,在 clear()
方法中遍历调用 closeWithRuntimeException
取消了 viewModelScope
的协程,实现流程非常清晰。相关代码如下:
- // androidx.lifecycle.ViewModel
-
- // clear() -> closeWithRuntimeException() -> coroutineContext.cancel()
-
- private final Map<String, Object> mBagOfTags = new HashMap<>();
-
- <T> T getTag(String key) {
- synchronized (mBagOfTags) {
- return (T) mBagOfTags.get(key);
- }
- }
-
- <T> T setTagIfAbsent(String key, T newValue) {
- T previous;
- synchronized (mBagOfTags) {
- previous = (T) mBagOfTags.get(key);
- if (previous == null) {
- mBagOfTags.put(key, newValue);
- }
- }
- T result = previous == null ? newValue : previous;
- if (mCleared) {
- closeWithRuntimeException(result);
- }
- return result;
- }
-
- @MainThread
- final void clear() {
- mCleared = true;
- if (mBagOfTags != null) {
- for (Object value : mBagOfTags.values()) {
- closeWithRuntimeException(value);
- }
- }
- onCleared();
- }
-
- private static void closeWithRuntimeException(Object obj) {
- if (obj instanceof Closeable) {
- try {
- ((Closeable) obj).close();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
- }
- 复制代码
结论
如果你也正在使用 MVVM 和协程,非常推荐在 ViewModel 中使用 viewModelScope
方式。不仅简化 ViewModel 代码,而且还能管理协程生命周期。