美文网首页Android进阶之路
史上最优雅的在VM层取消Coroutine的方式

史上最优雅的在VM层取消Coroutine的方式

作者: cff70524f5cf | 来源:发表于2019-05-27 22:24 被阅读33次

    前提

    在Android MVVM模式,我使用了Jetpack包中的ViewModel来实现业务层,当然你也可以使用DataBinding,关于Android业务层架构的选择在这这里推荐一篇文章,文中有更详细的说明:Android开发中API层的最佳实践

    业务层无非就是网络请求,存储操作和数据处理操作,然后将处理好的数据更新给LiveData,UI层则自动更新。其中网络请求我是使用的协程来进行,而不是线程。

    问题

    为了防止UI销毁时异步任务仍然在进行所导致的内存泄露,我们都会在onCleared()方法中去取消异步任务。如何取消异步任务呢?懒惰的我们当然不会在每个ViewModel中去取消,而是去定义一个BaseVM类来存储每个Job对象,然后统一取消。代码如下:

    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中统一取消所有的协程,这样不就能实现自动销毁协程么。

    当我开始动手的时候,发现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来开启即可。

    原理分析:

    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 + 协程绝对是最先进的,效率最高,最优雅的技术栈组合。

    最后文末放上一个技术交流群:Android架构设计(185873940)

    群内有许多技术大牛,有任何问题,欢迎广大网友一起来交流,群内还不定期免费分享高阶Android学习视频资料和面试资料包~

    再推荐一篇文章,具体的架构视频,面试专题,学习笔记都在这篇文章中:“寒冬未过”,阿里P9架构分享Android必备技术点,让你offer拿到手软!

    偷偷说一句:群里高手如云,欢迎大家加群和大佬们一起交流讨论啊!

    相关文章

      网友评论

        本文标题:史上最优雅的在VM层取消Coroutine的方式

        本文链接:https://www.haomeiwen.com/subject/xmxltctx.html