Android食用指南之协程的创建、取消与超时(二)

1,263 阅读5分钟

Android 协程系列博客:

协程Coroutines基础知识(一)

协程的创建、取消与超时(二)

父子协程和挂起函数(三)

经过上篇文章的客(ji)观(li)分(chui)析(peng),相信没有接触过协程的盆友们已经动心了,那么接下来我们就探讨一下协程的基本用法。

本博客对Kotlin协程的一些基本用法做了一些梳理,如有巧合,纯属雷同。😹

PS:以下示例均使用GlobalScope,但在实际的Android开发中,不建议直接使用GlobalScope。

一、协程的创建

CoroutineScope是GlobalScope的父接口,也是协程作用域的顶级接口,该接口里面只定义了一个名为coroutineContext的协程上下文,而协程的launch、async等方法都是通过Kotlin扩展函数的方式增加的。

启动协程的方法很简单,如下即可:

var job = GlobalScope.launch {
     //do something
}

让我们跟踪一下launch方法的具体实现:

public fun CoroutineScope.launch( //为CoroutineScope拓展一个名为launch的方法
    context: CoroutineContext = EmptyCoroutineContext,//协程上下文,可选参数
    start: CoroutineStart = CoroutineStart.DEFAULT,//协程启动项,可选参数
    block: suspend CoroutineScope.() -> Unit//类型为CoroutineScope返回类型为Unit(即Java的void)的挂起函数
): Job //该方法返回一个Job类型的返回值
{
    val newContext = newCoroutineContext(context)
    val coroutine = 
        if (start.isLazy){//判断启动项是否为懒加载类型,根据启动项,创建不同类型的协程对象
            LazyStandaloneCoroutine(newContext, block) 
        }else{
            StandaloneCoroutine(newContext, active = true)
        }
    coroutine.start(start, coroutine, block)//启动协程
    return coroutine
}

通过跟踪源码我们终于知道为啥在launch代码块中可以调用suspend函数了,因为这个代码块本身就是一个suspend函数。

launch的第一个参数是用于调度协程的,官方已在Dispatchers类中默认提供四种调度器的实现,分别为: Default

Main

Unconfined

IO

我们可以分别把这四种调度器在协程创建的时候传入,查看协程运行的线程信息:

runBlocking<Unit> {//阻塞代码块,防止协程结束前当前线程已结束
    launch(Dispatchers.Default) {
        println("Default 运行在 : ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Unconfined) {
        println("Unconfined 运行在 : ${Thread.currentThread().name}")
    }
    launch(Dispatchers.IO) {
        println("IO 运行在 : ${Thread.currentThread().name}")
    }
    
    //这里并没有对Main进行打印,在Java代码中调用Main会抛出异常
}

输出结果为:

Default 运行在 : DefaultDispatcher-worker-1 @coroutine#2
Unconfined 运行在 : main @coroutine#3
IO 运行在 : DefaultDispatcher-worker-1 @coroutine#4

当然,我们也可以自己实现一个调度器,以满足在不同场景下的试用。

image.png

说完第一个参数,我们再说说第二个:CoroutineStart

该枚举里面为我们定义了四种启动模式

DEFAULT默认,立即对协程进行调度

LAZY延迟启动协程,只在需要的时候启动

ATOMIC让协程具有原子性,立即调度且无法对该协程进行取消操作

UNDISPATCHED立即启动协程直到第一个挂起点,挂起的恢复的线程根据第一个参数调度器分派

二、协程的取消

分析完协程的创建之后,让我们看下如何取消一个协程。

协程创建的launch方法会有一个Job类型的返回值,我们在Android开发的时候需要注意,在界面的生命周期结束的时候,需要对正在执行耗时操作的协程进行取消操作,否则会发生内存泄漏的问题,而协程的取消就是通过Job来处理的。

Job.cancel()可以取消协程的作业。

Job.join()是等待协程作业结束。

Job.cancelAndJoin()是合并了cancel和join的操作。

但是有时候我们调用cancel方法的时候,协程并不一定被cancel掉,因为:

image.png

image.png

可以看出,协程并没有被正确的取消掉。

为了保证协程能够正确的取消,官方提供了两种方式来解决这个问题:

1、在计算方法中使用yield()方法。

image.png 2、在计算方法执行时检测isActive状态,这个跟Thread的interrupt有些类似。

image.png

不可取消的代码块

我们在协程中可以使用try{}finally{}来包裹代码块,让必须执行的操作放在finally{}代码块中,用法和Java中的try{}finally{}一致,为了能够让一些必须进行的操作不可被取消,如关闭IO流,Kotlin提供了withContext(NonCancellable) {},就算协程被调用了cancel方法取消执行,也能确保finally里面的操作能够正常执行完毕。

image.png

三、协程的超时

如果说现在我有这么一个场景,我需要在1000ms内获取一个操作的结果,如果这个结果没有办法在1000ms内返回,我就不再需要了(没错,就是这么任性),那我们应该如何处理这个需求?

withTimeout

image.png 我们可以使用withTimeout来包裹我们的代码块轻松实现协程的超时问题,不过这个方法会抛出一个kotlinx.coroutines.TimeoutCancellationException的异常,如果不想增加异常处理的代码,可以使用下面的方法。

withTimeoutOrNull

image.png

这个方法已经为我们处理了上面方法抛出的异常。

关于超时取消还有个地方需要特别注意,就是一些需要关闭的操作一定要在finally中进行处理,否则会产生泄漏的问题,具体处理方法可以参考不可取消的代码块

以上就是关于协程创建、取消和超时的相关操作,使用起来还是相当简单的,赶快上号试试吧~

image.png

如果该文章能够帮到你,欢迎点赞和评论,一起交流探讨~