资讯专栏INFORMATION COLUMN

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

cuieney / 1849人阅读

摘要:问题为了防止销毁时异步任务仍然在进行所导致的内存泄露,我们都会在方法中去取消异步任务。总结层可以天然自动监视销毁,我一直在找寻如何优雅的自动取消异步任务,在目前来看是最佳的方案。协程绝对是最先进的,效率最高,最优雅的技术栈组合。

前提

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

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

问题

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

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

这样写看起来简洁统一,但并不是最优雅的,它有两个问题:

    需要我们手动取消,现在是9102年,不该啊

    不够灵活,它会傻瓜式的取消所有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() 
    fun login() {
        viewModelScope.launch {
            userData.postLoading()
            val result = "https://lixiaojun.xin/api/login".http(this).get>().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()
    }

它大概做了这样几个事情:

    给ViewModel增加了扩展属性viewModelScope,这样的好处是使用起来更方便。

    然后重写viewModelScope属性的getter方法,根据JOB_KEY取出CoroutineScope对象,目前来看JOB_KEY是固定的,后期可能增加多个Key。

    如果CoroutineScope对象为空,则创建CloseableCoroutineScope对象并通过setTagIfAbsent方法进行缓存,根据方法名能看出是线程安全的操作。

    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 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 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 ");if (mCleared) {
            closeWithRuntimeException(result);
        }
        return result;
    }

    /**
     * Returns the tag associated with this viewmodel and the specified key.
     */
    @SuppressWarnings("TypeParameterUnusedInFormals")
     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做了这样几个事情:

    提供一个Map来存储协程Scope对象,并提供了用来set和get的方法

    onCleared遍历所有的Scope对象,调用他们的close,取消协程的执行

整个执行过程跟我们之前的分析差不多,通过让父类来管理协程对象,并在onCleared中去干掉这些协程。

总结

VM层可以天然自动监视UI销毁,我一直在找寻如何优雅的自动取消异步任务,viewModelScope在目前来看是最佳的方案。

有些人说老子用MVP,不用MVVM。MVP架构下逻辑层和UI层交互有这样几个方式:

    为了解耦,定义接口互调,调来调去绕弯子

    用EventBus发消息,代码大的话会有几百个标识,很难管理

    Kotlin的协程和高阶函数也完全能够碾压它

如果3年前我会推荐你使用MVP,现在的话,相信我,用MVVM吧。ViewModel + Kotlin + 协程绝对是最先进的,效率最高,最优雅的技术栈组合。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/7845.html

相关文章

  • Android OkHttp 上最优雅设置baseUrl

    前言 RxHttp截止本文发表已经推广了4个礼拜,目前已经有了141个star,如下: showImg(https://user-gold-cdn.xitu.io/2019/5/20/16ad5f3b6d10d9be); 其中一文,Android 史上最优雅的实现文件上传、下载及进度的监听更是得到了大神刘皇叔微信公众号的推送,欢迎读者关注刘皇叔微信公众号「刘望舒」,每天都有精彩的文章推送,真的很棒...

    番茄西红柿 评论0 收藏0
  • 跨域解决方案(上最易懂)

    摘要:跨域总结跨域思路跨域解决方案一般分为两种前端解决,后端解决前端解决方案通过前端解决的思想就是,通过设置中间件把跨域的请求转发一下,其实就是反向代理,比如想要访问豆瓣的接口很会有跨域问题,但是如果请求的是就不存在跨域反向代理就是截取之后的请求 跨域总结 1.跨域思路 跨域解决方案一般分为两种:前端解决,后端解决 1.1 前端解决方案 通过前端解决的思想就是,通过设置中间件把跨域的请求转发...

    wh469012917 评论0 收藏0
  • 架构~微服务

    摘要:接下来继续介绍三种架构模式,分别是查询分离模式微服务模式多级缓存模式。分布式应用程序可以基于实现诸如数据发布订阅负载均衡命名服务分布式协调通知集群管理选举分布式锁和分布式队列等功能。 SpringCloud 分布式配置 SpringCloud 分布式配置 史上最简单的 SpringCloud 教程 | 第九篇: 服务链路追踪 (Spring Cloud Sleuth) 史上最简单的 S...

    xinhaip 评论0 收藏0
  • What? 你还不知道Kotlin Coroutine?

    摘要:例如,在方面它主要能够帮助你解决以下两个问题在主线程中执行耗时任务导致的主线程阻塞,从而使发生。提供主线程安全,同时对来自于主线程的网络回调磁盘操提供保障。在线程通过从数据库取数据,一旦数据返回,在主线程进行处理。 showImg(https://segmentfault.com/img/bVbuqpM?w=800&h=320); 今天我们来聊聊Kotlin Coroutine,如果你...

    yunhao 评论0 收藏0

发表评论

0条评论

cuieney

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<