前言
相信各位Android开发同学们对于协程的使用已经比较熟悉了。大多数情况下我们会使用viewModelScope或者lifecycleScope来开启协程。
因为viewModelScope和lifecycleScope分别绑定了ViewModel和LifecycleOwnner的生命周期。 通过使用它们可以在ViewModel或者LifecycleOwner生命周期结束时,主动取消协程执行,从而防止协程泄漏。
但是通过使用viewModelScope和lifecycleScope真的解决所有的协程的泄漏问题吗?在实际开发中为了防止协程泄漏又有哪些手段,接下来从一个案例出发,开始今天的学习。
案例一
fun testLongTask() {
viewModelScope.launch(Dispatchers.IO) {
var i = 0L
while (true) {
i++
if (i % 100000000L == 0L) {
Log.i("TAG", "i=$i ThreadName=${Thread.currentThread().name} ${isActive}")
}
}
}
}
当ViewModel生命周期结束时,ViewModel会调用clear()方法,进而调用到
CloseableCoroutineScope对象的close方法,从而取消viewModelScope代码块的执行。
按照上述的代码分析,当ViewModel销毁时,CPU将不再执行该代码块的内容。
代码运行结果:
很明显,当ViewModel销毁时,协程状态isActive为false,该代码块仍然在执行,也就是说上述代码发生了泄漏,依然在占据CPU资源。
不过我们仔细分析下代码,就知道为何会出现上面的运行结果了。
首先上面是运行在IO线程上的代码块,因为不涉及挂起函数,所以会一直在该IO线程执行。又因为是死循环,所以会一直占着IO线程资源不放,从而导致该代码块无法停止。
所以针对上述泄漏有个简单的解决方法,就是可以使用isActive作为是否继续执行的条件,如while(isActive), 这样就能在协程被取消时,结束相应代码块的执行了。
案例二
fun testLongTask() {
viewModelScope.launch(Dispatchers.IO) {
var i = 0L
while (true) {
i++
if (i % 100000000L == 0L) {
Log.i("TAG", "i=$i ThreadName=${Thread.currentThread().name} ${isActive}")
delay(1) //相比案例一,多了这句代码,是个挂起函数,表示延时1ms
}
}
}
}
代码执行结果:
很神奇吧,加完这句delay(1),就能够退出循环,这是怎么做到的呢?根据以往的开发经验,如果有未捕获的异常抛出,会导致线程崩溃退出运行。
所以有可能是delay这个挂起方法抛出异常导致死循环的退出。
接下来把delay方法try catch一下, 运行结果如下:
果然跟我猜想的一样,delay会协程被cancel时,调用会抛出JobCancellationException,从而退出线程的执行。当该异常被捕获,则代码块则会不停地执行下去。
接下来看下delay的异常是在哪里抛出来的
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
// if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
//代码块0
if (timeMillis < Long.MAX_VALUE) {
//去执行延时任务
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
}
public suspend inline fun <T> suspendCancellableCoroutine(
crossinline block: (CancellableContinuation<T>) -> Unit
): T =
suspendCoroutineUninterceptedOrReturn { uCont ->
val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
/*
* For non-atomic cancellation we setup parent-child relationship immediately
* in case when `block` blocks the current thread (e.g. Rx2 with trampoline scheduler), but
* properly supports cancellation.
*/
cancellable.initCancellability()
block(cancellable) //传入cancellable,调用代码块0
cancellable.getResult() //代码1
}
// CancellableContinuationImpl.kt
internal fun getResult(): Any? {
//省略无关代码
if (resumeMode.isCancellableMode) {
val job = context[Job]
if (job != null && !job.isActive) {
val cause = job.getCancellationException()
cancelCompletedResult(state, cause)
throw recoverStackTrace(cause, this) //抛异常
}
}
return getSuccessfulResult(state)
}
很明显,delay使用到了suspendCancellableCoroutine这个函数,在该函数中会执行到代码1的位置,在该位置的getResult()函数中,发现当协程处于取消状态时,则会抛异常。
suspendCancellableCoroutine
suspendCancellableCoroutine可以将回调函数转化为协程,而且其会暴露一个CancellableContinuation对象,拿到continuation对象, 就可以用 resume、resumeWithException 来处理回调 和抛出 CancellationException 异常。
而且suspendCancellableCoroutine还支持取消回调
以下是suspendCancellableCoroutine在Retrofit框架的经典使用场景
suspend fun <T : Any> Call<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel() //当协程代表块被取消时,会收到回调,从而取消okhttp请求
}
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
val invocation = call.request().tag(Invocation::class.java)!!
val method = invocation.method()
val e = KotlinNullPointerException("Response from " +
method.declaringClass.name +
'.' +
method.name +
" was null but response body type was declared as non-null")
continuation.resumeWithException(e)
} else {
continuation.resume(body)
}
} else {
continuation.resumeWithException(HttpException(response))
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
通过上面的例子可以看出,suspendCancellableCoroutine很适合用来支持取消操作的业务逻辑场景。值得注意的是当在使用它时,记得调用invokeOnCancellation进行监听,在协程cancel时,进行相应的资源清理,否则有可能发生泄漏。
小结
本文先通过案例一来分析Kotlin协程可能泄漏的场景以及针对该场景的解决方案。接着通过案例二分析引出了suspendCancellableCoroutine这个函数,并通过Retrofit的源码例子演示了它的使用场景。通过本文的分析,我们知道协程的取消是协作型的,并不是我们相应的scope.cancel就能取消所有的操作。所以我们在协程做一些耗时任务时,进行取消监听,尽早地释放资源,防止造成相应的泄漏,从而提高代码健壮性。
您若喜欢,请点赞、关注,您的鼓励是我前进的动力