细说协程零六、协程泄露及async异常的处理

Android项目开发中对协程的应用是比较灵活的,虽然依然是用****.luanch()来开启一段协程,通常指定Dispatchers.Main主线程;但也并不像我们在学习写demo的时候那样直接通过GlobalScope.launch { }或者CoroutineScope(Dispatchers.Main).launch {}去创建协程。

因为我们需要有效的避免或者解决协程泄露的问题,所谓协程泄露,其实就是线程泄露,本质上还是内存泄漏的一种。

一、有效避免协程泄露的方式

1、通过Job来处理

每当我们通过launch { }创建一个协程,就可以得到一个返回值job,然后我们在不需要协程的地方取消即可:

var job: Job = CoroutineScope(Dispatchers.Main).launch {
	......
}

为了防止协程泄露,上面的Job对象一定要初始化,这一步不能忘记了。CoroutineScope可以理解为对多个协程进行管理的集中地。也就是线程管理。包括取消。但是GlobalScope因为是全局性的,作用域比较广,没法实现上述功能。

然后我们在Activity或者Fragment的onDestroy()方法里面取消:

if (null != job) {
    job.cancel()
}

如此我们便可以有效规避协程泄露,但这并不是最好的实现方式,一是因为每次我们都要创建Job对象,然后取消,这样的操作好容易忘记。二是因为作用域的问题不同方式创建的Job,作用于不一样,生命周期不一样,并不能及时的响应JVM虚拟机的GC垃圾回收器的回收机制。因此,我们来看看第二个解决方案。

2、通过MainScope()来处理

创建一个在主线程上面运行的、在主线程上面启动所有协程的CoroutineScope对象,然后在其子类里面使用。然后他同样可以在onDestroy()方法里面调用cancel()方法取消,避免协程泄露。

因此我们可以在Activity或者Fragment的onCreate()方法里面创建CoroutineScope的对象:

val baseScope = MainScope()

使用的时候这样来使用:

baseScope.launch {
	......
}

然后统一是在onDestroy()方法里面取消CoroutineScope对象:

override fun onDestroy() {
    super.onDestroy()
    baseScope.cancel()
}

因为baseScope是创建在主线程的,在主线程启动协程的对象,因此这里我们可以不用像GlobalScope.launch(Dispatchers.Main){}和CoroutineScope(Dispatchers.Main).launch {}那样指定线程,直接省略掉“Dispatchers.Main”。

方案二很好的解决了方案一存在的问题,但还是感觉不好,毕竟每次我们都需要创建一个CoroutineScope的对象,重复写模板代码。对于当下流行MVVM开发模式来说,不够正宗不够简洁。那么有没有更好的方式呢?答案是有,而且还不止一个。

3、viewModelScope对象

viewModelScope是依赖于ViewModel的,而且最新的AndroidSDK的ViewModel.kt已经帮我们创建好了,因此可以直接在ViewModel里面调用或者通过ViewModel对象来调用。

class HomeViewModel :ViewModel() {
	suspend fun getBanner(): List<BannerBean> {
        viewModelScope.launch{
			......
        }
    }
}

ViewModel的生命周期跟Activity/Fragment的生命周期是同步的,因此,viewModelScope对象创建的协程系统会自动帮我们管理好,不用我们去关心创建、启动和销毁这些时间,我们只用专注于业务逻辑的开发即可。

4、lifecycleScope对象

lifecycleScope是依赖于lifecycleOwner的,而Activity/Fragment通常是实现了该接口,因此我们可以直接在Activity/Fragment调用lifecycleScope来创建、启动并管理协程,我们可以在任何生命周期中调用,优先推荐使用该方式。

lifecycleScope.launch {
	......
}

当天Android还为我们做了更加仔细的,针对每一个生命周期的创建协程的对象,比如:

表示onCreated()方法被调用之后这里的协程才会被创建并启动的launchWhenCreated

        lifecycleScope.launchWhenCreated {  }

