前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[译]Android中的简易协程:viewModelScope

[译]Android中的简易协程:viewModelScope

作者头像
Android 开发者
发布2024-01-26 10:23:18
3200
发布2024-01-26 10:23:18
举报
文章被收录于专栏:Android 开发者

Virginia Poltrack 绘图

取消不再需要的协程(coroutine)是件容易被遗漏的任务,它既枯燥又会引入大量模版代码。viewModelScope结构化并发 的贡献在于将一项扩展属性加入到 ViewModel 类中,从而在 ViewModel 销毁时自动地取消子协程。

声明viewModelScope 将会在尚在 alpha 阶段的 AndroidX Lifecycle v2.1.0 中引入。正因为在 alpha 阶段,API 可能会更改,可能会有 bug。点这里报错。

ViewModel的作用域

CoroutineScope 会跟踪所有它创建的协程。因此,当你取消一个作用域的时候,所有它创建的协程也会被取消。当你在 ViewModel 中运行协程的时候这一点尤其重要。如果你的 ViewModel 即将被销毁,那么它所有的异步工作也必须被停止。否则,你将浪费资源并有可能泄漏内存。如果你觉得某项异步任务应该在 ViewModel 销毁后保留,那么这项任务应该放在应用架构的较低一层。

创建一个新作用域,并传入一个将在 onCleared() 方法中取消的 SupervisorJob,这样你就在 ViewModel 中添加了一个 CoroutineScope。此作用域中创建的协程将会在 ViewModel 使用期间一直存在。代码如下:

代码语言:javascript
复制
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 来进行简化。

viewModelScope 可以减少模版代码

AndroidX lifecycle v2.1.0 在 ViewModel 类中引入了扩展属性 viewModelScope。它以与前一小节相同的方式管理协程。代码则缩减为:

代码语言:javascript
复制
class MyViewModel : ViewModel() {
  
    /**
     * 没法在主线程完成的繁重操作
     */
    fun launchDataLoad() {
        viewModelScope.launch {
            sortList()
            // 更新 UI
        }
    }
  
    suspend fun sortList() = withContext(Dispatchers.Default) {
        // 繁重任务
    }
}

所有的 CoroutineScope 创建和取消步骤都为我们准备好了。使用时只需在 build.gradle 文件导入如下依赖:

代码语言:javascript
复制
implementation “androidx.lifecycle.lifecycle-viewmodel-ktx$lifecycle_version”

我们来看一下底层是如何实现的。

深入viewModelScope

AOSP有分享的代码。viewModelScope 是这样实现的:

代码语言:javascript
复制
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 代码在此,但我们只会讨论大家关心的部分:

代码语言:javascript
复制
@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 接口。

代码语言:javascript
复制
internal class CloseableCoroutineScope(
    context: CoroutineContext
) : Closeable, CoroutineScope {
  
    override val coroutineContext: CoroutineContext = context
  
    override fun close() {
        coroutineContext.cancel()
    }
}

默认使用 Dispatchers.Main

Dispatchers.MainviewModelScope 的默认 CoroutineDispatcher

代码语言:javascript
复制
val scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main)

Dispatchers.Main 在此合用是因为 ViewModel 与频繁更新的 UI 相关,而用其他的派发器就会引入至少2个线程切换。考虑到挂起方法自身有线程封闭机制,使用其他派发器并不合适,因为我们不想去取代 ViewModel 已有的功能。

单元测试 viewModelScope

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 来简化你的代码。

代码语言:javascript
复制
@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()
  }
}

现在,你可以把它加入你的单元测试了。

代码语言:javascript
复制
class MainViewModelUnitTest {
  
    @get:Rule
    var coroutinesMainDispatcherRule = CoroutinesMainDispatcherRule()
  
    @Test
    fun test() {
        ...
    }
}

请注意这是有可能变的。TestCoroutineContext 与结构化并发集成的工作正在进行中,详细信息请看这个 issue


如果你使用 ViewModel 和协程, 通过 viewModelScope 让框架管理生命周期吧!不用多考虑了!

Coroutines codelab 已经更新并使用它了。学习一下怎样在 Android 应用中使用协程吧。

文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-01-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ViewModel的作用域
  • viewModelScope 可以减少模版代码
  • 深入viewModelScope
  • 默认使用 Dispatchers.Main
  • 单元测试 viewModelScope
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档