关于Kotlin 协程、 ViewModel 相关知识,在本篇文章中不做介绍。
在了解这个问题之前,需先了解 ViewModelScope 是什么?
Tips 使用 ViewModelScope 前需添加依赖:
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0'
package androidx.lifecycle
...省略倒包...
private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"
/**
* 与此[ViewModel]绑定的[CoroutineScope]
* [CoroutineScope] tied to this [ViewModel].
* 清除ViewModel时,即调用[ViewModel.onCleared]时,将取消此作用域
* This scope will be canceled when ViewModel will be cleared, i.e [ViewModel.onCleared] is called
*
* This scope is bound to
* [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
*/
public val ViewModel.viewModelScope: CoroutineScope
get() {
// 先尝试从缓存中获取 tag 为 JOB_KEY 的 CoroutineScope 对象
// 若命中,则返回 viewModelScope
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
// 未命中缓存,则通过 setTagIfAbsent() 添加到 ViewModel 中
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 对象是 ViewModel 的一个扩展属性。
并且根据 viewModelScope 的注释我们知道了:在 ViewModel 被清除,即调用 [ViewModel.onCleared] 时,将取消此作用域。
问题来了:
- ViewModel 什么时候被清除?
- 为什么调用 [ViewModel.onCleared] 时,viewModelScope 会被取消。
带着问题看源码事半功倍!
关于第一个问题 “ViewModel 什么时候被清除?” 在后续 ViewModel 的文章中进行介绍
我们来看第二个问题 “为什么调用 [ViewModel.onCleared] 时,viewModelScope 会被取消。”
去看看 ViewMode 的 onCleared 方法就知道了。
public abstract class ViewModel {
// Can't use ConcurrentHashMap, because it can lose values on old apis (see b/37042460)
@Nullable
private final Map<String, Object> mBagOfTags = new HashMap<>();
private volatile boolean mCleared = false;
/**
* 当不再使用此ViewModel并将其销毁时,将调用此方法。
* This method will be called when this ViewModel is no longer used and will be destroyed.
* <p>
* 当ViewModel观察到一些数据并且需要清除对的订阅时,它非常有用防止此ViewModel泄漏。
* It is useful when ViewModel observes some data and you need to clear this subscription to
* prevent a leak of this ViewModel.
*/
@SuppressWarnings("WeakerAccess")
protected void onCleared() {
}
@MainThread
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);
}
}
}
onCleared();
}
@SuppressWarnings("unchecked")
<T> T setTagIfAbsent(String key, T newValue) {
T previous;
synchronized (mBagOfTags) {
previous = (T) mBagOfTags.get(key);
if (previous == null) {
mBagOfTags.put(key, newValue);
}
}
T result = previous == null ? newValue : previous;
if (mCleared) {
closeWithRuntimeException(result);
}
return result;
}
@SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
<T> T getTag(String key) {
if (mBagOfTags == null) {
return null;
}
synchronized (mBagOfTags) {
return (T) mBagOfTags.get(key);
}
}
private static void closeWithRuntimeException(Object obj) {
if (obj instanceof Closeable) {
try {
((Closeable) obj).close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
复制代码
onCleared() 方法是空方法??? 说好的 “在 ViewModel 被清除,即调用[ViewModel.onCleared] 时,将取消此作用域。” 呢? 没有代码怎么取消?
看 ViewModel 的源码发现 onCleared() 被 clear() 调用了,那我们来看看 clear() 做了什么:
@MainThread
final void clear() {
// 标记当前 ViewModel 已经被清除
mCleared = true;
// 判断 mBagOfTags 是否为空,不为空则遍历 map 的 value 并且调用了 closeWithRuntimeException() 方法
if (mBagOfTags != null) {
synchronized (mBagOfTags) {
for (Object value : mBagOfTags.values()) {
// see comment for the similar call in setTagIfAbsent
closeWithRuntimeException(value);
}
}
}
onCleared();
}
复制代码
我们再看看 mBagOfTags 是干什么的:
@Nullable
private final Map<String, Object> mBagOfTags = new HashMap<>();
复制代码
它是一个 Map ,找一下什么时候调用 mBagOfTags.put() :
/**
* 设置与此viewmodel关联的标记和键。
* Sets a tag associated with this viewmodel and a key.
* 如果给定的 newValue 是 Closeable,一旦 clear(),它就会关闭。
* If the given {@code newValue} is {@link Closeable},
* it will be closed once {@link #clear()}.
* <p>
* 如果已经为给定的键设置了一个值,则此调用不执行任何操作,并且返回当前关联的值,给定的 newValue 将被忽略
* If a value was already set for the given key, this calls do nothing and
* returns currently associated value, the given {@code newValue} would be ignored
* <p>
* 如果ViewModel已经被清除,那么将对返回的对象调用close(),如果它实现了 closeable。同一个对象可能会收到多个close调用,因此方法应该是幂等的。
* If the ViewModel was already cleared then close() would be called on the returned object if
* it implements {@link Closeable}. The same object may receive multiple close calls, so method
* should be idempotent.
*/
@SuppressWarnings("unchecked")
<T> T setTagIfAbsent(String key, T newValue) {
// 原值
T previous;
synchronized (mBagOfTags) {
// 尝试获取是否命中 key
previous = (T) mBagOfTags.get(key);
if (previous == null) {
// 未命中,则 put 到 mBagOfTags 中
mBagOfTags.put(key, newValue);
}
}
// 返回值赋值
T result = previous == null ? newValue : previous;
// 如果此 viewModel 被标记清除
if (mCleared) {
// 我们可能会在同一个对象上多次调用close(),
// 但是Closeable接口要求close方法是幂等的:“如果流已经关闭,那么调用这个方法就没有效果。”
// It is possible that we'll call close() multiple times on the same object, but
// Closeable interface requires close method to be idempotent:
// "if the stream is already closed then invoking this method has no effect." (c)
// 调用 Closeable 的 close 方法
closeWithRuntimeException(result);
}
return result;
}
复制代码
setTagIfAbsent() 方法有点熟悉?原来它在获取 viewModelScope 对象时候被调用了。
得知 viewModelScope 对象缓存在 Map 类型的对象 mBagOfTags 中。
setTagIfAbsent 的注释上说 T newValue 是实现了 Closeable 接口的。
再回顾下 viewModelScope 怎么获取的:
private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"
/**
* 与此[ViewModel]绑定的[CoroutineScope]
* [CoroutineScope] tied to this [ViewModel].
* 清除ViewModel时,即调用[ViewModel.onCleared]时,将取消此作用域
* This scope will be canceled when ViewModel will be cleared, i.e [ViewModel.onCleared] is called
*
* This scope is bound to
* [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
*/
public val ViewModel.viewModelScope: CoroutineScope
get() {
// 先尝试从缓存中获取 tag 为 JOB_KEY 的 CoroutineScope 对象
// 若命中,则返回 viewModelScope
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
// 未命中缓存,则通过 setTagIfAbsent() 添加到 ViewModel 中
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 第一次被调用时,会调用 setTagIfAbsent(JOB_KEY,CloseableCoroutineScope) 进行缓存。
看下 CloseableCoroutineScope 类,实现了 Closeable 接口,并且在 close() 中进行了协程作用域 coroutineContext 对象的取消操作。
至此,我们知道了 viewModelScope 对象怎么添加到 ViewModel 里
并且在 ViewModel 被清除时 viewModelScope 会被取消。
第2个问题 为什么调用 [ViewModel.onCleared] 时,viewModelScope 会被取消。 解决!