表示onStarted()方法被调用之后这里的协程才会被创建并启动的launchWhenStarted

        lifecycleScope.launchWhenStarted {  }

表示onResumed()方法被调用之后这里的协程才会被创建并启动的launchWhenResumed

        lifecycleScope.launchWhenResumed {  }

上面三种方法如果是直接在Activity/frag的生命周期里面调用,那没多少意义, 因此我们还是优先推荐上面方案四的调用方式。

二、异常Exception以及async并发下的异常处理

同坐协程实现网络请求,并没有我们以前的Java环境下的CallBack回调对成功的结果解析;失败时对异常的处理。因为协程配合Retrofit环境下,他只是直接返回成功的结果,对失败的议程情况,则需要我们自己手动处理,这一点很是不太友善:

try {
	var listBanner = mViewModel.getBanner()
	updateBannerUI(listBanner)	
	var article = mViewModel.getHomeArticle(page.toString())
	updateArticleUI(article)
}catch (e: Exception) {
	mContext.handlerException(e)
}

这样,当我们的请求出现异常时,协程会跑出异常,然后我们通过try{}catch{}捕捉到并做进一步的处理。
getBanner()方法源码:

suspend fun requestBannerData() = apiService.getBanner().returnData()

当某一个需求,要分先后请求多个接口的时候,就可以是像下面这样来:虽然是在协程里面,但是请求是在同一个CoroutineScope.launch开启的协程里面,相当于在同一个线程里面发起两个甚至多个请求,这个时候请求是串行的:先发起并完成前面一个请求,然后在发起并完成下一个请求,以此类推,直到完成所有请求。这样的方式优点:可以完成有请求顺序要求的场景;缺点:因为是串行,效率比较低。
因此,如果我们没有请求顺序的要求,并要求高效并发,因此我们可以像下面这样用通过async{}来发起请求,通过async,我们可以在同一时间发起多个不同的请求,实现并发高效。

try {
	var deferBanner: Deferred<List<BannerBean>?> = async {
	    mViewModel.getBanner()
	}
	var listBanner = deferBanner.await()
	if (listBanner != null) updateBannerUI(listBanner)
	
	var mDeferred: Deferred<HomeArticleBean?> = async {
	    mViewModel.getHomeArticle(page.toString())
	}
	var article = mDeferred.await()
	if (article != null) updateArticleUI(article)
}catch (e: Exception) {
	mContext.handlerException(e)
}

需要注意几点,一是async并不是一个挂起函数,他只是创建一个协程,真正的挂起函数是.await()方法。二是要注意 async {}这个函数,他是有返回值的,类型为Deferred 。并且并不是马上就能拿到数据,只有当.await()方法运行结束之后,才能够拿到数据。三是上面的try{}catch (e: Exception) {},你会发现,并不能捕捉到请求过程中跑出来的异常。这是为什么呢?
这是因为,异常是在async {}里面抛出来的,而真正跑出异常的地方是.await()方法里面,.await()方法也是一个suspend关键字修饰的挂起函数。你在async {}的外面去捕捉,是捕捉不到的。有效的方法是在async {}里面去捕捉异常:

var deferBanner: Deferred<List<BannerBean>?> = async {
    try {
        mViewModel.getBanner()
    } catch (e: Exception) {
        mContext.handlerException(e)
        null
    }
}
var listBanner = deferBanner.await()
if (listBanner != null) updateBannerUI(listBanner)

var mDeferred: Deferred<HomeArticleBean?> = async {
    try {
        mViewModel.getHomeArticle(page.toString())
    } catch (e: Exception) {
        mContext.handlerException(e)
        null
    }
}
var article = mDeferred.await()
if (article != null) updateArticleUI(article)

注意上面的代码里面try{}catch (e: Exception) {}捕捉异常的位置,不能加错了,否则异常捕捉失败。

