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

用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 代码中:

less 复制代码
    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();
            }
        }
    });

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

scss 复制代码
if (!isChangingConfigurations()) {
    getViewModelStore().clear();
}

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

kotlin 复制代码
fun clear() {
    for (vm in map.values) {
        vm.clear()
    }
    map.clear()
}

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

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

arduino 复制代码
final void clear() {}

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

csharp 复制代码
protected void onCleared() {
}

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

scss 复制代码
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();
}

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

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

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

swift 复制代码
@Nullable
private final Map<String, Object> mBagOfTags = new HashMap<>();
@Nullable
private final Set<Closeable> mCloseables = new LinkedHashSet<>();

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

viewModelScope 如何取消后台任务

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

kotlin 复制代码
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()
    }
}

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

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

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

kotlin 复制代码
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)
        )
    }

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

总结

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

相关推荐
HerayChen1 小时前
HbuildderX运行到手机或模拟器的Android App基座识别不到设备 mac
android·macos·智能手机
顾北川_野1 小时前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
hairenjing11231 小时前
在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序
android·人工智能·windows·macos·智能手机
小黄人软件2 小时前
android浏览器源码 可输入地址或关键词搜索 android studio 2024 可开发可改地址
android·ide·android studio
dj15402252032 小时前
group_concat配置影响程序出bug
android·bug
周全全2 小时前
MySQL报错解决:The user specified as a definer (‘root‘@‘%‘) does not exist
android·数据库·mysql
- 羊羊不超越 -3 小时前
App渠道来源追踪方案全面分析(iOS/Android/鸿蒙)
android·ios·harmonyos
wk灬丨4 小时前
Android Kotlin Flow 冷流 热流
android·kotlin·flow
千雅爸爸4 小时前
Android MVVM demo(使用DataBinding,LiveData,Fresco,RecyclerView,Room,ViewModel 完成)
android