Kotlin协程实战进阶之筑基篇2

电子说

1.2w人已加入

描述

launch 创建子协程

通过launch在一个协程中启动子协程,可以根据业务需求创建一个或多个子协程:

fun launchTest3() {
    print("start")
    GlobalScope.launch {
        delay(1000)
        print("CoroutineScope.launch")

        //在协程内创建子协程
        launch {
            delay(1500)//1.5秒无阻塞延迟(默认单位为毫秒)
            print("launch 子协程")
        }
    }
    print("end")
}

打印数据如下:

kotlin

launch3.gif

async

async类似于launch,都是创建一个不会阻塞当前线程的新的协程。它们区别在于:async的返回是Deferred对象,可通过Deffer.await()等待协程执行完成并获取结果,而 launch 不行。常用于并发执行-同步等待和获取返回值的情况。

public fun  CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred
  • context: 协程的上下文,同launch
  • start:  协程启动模式,同launch
  • block:  协程代码,同launch
  • Deferred: 协程构建函数的返回值,继承自Job,一个有结果的Job,可通过Deffer.await()等待协程执行完成并获取结果。
await 获取返回值
//获取返回值
fun asyncTest1() {
    print("start")
    GlobalScope.launch {
        val deferred: Deferred<String> = async {
            //协程将线程的执行权交出去,该线程继续干它要干的事情,到时间后会恢复至此继续向下执行
            delay(2000)//2秒无阻塞延迟(默认单位为毫秒)
            print("asyncOne")
            "HelloWord"//这里返回值为HelloWord
        }

        //等待async执行完成获取返回值,此处并不会阻塞线程,而是挂起,将线程的执行权交出去
        //等到async的协程体执行完毕后,会恢复协程继续往下执行
        val result = deferred.await()
        print("result == $result")
    }
    print("end")
}

上面例子中返回对象Deferred, 通过函数await()获取结果值。打印数据如下:

kotlin

async1.gif

注意:await() 不能在协程之外调用,因为它需要挂起直到计算完成,而且只有协程可以以非阻塞的方式挂起。所以把它放到协程中。

async 并发

当在协程作用域中使用async函数时可以创建并发任务:

fun asyncTest2() {
    print("start")
    GlobalScope.launch {
        val time = measureTimeMillis {//计算执行时间
            val deferredOne: Deferred<Int> = async {
                delay(2000)
                print("asyncOne")
                100//这里返回值为100
            }

            val deferredTwo: Deferred<Int> = async {
                delay(3000)
                print("asyncTwo")
                200//这里返回值为200
            }

            val deferredThr: Deferred<Int> = async {
                delay(4000)
                print("asyncThr")
                300//这里返回值为300
            }

            //等待所有需要结果的协程完成获取执行结果
            val result = deferredOne.await() + deferredTwo.await() + deferredThr.await()
            print("result == $result")
        }
        print("耗时 $time ms")
    }
    print("end")
}

打印数据如下:

kotlin

async2.gif

上面的代码就是一个简单的并发示例,async是不阻塞线程的,也就是说上面三个async{}异步任务是同时进行的。通过await()方法可以拿到async协程的执行结果,可以看到两个协程的总耗时是远少于9秒的,总耗时基本等于耗时最长的协程。

1.Deferred集合还可以使用awaitAll()等待全部完成;

2.如果Deferred不执行await()async内部抛出的异常不会被logCattryCatch捕获, 但是依然会导致作用域取消和异常崩溃; 但当执行await时异常信息会重新抛出。

3.惰性并发,如果将async函数中的启动模式设置为CoroutineStart.LAZY懒加载模式时则只有调用Deferred对象的await时(或者执行async.satrt())才会开始执行异步任务。

launch构建器适合执行 "一劳永逸" 的工作,意思就是说它可以启动新协程而不需要结果返回;async构建器可启动新协程并允许您使用一个名为await的挂起函数返回result,并且支持并发。另外launchasync之间的很大差异是它们对异常的处理方式不同。如果使用async作为最外层协程的开启方式,它期望最终是通过调用 await 来获取结果 (或者异常),所以默认情况下它不会抛出异常。这意味着如果使用 async启动新的最外层协程,而不使用await,它会静默地将异常丢弃。

2.Job & Deferred

反观线程,java平台上很明确地给出了线程的类型Thread,我们也需要一个这样的类来描述协程,它就是Job。它的API设计与Java的Thread殊途同归。

Job

Job 是协程的句柄。如果把门和门把手比作协程和Job之间的关系,那么协程就是这扇门,Job就是门把手。意思就是可以通过Job实现对协程的控制和管理。