网上有很多文章说async {}的异常可以在.await()方法这里捕捉,也就是下面这样:

var mDeferred: Deferred<HomeArticleBean?> = async {
    mViewModel.getHomeArticle(page.toString())
}
try {
    var article = mDeferred.await()
if (article != null) updateArticleUI(article)
} catch (e: Exception) {
    mContext.handlerException(e)
    null
}

我可以直接告诉你,这是扯淡的,无法捕捉到异常,app会直接崩溃掉,不信可以试试。

三、几个题外话:

1、RxJava与kotlin协程比较

1、都可以切换线程
2、都不需要嵌套
3、RxJava需要回调和包装,而协程不需要
4、在完成相同功能下,协程比RxJava逻辑更简单,代码量更少,RxJava更复杂
RxJava的调用是链式调用,也叫流式调用,再配合回调,这样在结构上就简化了很多,这样功能就可以得到增强,逻辑更复杂
而协程更狠,直接连回调都省了,没有回调,那么操作符也就直接不要了。更节省代码和逻辑。

RxJava与协程怎么选:
需要注意的是,协程的出现并不是为了取代RxJava,而是为了解决以及简化并发场景的问题,因此协程并不是为了取代RxJava。另外协程在性能方面相比RxJava稍有不及。

2、AsyncTask内存泄露

AsyncTask会造成内存泄露的原因:

首先,Java虚拟机的垃圾回收器GC Root在回收资源的时候,会同时判断对象是否又被继续使用,以及该对象内部是否有活跃的对象存在。只有当这两个条件同时满足,也就是既没有被继续使用,其内部也没有活跃的对象在运行的时候才会被回收。

然后,AsyncTask对象被四大组件初始化之后,他的内部就持有四大组件的对象了,另外,AsyncTask的内部还有一个线程,这个线程很有可能是会一直活跃的。因此当GC回收AsyncTask对象的时候,尽管Activity里面AsyncTask对象已经不再继续被使用,但是AsyncTask内部有一个活跃的线程。这就导致了AsyncTask对象无法被GC回收,那么AsyncTask对象持有的Activity也就无法被回收,从而造成内存泄漏。

因此,AsyncTask引起内存泄露有两方面的原因,一是持有外部类(比如AsyncTask被写在Activity内部作为内部类存在的时候)的引用,二是AsyncTask内部的线程一直活跃。AsyncTask内部活跃的线程是造成GC无法回收的最根本原因。

线程,活跃的线程,还在运行的线程,是造成GC无法回收的最根本原因。

这也从侧面解释了为什么内部类或者匿名内部类并不是造成内存泄露的原因。你见过setOnClickListener造成过内存泄漏吗?

现在再说回协程泄露。协程泄露是问题吗?肯定是,那怎么优化处理?就是不再是用协程的时候,调用cancel()方法取消它。具体实现方面,前面已经讲得很清楚。

3、协程的delay()和Thread.sleep()谁的性能更好?

单单论线程的话,delay()性能会好些。因为Thread是一个单一的对象,每个线程调用一次Thread.sleep()那么这个线程就会休眠。如果我调用10000个,那么就是一万个线程休眠。但是因为delay()内部做了很多工作,实现了对Excutor线程池的管理,因此其内部可能只有指定的几个线程处于休眠状态,而不是启动多少个线程,就有多少个线程休眠。但是如果我们把Thread换成Java的Excutor再来和delay()比较,那么其实也不相上下了。

4、什么时候需要切线程?

耗时操作:比如IO以及CPU密集计算、需要放在后台的任务
工作比较特殊:放在指定线程——————一般是主线程;比如Android里面UI线程更新UI。

CoroutineScope(Dispatchers.Main).launch {
	/**这里,通过try{}} catch (e: Exception) {}捕捉到网络请求时抛出来的异常。特别强调一定不要忘记了*/
	try {
		......
	} catch (e: Exception) {
		handlerException(e)
	}
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值