Android 面试总结 - viewModelScope 什么时候关闭的?

2,290 阅读4分钟

这是我参与更文挑战的第2天,活动详情查看: 更文挑战

关于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] 时,将取消此作用域。

问题来了:

  1. ViewModel 什么时候被清除?
  2. 为什么调用 [ViewModel.onCleared] 时,viewModelScope 会被取消。

带着问题看源码事半功倍!

关于第一个问题 “ViewModel 什么时候被清除?” 在后续 ViewModel 的文章中进行介绍

我们来看第二个问题 “为什么调用 [ViewModel.onCleared] 时,viewModelScope 会被取消。

去看看 ViewModeonCleared 方法就知道了。

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 会被取消。 解决!