从上面可以知道Joblaunch构建协程返回的一个协程任务,完成时是没有返回值的。可以把Job看成协程对象本身,封装了协程中需要执行的代码逻辑,协程的操作方法都在Job身上。Job具有生命周期并且可以取消,它也是上下文元素,继承自CoroutineContext

这里列举Job几个比较有用的函数:

public interface Job : CoroutineContext.Element {
    //活跃的,是否仍在执行
    public val isActive: Boolean

    //启动协程,如果启动了协程,则为true;如果协程已经启动或完成,则为false
    public fun start(): Boolean

    //取消Job,可通过传入Exception说明具体原因
    public fun cancel(cause: CancellationException? = null)

    //挂起协程直到此Job完成
    public suspend fun join()

    //取消任务并等待任务完成,结合了[cancel]和[join]的调用
    public suspend fun Job.cancelAndJoin() 

    //给Job设置一个完成通知,当Job执行完成的时候会同步执行这个函数
    public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
}

Thread相比,Job同样有join(),调用时会挂起(线程的join()则会阻塞线程),直到协程完成;它的cancel()可以类比Threadinterrupt(),用于取消协程;isActive则是可以类比ThreadisAlive(),用于查询协程是否仍在执行。

Job是一个接口类型,它具有以下三种状态:

状态 说明
isActive 活跃的。当Job处于活动状态时为true,如果Job已经开始,但还没有完成、也没有取消或者失败,则是处于active状态。
isCompleted 已完成。当Job由于任何原因完成时为true,已取消、已失败和已完成Job都是被视为完成状态。
isCancelled 已退出。当Job由于任何原因被取消时为true,无论是通过显式调用cancel或这因为它已经失败亦或者它的子或父被取消,都是被视为已退出状态。

这里模拟一个无限循环的协程,当协程是活跃状态时每秒钟打印两次消息,1.2秒后取消协程:

fun jobTest() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default){
        var nextPrintTime = startTime
        var i = 0

        while (isActive) {//当job是活跃状态继续执行
            if (System.currentTimeMillis() >= nextPrintTime) {//每秒钟打印两次消息
                print("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500
            }
        }
    }

    delay(1200)//延迟1.2s
    print("等待1.2秒后")


    //job.join()
    //job.cancel()
    job.cancelAndJoin()//取消任务并等待任务完成
    print("协程被取消并等待完成")
}

join()是一个挂起函数,它需要等待协程的执行,如果协程尚未完成,join()立即挂起,直到协程完成;如果协程已经完成,join()不会挂起,而是立即返回。打印数据如下:

kotlin

join.gif

Job 还可以有层级关系,一个Job可以包含多个子Job,当父Job被取消后,所有的子Job也会被自动取消;当子Job被取消或者出现异常后父Job也会被取消。具有多个子 Job 的父Job 会等待所有子Job完成(或者取消)后,自己才会执行完成。

总的来说:它的作用是Job实例作为协程的唯一标识,用于处理协程,并且负责管理协程的生命周期。

Deferred

Deferred继承自Job,具有与Job相同的状态机制。它是async构建协程返回的一个协程任务,可通过调用await()方法等待协程执行完成并获取结果。不同的是Job没有结果值,Deffer有结果值。

public interface Deferred<out T> : Job {
    //等待协程执行完成并获取结果
    public suspend fun await(): T
}
  • await(): 等待协程执行完毕并返回结果,如果异常结束则会抛出异常;如果协程尚未完成,则挂起直到协程执行完成。
  • T:    这里多了一个泛型参数T,它表示返回值类型,通过await()函数可以拿到这个返回值。

上面已有Deferred代码演示,这里就不再重复实践。

3.作用域

通常我们提到的,都是用来描述范围的,既有约束作用又有提供额外能力的作用。

协程作用域(CoroutineScope)其实就是为协程定义的作用范围 ,为了确保所有的协程都会被追踪,Kotlin 不允许在没有使用CoroutineScope的情况下启动新的协程。CoroutineScope可被看作是一个具有超能力的ExecutorService的轻量级版本。它能启动新的协程,同时这个协程还具备上面所说的suspendresume的优势。

每个协程生成器launchasync等都是CoroutineScope的扩展,并继承了它的coroutineContext自动传播其所有元素和取消。协程作用域本质是一个接口:

public interface CoroutineScope {
    //此域的上下文。Context被作用域封装,用于在作用域上扩展的协程构建器的实现。
    public val coroutineContext: CoroutineContext
}

因为 启动协程需要作用域 ,但是作用域又是在协程创建过程中产生的,这似乎是一个“先有鸡后有蛋还是先有蛋后有鸡”的问题。

常用作用域

