[译]Coroutines in Android ViewModelScope篇

2,222 阅读3分钟

原文作者 :Manuel Vivo

原文地址: Easy Coroutines in Android: viewModelScope

译者 : 京平城

想省时间的读者可以直接看文章最后的译者总结

Scopes in ViewModels

CoroutineScope管理着它所创建的coroutines,当CoroutineScope被cancel的时候,它所创建的 coroutines也都会被cancel。当你在ViewModel中使用coroutines的时候,这个特性非常有用。
当ViewModel销毁的时候,所有正在运行的异步任务也应当停止,不然会造成资源的浪费和潜在的memory leak风险。

首先我们来看一下,在不使用ViewModelScope的情况下,我们是如何在ViewModel中使用coroutines的。
利用SupervisorJob来创建一个新的CoroutineScope,这个CoroutineScope将和ViewModel的生命周期一致。 在onCleared()方法中调用SupervisorJob的cancel方法,那么uiScope下的所有coroutines都会被cancel。

class MyViewModel : ViewModel() {

    /**
     * This is the job for all coroutines started by this ViewModel.
     * Cancelling this job will cancel all coroutines started by this ViewModel.
     */
    private val viewModelJob = SupervisorJob()
    
    /**
     * This is the main scope for all coroutines launched by MainViewModel.
     * Since we pass viewModelJob, you can cancel all coroutines 
     * launched by uiScope by calling viewModelJob.cancel()
     */
    private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
    
    /**
     * Cancel all coroutines when the ViewModel is cleared
     */
    override fun onCleared() {
        super.onCleared()
        viewModelJob.cancel()
    }
    
    /**
     * Heavy operation that cannot be done in the Main Thread
     */
    fun launchDataLoad() {
        uiScope.launch {
            sortList() // happens on the background
            // Modify UI
        }
    }
    
    // Move the execution off the main thread using withContext(Dispatchers.Default)
    suspend fun sortList() = withContext(Dispatchers.Default) {
        // Heavy work
    }
}

下面我们来看看使用viewModelScope是怎么帮助我们简化上述代码的。

viewModelScope means less boilerplate code

使用AndroidX lifecycle v2.1.0中新增的扩展属性viewModelScope能帮助我们把上述代码简化为

class MyViewModel : ViewModel() {
  
    /**
     * Heavy operation that cannot be done in the Main Thread
     */
    fun launchDataLoad() {
        viewModelScope.launch {
            sortList()
            // Modify UI
        }
    }
  
    suspend fun sortList() = withContext(Dispatchers.Default) {
        // Heavy work
    }
}

如果要使用viewModelScope的话,首先请在你的build.gradle中添加

implementation "androidx.lifecycle.lifecycle-viewmodel-ktx$lifecycle_version"

接下来我们来探究一下viewModelScope的实现

Digging into 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.immediate))
    }

在ViewModel中使用了ConcurrentHashSet来保存CoroutineScope,CoroutineScope的key就是JOB_KEY
通过this.getTag(JOB_KEY)来获取CoroutineScope,如果没有就创建一个
setTagIfAbsent(JOB_KEY, CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
译者:我并没有能够在ViewModel源码中找到ConcurrentHashSet,只发现了一个hashmap和一行注释,不清楚是原作者的笔误还是代码已经发生了变化.

    // Can't use ConcurrentHashMap, because it can lose values on old apis (see b/37042460)
    @Nullable
    private final Map<String, Object> mBagOfTags = new HashMap<>();

当ViewModel执行clear操作的时候会在onCleared()方法之前先调用clear()方法。
在clear()方法中会循环遍历所有实现了Closeable接口的对象,并且调用他们的close()方法。

@MainThread
final void clear() {
    mCleared = true;
    // Since clear() is final, this method is still called on mock 
    // objects and in those cases, mBagOfTags is null. It'll always 
    // be empty though because setTagIfAbsent and getTag are not 
    // final so we can skip clearing it
    if (mBagOfTags != null) {
        for (Object value : mBagOfTags.values()) {
            // see comment for the similar call in setTagIfAbsent
            closeWithRuntimeException(value);
        }
    }
    onCleared();
}

private static void closeWithRuntimeException(Object obj) {
    if (obj instanceof Closeable) {
        try {
            ((Closeable) obj).close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

我们来看一下viewModelScope存在ViewModel中的那个CloseableCoroutineScope对象

internal class CloseableCoroutineScope(
    context: CoroutineContext
) : Closeable, CoroutineScope {
  
    override val coroutineContext: CoroutineContext = context
  
    override fun close() {
        coroutineContext.cancel()
    }
}

CloseableCoroutineScope实现了Closeable接口,并且当ViewModel执行clear操作的时候,它的close方法会被调用coroutineContext.cancel() 它的相关实现如下

/**
 * Cancels [Job] of this context with an optional cancellation cause.
 * See [Job.cancel] for details.
 */
public fun CoroutineContext.cancel(cause: CancellationException? = null) {
    this[Job]?.cancel(cause)
}

最后会调用job的cancel方法。

译者总结:
ViewModelScope是ViewModel的一个扩展属性,实现了CoroutineScope接口,是一个自定义的CoroutineScope。

当调用viewModelScope.launch(launch是CoroutineScope的一个扩展方法)的时候,它会在ViewModel的内部存储(hashmap)里添加一个CloseableCoroutineScope对象(即使多次调用viewModelScope.launch也只会创建一次CloseableCoroutineScope对象)。

该对象实现了Closeable接口和CoroutineScope接口,它的CoroutineContext是SupervisorJob + Dispatchers.Main.immediate。

在ViewModel执行clear操作的时候,会循环遍历内部存储(hashmap),并且执行实现了Closeable接口对象的close()方法,那么就相当于帮我们自动执行了viewModelScope里的SupervisorJob()的cancel()方法

再直白一点说就是ViewModelScope帮我们省去了在ViewModel中手动创建一个CoroutineScope对象的步骤 private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

并且帮我们省去了在onCleared()方法中手动调用job的cancel()方法

/**
* Cancel all coroutines when the ViewModel is cleared
*/
override fun onCleared() {
    super.onCleared()
    viewModelJob.cancel()
}