前言

用kotlin 搬砖Android 就知道,在Android 中viewModel 中使用协程,建议用viewModelScope。这个玩意需要导入包:

androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2

嗯,在google Android 中 有这么一句话:

ViewModelScope是为 ViewModel您的应用程序中的每个定义的。如果清除,在此范围内启动的任何协程都会自动取消ViewModel 。当您只有在活动时才需要完成工作时,协程非常有用ViewModel。例如,如果您正在计算布局的一些数据,则应将工作范围限制在 ,ViewModel以便在 ViewModel清除 时,工作会自动取消以避免消耗资源。

通俗的理解就是,viewModelScope创建的协程的生命周期和viewModel 的生命周期是绑定的,当viewModel 被取消的时候,viewModelScope创建的协程也会被取消。而我们知道,VIewMoel 的生命周期和Activity或fragment 绑定的。

viewModel 如何取消后台任务

在activity的源码 androidx.activity.ComponentActivity 代码中:

getLifecycle().addObserver(new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
            if (event == Lifecycle.Event.ON_DESTROY) {
                // Clear out the available context
                mContextAwareHelper.clearAvailableContext();
                // And clear the ViewModelStore
                if (!isChangingConfigurations()) {
                    getViewModelStore().clear();
                }
                mReportFullyDrawnExecutor.activityDestroyed();
            }
        }
    });
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

可以看到,当activity 处于ON_DESTROY的时候,会调用 :

if (!isChangingConfigurations()) {
    getViewModelStore().clear();
}
  • 1.
  • 2.
  • 3.

通常来说,界面的配置是不会更改的,更改的时候,比如说,横竖屏切换,比如说 字体,字号,系统主题等等。所以我们就可以简单认为当activity 销毁的时候,就会销毁这个玩意。我们知道 ViewModelStore是用于存放多个viewModel 的,所以他的clear,其实也通过循环调用的VIewModel 中的clear() 函数。

fun clear() {
    for (vm in map.values) {
        vm.clear()
    }
    map.clear()
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

而fragment 中就稍显复杂,我们这里先不讨论这个玩意。

结合activity 里面调用ViewModel 的clear() 函数的思路,我们就可以发现,无论是JAVA 还是kotlin 需要终止一个后台任务,其中一个办法就是重写ViewModel 中和clear() 相关的函数。但是我们通过阅读源码可以发现:

final void clear() {}
  • 1.

这个函数不可更改,但是他在clear() 函数中调用了:

protected void onCleared() {
}
  • 1.
  • 2.

那么我们想要取消自己的后台任务,那么其中一个思路就是重写onCleared 函数,那么还有没有其他思路呢?我们结合clear的源码:

final void clear() {
    mCleared = true;
    if (mBagOfTags != null) {
        synchronized (mBagOfTags) {
            for (Object value : mBagOfTags.values()) {
                // see comment for the similar call in setTagIfAbsent
                closeWithRuntimeException(value);
            }
        }
    }
    if (mCloseables != null) {
        synchronized (mCloseables) {
            for (Closeable closeable : mCloseables) {
                closeWithRuntimeException(closeable);
            }
        }
    }
    onCleared();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

可以看到,两个变量mBagOfTags和mCloseables 都调用了closeWithRuntimeException 函数。

private static void closeWithRuntimeException(Object obj) {
    if (obj instanceof Closeable) {
        try {
            ((Closeable) obj).close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

当循环对象是实现与Closeable 的时候,就调用Closeable 的close() 函数。所以我们还有一个思路,就是后台任务实现Closeable 接口,然后为viewModel 添加mBagOfTags或mCloseables。具体添加什么,看业务诉求,结合源码:

@Nullable
private final Map<String, Object> mBagOfTags = new HashMap<>();
@Nullable
private final Set<Closeable> mCloseables = new LinkedHashSet<>();
  • 1.
  • 2.
  • 3.
  • 4.

我们可以看出mBagOfTags 适合用于要取值得情况,而mCloseables 则通常用于不需要取出来的情况。OK,到这里,我们可以大概知道viewModel 中取消后台任务的几种方式了,那么viewModelScope 想要取消,就一定得通过这几种方式来处理。

viewModelScope 如何取消后台任务

在没有看viewModelScope 的源码 时候,我们可以猜测一下 viewModelScope是如何实现的,这个玩意是变量,而且是一个额外的库实现的,所以,这个这个玩意的实现,一定是扩展属性,对吧。但是扩展属性,得有一个地方存放这个调调,总不能存储为全局变量吧,而恰好,viewModel 中有一个mBagOfTags 支持存取,所以他可能是用到了mBagOfTags。OK,那么我们就来看源码:

private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"

public 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)
        )
    }

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context

    override fun close() {
        coroutineContext.cancel()
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

源码超级少,超级单纯好吧。viewModelScope是一个实现Closeable接口的CoroutineScope,而CoroutineScope是协程上下文。我们创建协程的时候,这个玩意是必要的。所以:

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context

    override fun close() {
        coroutineContext.cancel()
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

这个的作用其实是取消协程。

public 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)
        )
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

重写了viewModelScope的get方法,优先在viewModel 中的mBagOfTags获取,如果没有就创建一个新的,这么就保证了viewModelScope在viewModel 中的唯一性,我们通过viewModelScope 创建的所有协程,都将是viewModelScope的子协程,我们知道当父协程取消的时候,子协程也将被取消,所以viewModelScope其实是利用了这个特性。viewModelScope被添加到mBagOfTags,当viewModel 被取消的时候,会取消mBagOfTags 里面的Closeable。

总结

其实这个代码逻辑是非常简单的,利用了Closeable接口和扩展属性,利用viewModel 取消mBagOfTags的特性实现了这一功能。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。

kotlin中对于ViewModel的扩展属性viewModelScope理解_android


相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。