官方库给我们提供了一些作用域可以直接来使用:

  • runBlocking:顶层函数,它的第二个参数为接收者是CoroutineScope的函数字面量,可启动协程。但是它会阻塞当前线程,主要用于测试。
  • GlobalScope:全局协程作用域,通过GlobalScope创建的协程不会有父协程,可以把它称为根协程。它启动的协程的生命周期只受整个应用程序的生命周期的限制,且不能取消,在运行时会消耗一些内存资源,这可能会导致内存泄露,所以仍不适用于业务开发。
  • coroutineScope:创建一个独立的协程作用域,直到所有启动的协程都完成后才结束自身。它是一个挂起函数,需要运行在协程内或挂起函数内。当这个作用域中的任何一个子协程失败时,这个作用域失败,所有其他的子程序都被取消。为并行分解工作而设计的。
  • supervisorScope:与coroutineScope类似,不同的是子协程的异常不会影响父协程,也不会影响其他子协程。(作用域本身的失败(在block或取消中抛出异常)会导致作用域及其所有子协程失败,但不会取消父协程。)
  • MainScope:为UI组件创建主作用域。一个顶层函数,上下文是SupervisorJob() + Dispatchers.Main,说明它是一个在主线程执行的协程作用域,通过cancel对协程进行取消。推荐使用。
fun scopeTest() {
    //创建一个根协程
    GlobalScope.launch {//父协程
        launch {//子协程
            print("GlobalScope的子协程")
        }
        launch {//第二个子协程
            print("GlobalScope的第二个子协程")
        }
    }

    //为UI组件创建主作用域
    val mainScope = MainScope()
    mainScope.launch {//启动协程
        //todo
    }
}

注意:MainScope作用域的好处就是方便地绑定到UI组件的声明周期上,在Activity销毁的时候mainScope.cancel()取消其作用域。

Lifecycle的协程支持

Android 官方对协程的支持是非常友好的,KTX 为 Jetpack 的Lifecycle相关组件提供了已经绑定UV声明周期的作用域供我们直接使用:

  • lifecycleScopeLifecycle Ktx库提供的具有生命周期感知的协程作用域,与Lifecycle绑定生命周期,生命周期被销毁时,此作用域将被取消。会与当前的UI组件绑定生命周期,界面销毁时该协程作用域将被取消,不会造成协程泄漏,推荐使用。
  • viewModelScope:与lifecycleScope类似,与ViewModel绑定生命周期,当ViewModel被清除时,这个作用域将被取消。推荐使用。

build.gradle添加Lifecycle相应基础组件后,再添加以下组件即可:

// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
// 只有Lifecycles(没有 ViewModel 和 LiveData)
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"

因为Activity 实现了LifecycleOwner这个接口,而lifecycleScope则正是它的拓展成员,可以在Activity中直接使用lifecycleScope协程实例:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btn_data.setOnClickListener {
            lifecycleScope.launch {//使用lifecycleScope创建协程
                //协程执行体
            }
        }
    }
}

ViewModel中使用创建协程:

class MainViewModel : ViewModel() {
    fun getData() {
        viewModelScope.launch {//使用viewModelScope创建协程
            //执行协程
        }
    }
}

注意:VIewModel 的作用域会在它的 clear 函数调用时取消。

分类和行为规则

官方框架在实现复合协程的过程中也提供了作用域,主要用于明确父子关系,以及取消或者异常处理等方面的传播行为。该作用域分为以下三种:

  • 顶级作用域 :没有父协程的协程所在的作用域为顶级作用域。
  • 协同作用域 :协程中启动新的协程,新协程为所在协程的子协程,这种情况下,子协程所在的作用域默认为协同作用域。此时子协程抛出的未捕获异常,都将传递给父协程处理,父协程同时也会被取消。
  • 主从作用域 :与协同作用域在协程的父子关系上一致,区别在于,处于该作用域下的协程出现未捕获的异常时,不会将异常向上传递给父协程。除了三种作用域中提到的行为以外,父子协程之间还存在以下规则:
  • 父协程被取消,则所有子协程均被取消。由于协同作用域和主从作用域中都存在父子协程关系,因此此条规则都适用。
  • 父协程需要等待子协程执行完毕之后才会最终进入完成状态,不管父协程自身的协程体是否已经执行完。
  • 子协程会继承父协程的协程上下文中的元素,如果自身有相同key的成员,则覆盖对应的key,覆盖的效果仅限自身范围内有效。

4.调度器

在上面介绍协程概念的时候,协程的挂起与恢复在哪挂起,什么时候恢复,为什么能切换线程,这因为调度器的作用:它确定相应的协程使用那些线程来执行。

