史上最優雅的在VM層取消Coroutine的方式

前提

在Android MVVM模式,我使用了Jetpack包中的ViewModel來實現業務層,固然你也可使用DataBinding,關於Android業務層架構的選擇我在這篇文章中有更詳細的說明:Android開發中API層的最佳實踐java

業務層無非就是網絡請求,存儲操做和數據處理操做,而後將處理好的數據更新給LiveData,UI層則自動更新。其中網絡請求我是使用的協程來進行,而不是線程。android

問題

爲了防止UI銷燬時異步任務仍然在進行所致使的內存泄露,咱們都會在onCleared()方法中去取消異步任務。如何取消異步任務呢?懶惰的咱們固然不會在每一個ViewModel中去取消,而是去定義一個BaseVM類來存儲每一個Job對象,而後統一取消。代碼以下:api

open class BaseVM : ViewModel(){
    val jobs = mutableListOf<Job>()
    override fun onCleared() {
        super.onCleared()
        jobs.forEach { it.cancel() }
    }
}
//UserVM
class UserVM : BaseVM() {
    val userData = StateLiveData<UserBean>() 
    fun login() {
        jobs.add(GlobalScope.launch {
            userData.postLoading()
            val result = "https://lixiaojun.xin/api/login".http(this).get<HttpResult<UserBean>>().await()
            if (result != null && result.succeed) {
                userData.postValueAndSuccess(result.data!!)
            } else {
                userData.postError()
            }
        })
    }
    
    fun register(){ 
        //...
    }
}
複製代碼

這樣寫看起來簡潔統一,但並非最優雅的,它有兩個問題:緩存

  1. 須要咱們手動取消,如今是9102年,不應啊
  2. 不夠靈活,它會傻瓜式的取消全部VM的異步任務,若是咱們某個VM的某個異步任務的需求是即便UI銷燬也要在後臺進行(好比後臺上傳數據),那這個就不知足需求了

我所期待最好的樣子是: 咱們只需專一地執行異步邏輯,它可以自動的監視UI銷燬去自動幹掉本身,讓我能多一點時間打Dota。安全

分析

有了美好的願景後來分析一下目前代碼存在的問題,咱們使用GlobalScope開啓的協程並不能監視UI生命週期,若是讓父ViewModel負責管理和產生協程對象,子ViewModel直接用父類產生的協程對象開啓協程,而父ViewModel在onCleared中統一取消全部的協程,這樣不就能實現自動銷燬協程麼。markdown

當我開始動手的時候,發現Jetpack的ViewModel模塊最新版本正好增長了這個功能,它給每一個ViewModel增長了一個擴展屬性viewModelScope,咱們使用這個擴展屬性來開啓的協程就能自動在UI銷燬時幹掉本身。網絡

首先,添加依賴,注意必定要是androidx版本的哦:架構

def lifecycle_version = "2.2.0-alpha01"
// ViewModel and LiveData
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
複製代碼

重寫上面的代碼:異步

open class BaseVM : ViewModel(){
    override fun onCleared() {
        super.onCleared()
        //父類啥也不用作
    }
}
//UserVM
class UserVM : BaseVM() {
    val userData = StateLiveData<UserBean>() 
    fun login() {
        viewModelScope.launch {
            userData.postLoading()
            val result = "https://lixiaojun.xin/api/login".http(this).get<HttpResult<UserBean>>().await()
            if (result != null && result.succeed) {
                userData.postValueAndSuccess(result.data!!)
            } else {
                userData.postError()
            }
        }
    }
}
複製代碼

這個代碼就足夠優雅了,不再用關心何時UI銷燬,協程會關心,不再會有內存泄露產生。若是咱們但願某個異步任務在UI銷燬時也執行的話,仍是用GlobalScope來開啓便可。ide

原理分析:

viewModelScope的核心代碼以下:

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

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun close() {
        coroutineContext.cancel()
    }
複製代碼

它大概作了這樣幾個事情:

  1. 給ViewModel增長了擴展屬性viewModelScope,這樣的好處是使用起來更方便。
  2. 而後重寫viewModelScope屬性的getter方法,根據JOB_KEY取出CoroutineScope對象,目前來看JOB_KEY是固定的,後期可能增長多個Key。
  3. 若是CoroutineScope對象爲空,則建立CloseableCoroutineScope對象並經過setTagIfAbsent方法進行緩存,根據方法名能看出是線程安全的操做。
  4. CloseableCoroutineScope類是一個自定義的協程Scope對象,接收一個協程對象,它只有一個close()方法,在該方法中取消協程

而後看下ViewModel的核心代碼:

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;

    @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();
    }
    //線程安全的進儲協程對象
    <T> T setTagIfAbsent(String key, T newValue) {
        T previous;
        synchronized (mBagOfTags) {
            //noinspection unchecked
            previous = (T) mBagOfTags.get(key);
            if (previous == null) {
                mBagOfTags.put(key, newValue);
            }
        }
        T result = previous == null ? newValue : previous;
        if (mCleared) {
            closeWithRuntimeException(result);
        }
        return result;
    }

    /** * Returns the tag associated with this viewmodel and the specified key. */
    @SuppressWarnings("TypeParameterUnusedInFormals")
    <T> T getTag(String key) {
        //noinspection unchecked
        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);
            }
        }
    }
}

複製代碼

正如咱們所想,ViewModel作了這樣幾個事情:

  1. 提供一個Map來存儲協程Scope對象,並提供了用來set和get的方法
  2. onCleared遍歷全部的Scope對象,調用他們的close,取消協程的執行

整個執行過程跟咱們以前的分析差很少,經過讓父類來管理協程對象,並在onCleared中去幹掉這些協程。

總結

VM層能夠自然自動監視UI銷燬,我一直在找尋如何優雅的自動取消異步任務,viewModelScope在目前來看是最佳的方案。

有些人說老子用MVP,不用MVVM。MVP架構下邏輯層和UI層交互有這樣幾個方式:

  1. 爲了解耦,定義接口互調,調來調去繞彎子
  2. 用EventBus發消息,代碼大的話會有幾百個標識,很難管理
  3. Kotlin的協程和高階函數也徹底可以碾壓它

若是3年前我會推薦你使用MVP,如今的話,相信我,用MVVM吧。ViewModel + Kotlin + 協程絕對是最早進的,效率最高,最優雅的技術棧組合。

相關文章
相關標籤/搜索