取消不再需要的协程(coroutine)是件容易被遗漏的任务,它既枯燥又会引入大量模版代码。viewModelScope
对结构化并发 的贡献在于将一项扩展属性加入到 ViewModel 类中,从而在 ViewModel 销毁时自动地取消子协程。
声明:viewModelScope
将会在尚在 alpha 阶段的 AndroidX Lifecycle v2.1.0 中引入。正因为在 alpha 阶段,API 可能会更改,可能会有 bug。点这里报错。
CoroutineScope 会跟踪所有它创建的协程。因此,当你取消一个作用域的时候,所有它创建的协程也会被取消。当你在 ViewModel 中运行协程的时候这一点尤其重要。如果你的 ViewModel 即将被销毁,那么它所有的异步工作也必须被停止。否则,你将浪费资源并有可能泄漏内存。如果你觉得某项异步任务应该在 ViewModel 销毁后保留,那么这项任务应该放在应用架构的较低一层。
创建一个新作用域,并传入一个将在 onCleared()
方法中取消的 SupervisorJob,这样你就在 ViewModel 中添加了一个 CoroutineScope。此作用域中创建的协程将会在 ViewModel 使用期间一直存在。代码如下:
class MyViewModel : ViewModel() {
/**
* 这是此 ViewModel 运行的所有协程所用的任务。
* 终止这个任务将会终止此 ViewModel 开始的所有协程。
*/
private val viewModelJob = SupervisorJob()
/**
* 这是 MainViewModel 启动的所有协程的主作用域。
* 因为我们传入了 viewModelJob,你可以通过调用viewModelJob.cancel()
* 来取消所有 uiScope 启动的协程。
*/
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
/**
* 当 ViewModel 清空时取消所有协程
*/
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
/**
* 没法在主线程完成的繁重操作
*/
fun launchDataLoad() {
uiScope.launch {
sortList()
// 更新 UI
}
}
suspend fun sortList() = withContext(Dispatchers.Default) {
// 繁重任务
}
}
当 ViewModel 销毁时后台运行的繁重操作会被取消,因为对应的协程是由这个 uiScope
启动的。
但在每个 ViewModel 中我们都要引入这么多代码,不是吗?我们其实可以用 viewModelScope
来进行简化。
AndroidX lifecycle v2.1.0 在 ViewModel 类中引入了扩展属性 viewModelScope
。它以与前一小节相同的方式管理协程。代码则缩减为:
class MyViewModel : ViewModel() {
/**
* 没法在主线程完成的繁重操作
*/
fun launchDataLoad() {
viewModelScope.launch {
sortList()
// 更新 UI
}
}
suspend fun sortList() = withContext(Dispatchers.Default) {
// 繁重任务
}
}
所有的 CoroutineScope 创建和取消步骤都为我们准备好了。使用时只需在 build.gradle
文件导入如下依赖:
implementation “androidx.lifecycle.lifecycle-viewmodel-ktx$lifecycle_version”
我们来看一下底层是如何实现的。
AOSP有分享的代码。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))
}
ViewModel 类有个 ConcurrentHashSet
属性来存储任何类型的对象。CoroutineScope 就存储在这里。如果我们看下代码,getTag(JOB_KEY)
方法试图从中取回作用域。如果取回值为空,它将以前文提到的方式创建一个新的 CoroutineScope 并将其加标签存储。
当 ViewModel 被清空时,它会运行 clear()
方法进而调用如果不用 viewModelScope 我们就得重写的 onCleared()
方法。在 clear()
方法中,ViewModel 会取消 viewModelScope
中的任务。完整的 ViewModel 代码在此,但我们只会讨论大家关心的部分:
@MainThread
final void clear() {
mCleared = true;
// 因为 clear() 是 final 的,这个方法在模拟对象上仍会被调用,
// 且在这些情况下,mBagOfTags 为 null。但它总会为空,
// 因为 setTagIfAbsent 和 getTag 不是
// final 方法所以我们不用清空它。
if (mBagOfTags != null) {
for (Object value : mBagOfTags.values()) {
// see comment for the similar call in setTagIfAbsent
closeWithRuntimeException(value);
}
}
onCleared();
}
这个方法遍历所有对象并调用 closeWithRuntimeException
,此方法检查对象是否属于 Closeable
类型,如果是就关闭它。为了使作用域被 ViewModel 关闭,它应当实现 Closeable
接口。这就是为什么 viewModelScope
的类型是 CloseableCoroutineScope
,这一类型扩展了 CoroutineScope
、重写了 coroutineContext
并且实现了 Closeable
接口。
internal class CloseableCoroutineScope(
context: CoroutineContext
) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
Dispatchers.Main
是 viewModelScope
的默认 CoroutineDispatcher。
val scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main)
Dispatchers.Main
在此合用是因为 ViewModel 与频繁更新的 UI 相关,而用其他的派发器就会引入至少2个线程切换。考虑到挂起方法自身有线程封闭机制,使用其他派发器并不合适,因为我们不想去取代 ViewModel 已有的功能。
Dispatchers.Main
利用 Android 的 Looper.getMainLooper()
方法在 UI 线程执行代码。这个方法在 Instrumented Android 测试中可用,在单元测试中不可用。
借用 org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version
库,调用 Dispatchers.setMain
并传入一个 singleThreadExecutor 来替换主派发器。不要用Dispatchers.Unconfined
,它会破坏使用 Dispatchers.Main
的代码的所有假设和时间线。因为单元测试应该在隔离状态下运行完好且不造成任何副作用,所以当测试完成时,你应该调用 Dispatchers.resetMain()
来清理执行器。
你可以用以下体现这一逻辑的 JUnitRule 来简化你的代码。
@ExperimentalCoroutinesApi
class CoroutinesMainDispatcherRule : TestWatcher() {
private val singleThreadExecutor = Executors.newSingleThreadExecutor()
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(singleThreadExecutor.asCoroutineDispatcher())
}
override fun finished(description: Description?) {
super.finished(description)
singleThreadExecutor.shutdownNow()
Dispatchers.resetMain()
}
}
现在,你可以把它加入你的单元测试了。
class MainViewModelUnitTest {
@get:Rule
var coroutinesMainDispatcherRule = CoroutinesMainDispatcherRule()
@Test
fun test() {
...
}
}
请注意这是有可能变的。TestCoroutineContext 与结构化并发集成的工作正在进行中,详细信息请看这个 issue。
如果你使用 ViewModel 和协程, 通过 viewModelScope
让框架管理生命周期吧!不用多考虑了!
Coroutines codelab 已经更新并使用它了。学习一下怎样在 Android 应用中使用协程吧。
文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。