美文网首页kotlin
简说Kotlin的协程使用

简说Kotlin的协程使用

作者: 超级绿茶 | 来源:发表于2019-11-11 13:39 被阅读0次

    协程是什么

    协程被称为“轻线程”却不是线程,其理论早在上世纪就有了,只是近年来才在一些新型的编程语言中出现。至于为什么会有协程这玩意,还要从传统的线程起;

    线程本身是由操作系统实现,由操作系统管控和调度,编程语言只是调用系统所提供的功能,自由度有限,且线程的创建、切换或阻塞等操作对性能的开销都很能大,且这些问题都涉及到操作系统的内核层面。

    但协程不同,协程并非操作系统固有的功能,而是由编程语言实现,其本质就是处于同一线程下的多段代码块,其运行过程遇到调度、挂起、唤醒等操作完全由开发者把控。

    协程有自己的栈内存和局部变量,且一个线程可以包含多个协程,多协程之间不存在同时运行的情况,不会像多线程那样有竞争关系,所以在共享变量和处理异步操作等问题上不但没有性能上的问题,而且在代码的管理方面也较线程更易管换。

    协程适合的领域

    凡是涉及到“生产者/消费者”模式的代码都可以用协程代替以往的线程操作。可以这么说:协程完全可以取代RxJava等异步框架,且在代码的表现更为优雅。缺点是协程不能像线程那样使用到硬件上的优势,无法做到多核运算等功能。所以协程并不能完全取代线程的地位。

    CoroutineScope协程作用域

    协程不同于线程(线程具有全局性),协程是局部的,协程的执行需要在一个范围内,当这个范围取消的时候,里面所有的子协程也自动取消。所以使用协程的前提是必须有一个对应的CoroutineScope。由于协程是可以嵌套创建的,运行时总是先运行外层的协程再运行嵌套的内层协程。当作用域内的代码都执行完后协程的生命周期就进入“完成”状态。

    怎么创建协程

    创建协程最简单的办法是通过GlobalScope.launch方法创建。launch方法定义如下:

    public fun CoroutineScope.launch(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> Unit
    ): Job 
    

    可以看出launch方法其实是CoroutineScope的扩展函数。有三个参数和一个返回值。
    第一个参数为协程的上下文,默认是创建一个新的线程作为协程的容器。
    第二个参数决定协程的启动时机,默认为创建时启动,也可以设为懒启动;

    GlobalScope.launch( start = CoroutineStart.LAZY )
    第三个参数是对象函数,用于编写需要在协程上运行的代码。
    前两个参数都有默认值,第三个参数通常以代码块的形式使用。
    // 创建一个协程
    GlobalScope.launch {
        // 运行在协程上的代码            
    }
    

    此外我们也可以为协程指定运行的线程,可以通过newSingleThreadContext和newFixedThreadPoolContext这两个方法来实现,从使命名上就可以看出它们返回是一个单线程的线程池,毕竟协程只能运行在线程上。

    val single = newSingleThreadContext("test")
    GlobalScope.launch(single) {
        println("${Thread.currentThread().name}") // 打印test
    }
    

    也可以通过协程上下文CoroutineContext来指定协程运行在哪种线程上。

    CoroutineContext协程上下文

    • Dispatchers.Default 将协程随机调度到一个子线程上
    • Dispatchers.IO 作用同上
    • Dispatchers.Main 将协程调度到主线程上
    • Dispatchers.Unconfined 将协和调度到当前线程上
    // 在主线程上创建协程
    GlobalScope.launch(Dispatchers.Main) {
        println("${Thread.currentThread().name}") // 打印main
    }
    

    如何在Android中使用协程

    前面用GlobalScope.launch方法创建出来的是一个顶层协程,这样的协程有一个弊端,一旦我们在界面中忘记取消的话协程将一直占用资源,这将导致内存泄漏的问题出现,所以谷歌推荐采用结构化并发的方式来创建协程。把Activity或Fragment定义成协程的作用域,在定义类的时候继承CoroutineScope接口。然后在onDestory的生命周期中cancel掉协程即可。下面是谷歌给出的实例:

    class MyActivity : AppCompatActivity(), CoroutineScope by MainScope() {
        override fun onDestroy() {
            super.onDestroy()
            cancel() // 取消协程
        }
    
        fun showSomeData() = launch {
            draw(data) // 在主线程绘制
        }
    }
    

    协程作业Job

    当我们用launch方法启动一个协程后可以得到一个类型为Job的返回值,这个返回值就是协程作业,和线程相似协程作业也有生命周期;分为三个阶段“活动”、“完成”、“取消”。这三个阶段在Job中都有对应的方法可以调用:


    coroutcontext.png

    当我们创建了一个协程后可以通过返回值Job来持有这个协程,并可以在需要的时候通过cancel方法取消协程以释放资源。取消父协程会导致子协程也跟着取消。

     job.cancel() // 取消该作业
     job.join() // 等待作业执行结束
    

    如果协程处在长时间的计算当中,调用cancel方法是不会立马取消的,所以还需要用join等待协程把任务作完。
    Job的常用方法和属性如下:

    • job.isActive:判断是处于活动阶段
    • job.isCompleted:判断是否处于完成阶段
    • job.isCancelled:判断是否处于取消阶段
    • job.start():启动协程,除了 lazy 模式,协程都不需要手动启动
    • job.join():等待协程执行完毕
    • job.cancel():取消一个协程
    • job.cancelAndJoin():等待协程执行完毕然后再取消

    挂起函数

    在协程中可以使用suspend关键字定义挂起函数,挂起函数只能运行在协程的作用域内。挂起函数被调用后会挂起当前所在的协程,同时运行函数体内的代码直到结束或被另一个函数挂起。协程在挂起后不会造成线程的阻塞,只会让线程上的其它协程运行。协程在取消的时候会抛出CancellationException异常。

    async方法

    async方法和launch方法都能启动一个协程,但不同的是async主要是为了获取协程处理后的结果。async的返回值是Deferred对象,这个接口是Job接口的子类,可以通过Deferred接口的await方法获取到协程完成后的返回结果,这一个结果通常为了个对象。由于async方法是一个挂起函数,在代码中我们用lambda表达式方便的调用。

    MainScope().launch {
        // 创建一个协程,但不执行
        val deferred = async(Dispatchers.IO) {
            //此处是一个耗时任务
            // Dispatchers.IO是为了将耗时任务调度到子结程执行
            delay(3000L)
            "ok"
        }
        //此处继续执行其他任务
        //..........
        val result = deferred.await()  //挂起当前协程,并等待async执行完毕返回结果
        //当前协程被唤醒后继续执行
        tvResult.text = result
    }
    

    withContext方法

    withContext不会创建协程,主要作用是把挂起函数调度到不同的协程上文环境,直到挂起函数运行完成再调度回原先的协程上下文。说白了withContext就是先挂起当前的协程,然后把代码块切换到另一个线程上运行,运行完后再切换回来继续往下跑。所以非常适用于需要线程切换的情况;例如在主线程创建一个协程,做些前期准备(例如显示个loading对话框、封装个接口参数等),然后切换到子线程发起接口请求,等请求到结果后再切原先的主线程刷界面。

    // 在主线程上创建协程
    private fun onGetRequest() = launch {
        val dlg = indeterminateProgressDialog("loading")
        val url = "http://www.test.do"
        val mapParam = mapOf("cityname" to "上海")
        // 切换到子线程上发起接口请求
        val result = withContext(Dispatchers.IO) {
            SimpleHttpUtils.get(url, mapParam) // 发起GET请求
        }
        // 完成后再切换回主线程刷界面
        tvResult.text = result 
        dlg.dismiss()
    }
    

    协程调度

    协程是运行在线程上的,但并不是和线程绑定,可以通过协程调度器将协程从某个线程切换到另一个线程上。async和withContext方法都可以传入一个协程上下文CoroutineContext参数,根据这个参数可以很方便的在不同的线程之间切换同时以使代码保持了顺序阅读。

    点击链接加入群聊【口袋里的安卓】:https://jq.qq.com/?_wv=1027&k=5z4fzdT
    或关注微信公众号:

    qrcode_for_gh_31280308f3ef_258.jpg

    相关文章

      网友评论

        本文标题:简说Kotlin的协程使用

        本文链接:https://www.haomeiwen.com/subject/vggwbctx.html