CoroutineDispatcher调度器指定指定执行协程的目标载体,它确定了相关的协程在哪个线程或哪些线程上执行。可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。

协程需要调度的位置就是挂起点的位置,只有当挂起点正在挂起的时候才会进行调度,实现调度需要使用协程的拦截器。调度的本质就是解决挂起点恢复之后的协程逻辑在哪里运行的问题。调度器也属于协程上下文一类,它继承自拦截器:

public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {

    //询问调度器是否需要分发
    public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true

    //将可运行块的执行分派到给定上下文中的另一个线程上。这个方法应该保证给定的[block]最终会被调用。
    public abstract fun dispatch(context: CoroutineContext, block: Runnable)

    //返回一个continuation,它封装了提供的[continuation],拦截了所有的恢复。
    public final override fun  interceptContinuation(continuation: Continuation<T>): Continuation

它是所有协程调度程序实现扩展的基类(我们很少会自己自定义调度器)。可以使用newSingleThreadContextnewFixedThreadPoolContext创建私有线程池。也可以使用asCoroutineDispatcher扩展函数将任意java.util.concurrent.Executor转换为调度程序。

调度器模式

Kotlin 提供了四个调度器,您可以使用它们来指定应在何处运行协程:

调度器模式 说明 适用场景
Dispatchers.Default 默认调度器,非主线程。CPU密集型任务调度器,适合处理后台计算。 通常处理一些单纯的计算任务,或者执行时间较短任务比如:Json的解析,数据计算等。
Dispatchers.Main UI调度器, Andorid 上的主线程。 调度程序是单线程的,通常用于UI交互,刷新等。
Dispatchers.Unconfined 一个不局限于任何特定线程的协程调度程序,即非受限调度器。 子协程切换线程代码会运行在原来的线程上,协程在相应的挂起函数使用的任何线程中继续。
Dispatchers.IO IO调度器,非主线程,执行的线程是IO线程。 适合执行IO相关操作,比如:网络处理,数据库操作,文件读写等。

所有的协程构造器(如launchasync)都接受一个可选参数,即 CoroutineContext ,该参数可用于显式指定要创建的协程和其它上下文元素所要使用的CoroutineDispatcher

fun dispatchersTest() {
    //创建一个在主线程执行的协程作用域
    val mainScope = MainScope()
    mainScope.launch {
        launch(Dispatchers.Main) {//在协程上下参数中指定调度器
            print("主线程调度器")
        }
        launch(Dispatchers.Default) {
            print("默认调度器")
        }
        launch(Dispatchers.Unconfined) {
            print("任意调度器")
        }
        launch(Dispatchers.IO) {
            print("IO调度器")
        }
    }
}

打印数据如下:

kotlin

image.png

withContext

在 Andorid 开发中,我们常常在子线程中请求网络获取数据,然后切换到主线程更新UI。官方为我们提供了一个withContext顶级函数,在获取数据函数内,调用withContext(Dispatchers.IO)来创建一个在IO线程池中运行的块。您放在该块内的任何代码都始终通过IO调度器执行。由于withContext本身就是一个suspend函数,它会使用协程来保证主线程安全。

//用给定的协程上下文调用指定的挂起块,挂起直到它完成,并返回结果。
public suspend fun  withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T
  • context: 协程的上下文,同上(调度器也属于上下文一类)。
  • block:  协程执行体,同上。block中的代码会被调度到上面指定的调度器上执行,并返回结果值。

这个函数会使用新指定的上下文的dispatcher,将block的执行转移到指定的线程中。它会返回结果, 可以和当前协程的父协程存在交互关系, 主要作用为了来回切换调度器

GlobalScope.launch(Dispatchers.Main) {//开始协程:主线程
    val result: User = withContext(Dispatchers.IO) {//网络请求(IO 线程)
        userApi.getUserSuspend("FollowExcellence")
    }
    tv_title.text = result.name //更新 UI(主线程)
}

在主线程中启动一个协程,然后再通过withContext(Dispatchers.IO)调度到IO线程上去做网络请求,获取结果返回后,主线程上的协程就会恢复继续执行,完成UI的更新。

由于withContext可让在不引入回调的情况下控制任何代码行的线程池,因此可以将其应用于非常小的函数,如从数据库中读取数据或执行网络请求。一种不错的做法是使用withContext来确保每个函数都是主线程安全的,那么可以从主线程调用每个函数。调用方也就无需再考虑应该使用哪个线程来执行函数了。您可以使用外部 withContext来让 Kotlin 只切换一次线程,这样可以在多次调用的情况下,以尽可能避免了线程切换所带来的性能损失。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分