Kotlin |协程的理解和使用

简介: Kotlin |协程的理解和使用

协程是什么?


其实就是一套由 Kotlin 官方提供的线程 API 。可以以非常优雅的方式来执行异步代码。使用简单,高效。


协程的特性


结构化并发:

val scope = CoroutineScope(Dispatchers.Main).launch {
    launch {
        launch {
        }
    }
    launch {
    }
}
scope.cacel()


协程中支持嵌套,并且具有子协程和父协程的概念

如上,在启动了一个协程后,在内部有启动了多个子协程,在子协程中还可以继续启动协程。返回来看线程,线程中可以创建线程,但是他们之间是没有关联的


协程的这种特性被称为结构化并发,这种方式可以让协程非常方便的管理,比方说关闭协程,只需要关闭最外面的协程后,内部的协程都会被关闭。对于子协程也可以获取他的返回值并调用 cacel 进行关闭。


协程的取消

协称的异常处理


协程怎么使用


在项目中配置对 Kotlin 的协程的支持


//核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.1" 
//依赖当前平台对应的库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.1"


Kotlin 协程是以官方扩展库的形式进行支持的。而且 核心库 和 平台库的版本应该保持一致。


核心库中包含的代码主要是协程的公共 API 部分,有了这一层的代码,才使得协程在各个平台上的接口得到统一

平台库中包含的代码主要是协程框架在具体平台的具体实现方式。因为多线程在每个平台上都是有差异的。


开始使用协程


//launch 函数的含义:我要创建一个新的协程。并且在指定的线程上运行它
CoroutineScope(Dispatchers.IO).launch {
  println(Thread.currentThread().name)
}


可嵌套使用


CoroutineScope.launch(Dispatchers.IO) {
    val image = getImage(imageId)
    launch(Dispatch.Main) { //将会运行在 Main 线程
        avatarIv.setImageBitmap(image)
    }
}


如果只是嵌套,这并没有多少作用。协程有一个非常好用的函数 :withContext。这个函数可以切换到指定线程,并在闭包中的逻辑执行完后自动把线程切回去继续执行


CoroutineScope(Dispatchers.Main).launch {
    println(Thread.currentThread().name)
    val bitmap = withContext(Dispatchers.IO) {  //切换到 IO 线程
        getImage()
    }
    iv.setImageBitmap(bitmap)  //主线程更新
}


由于可以自动切回来,我们甚至可以把 withContext 放进一个单独的函数里面,如下:


suspend fun getImage(): Bitmap = withContext(Dispatchers.IO) {
    //.....
}


但是要注意 suspend 关键字。这个关键字后面在说,他中文意思是 暂停 或者 可挂起。


案例1:


通过协程下载一张网络图片,并且显示出来


override fun bindView(view: View) {
        val iv = view.findViewById<AppCompatImageView>(R.id.delegate_shop_iv)
        btn = view.findViewById(R.id.delegate_shop_btn)
        btn.setOnClickListener {
            CoroutineScope(Dispatchers.Main).launch {
                val bitmap = getImage()
                iv.setImageBitmap(bitmap)
            }
        }
    }
private suspend fun getImage(): Bitmap = withContext(Dispatchers.IO) {
    OkHttpClient().newCall(Request.Builder()                  .url("https://dss0.bdstatic.com/6Ox1bjeh1BF3odCf/it/u=4256581120,3161125441&fm=193")
                           .get()
                           .build())
    .execute().body()?.byteStream().use {
        BitmapFactory.decodeStream(it)
    }
}


suspend


协程指的就是 launch 中的代码,那么协程中的挂起是什么呢? 其实挂起的对象就是协程


当执行在 launch 中时,在执行到某一个 suspend 函数时,这个协程就会被挂起。让


时间静止,兵分两路,来看一下到底是怎么回事,这两路分别是协程和线程(UI线程)


线程


当代码执行到协程中的 suspend 函数后,就暂时不会执行协程代码,而是跳出协程的代码块。继续向下执行。


CoroutineScope(Dispatchers.Main).launch {
    val bitmap = getImage() //挂起
    iv.setImageBitmap(bitmap)
}
ToastUtils.show("哈哈哈哈")


当主线程执行到 getImage 时,会跳出协程,执行下面的 Toast。


这个协程本质上会往主线程 post 一个 Runnable。然后继续执行协程内部代码。当执行到被挂起的时候,Runnable 会提前结束,线程继续执行其他的东西。而协程则会被挂起。所以接下来看一下协程


协程


主线程在执行到 suspend 的时候会被掐断,接下来协程会继续往下执行。不过是执行在指定的线程。通过 withContext 传入的 Dispatchers.IO 所指定的 IO 线程


Dispatchers 调度器:将协程限制在一个特定的线程执行,或者将他分派到一个线程池。


日常使用的调度器:Main:Android主线程,IO:网络IO,Default:适合CPU 密集的任务,比如计算。


协程从 suspend 开始执行在指定的线程,执行完之后,就会自动将我们把线程切回来。


切回来就是切换到原本的线程,如原本是运行在主线程的,切回来后就会继续在主线程执行。也就是说协程会帮我们 post 一个 Runnable 到主线程。


通过上面两个角度,可以得到一个解释:协程在执行到有 suspend 的时候就会被挂起,而这个挂起,则就是切个线程;只不过挂起的执行完后会重新切回他原来的线程


这个切回来的动作,在 Kotlin 中叫做 resume ,恢复


suspend 的挂起


挂起函数,就是以 suspend 修饰的函数,挂起函数只能在 其他挂起函数或者协程中使用


这是一个关键字。但是他并不是正真的挂起。你可以写一个带 supend 的函数,运行后就会发现并没有挂起,为啥没有被挂起,应为它不知道往哪切,需要我们来告诉他。如下:


suspend fun get() = withContext(Dispatchers.IO) {
    }


withContext 本身就是一个挂起函数,接收一个 Dispatcher 参数,他必须依赖于这个参数,才能知道协程需要被挂起。接着才会切换到别的线程


所以 suspend 起不到任何挂起函数的作用,挂起函数是 kt 的协程帮我们做的


suspend 的意义


为啥 suspend 关键字没有实际的挂起,但 Kotlin 为啥要把它提供出来?


因为他本来就不是用来操作挂起的。也就是说切线程依赖的是函数中的代码,而不是这个关键字。所以这个关键字只是用来提醒。


如果你创建一个 suspend 函数,但是内部不包含正真的挂起逻辑,编译器会给你提醒: Redundant ‘suspend’ modifier ,这个关键字是多余的


因为这个函数并不会发生挂起,那这个 suspend 只有一个效果:限制此函数只能在协程中被调用,如果在非协程中调用,则编译不会通过


所以,创建一个 suspend 函数,为了让他包含挂起,要在内部直接或者间接调用 Kotlin 自带的 suspend 函数,这个时候函数才是有意义的


什么时候需要自定义 suspend 函数


如果你某个函数比较耗时,也就是需要等,就可以把它写成 suspend 函数


给函数加上 suspend 关键字,然后在 withContext 把函数内容包住就行了。当然并不是只有 withContext 来辅助我们实现自定义的函数,如 delay ,他的作用是等一段时间后在继续往下执行代码。


suspend fun get() {
  delay(5) //挂起
}


挂起函数的类型


suspend fun foo(){}


如果没有 suspend 这个函数的类型就是 ()-> Unit


但是加了 suspend 后,函数的类型为 suspend()-> Unit


只要你知道函数的类型是什么,然后在前面加一个 suspend 就是挂起函数的类型


suspend fun bar(a:Int):String{
        return "Hello"
}


挂起函数 bar 的类型为 suspend(Int)->String


还记得 suspend 为什么只能在 挂起函数或者协程中调用吗?


因为所有的挂起函数都有一个 Continuation 参数,Continuation 是从哪来的呢,suspend 关键字会隐含在 函数的参数列表的最后加一个 Continuation 参数。Continuation 的泛型参数是由函数的返回值来决定的


上面的 挂起函数 bar 最终的样子如上图,他会在参数列表的最后加一个参数,并且返回值成为了 Any。这个 Any 有两种情况,如果这个函数没有真正的挂起,比如 bar函数,foo 函数。在函数没有真正挂起的时候,这个 Any就是用来承载返回值结果。如果函数真的被挂起了,这个Any 返回的是一个挂起的标志 COROUTINE_SUSPENDED ,让外部的协程体知道我这个协程真正被挂起了。要等待这个函数的回调,才能继续往下执行。 所以这个 Any 是非常重要的


将回调写为挂起函数


通过 suspendCoroutine 来实现:


private suspend fun getImage() = suspendCoroutine<Bitmap> { 
        continuation ->
        OkHttpClient().newCall(Request.Builder()
                .url("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1581869591580&di=e0412feb1e101a144e416f7a873bd88d&imgtype=0&src=http%3A%2F%2Fi0.hdslb.com%2Fbfs%2Farticle%2F6febb183087736d089b6583a790c491f2dc7469a.jpg")
                .get()
                .build())
                .enqueue(object : Callback {
                    override fun onFailure(call: Call, e: IOException) {
                        continuation.resumeWithException(e)
                    }
                    override fun onResponse(call: Call, response: Response) {
                        continuation.resume(response.body()?.byteStream().use { BitmapFactory.decodeStream(it)})
                    }
                })
    }


如果要将回调转为挂起,需要使用 suspendCoroutine 这个函数调用获取当前函数的 Continuation。通过这个方法就可以拿得到 Continuation。这个参数被编译器藏了起来。


回调成功可以使用 resume 或者 resumeWith 将结果返回


异常就是用 resumeWithException 即可。


案例2


网络请求一张图片,并进行两次切割 。1,切成四份,取第一份。2,切9份,去最后一份


override fun bindView(view: View) {
        //        getSupportDelegate().loadRootFragment(R.id.delegate_shop_layout,
        //                BaseShopListDelegate.newInstance(CarPreference.getMyCar(), BusinessScope.BUSINESSSCOPE_SHOP_LIST));
        val iv1 = view.findViewById<AppCompatImageView>(R.id.delegate_shop_iv1)
        val iv2 = view.findViewById<AppCompatImageView>(R.id.delegate_shop_iv2)
        val iv3 = view.findViewById<AppCompatImageView>(R.id.delegate_shop_iv3)
        btn = view.findViewById(R.id.delegate_shop_btn)
        btn.setOnClickListener {
            CoroutineScope(Dispatchers.Main).launch {
                val bitmap = getImage()
                iv1.setImageBitmap(bitmap)//原图
                val bm1 = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width / 2, bitmap.height / 2)
                iv2.setImageBitmap(bm1)
                val bm2 = Bitmap.createBitmap(bitmap, bitmap.width / 3 * 2, bitmap.height / 3 * 2, bitmap.width / 3, bitmap.height / 3)
                iv3.setImageBitmap(bm2)
            }
        }
    }
    private suspend fun getImage(): Bitmap = withContext(Dispatchers.IO) {
        OkHttpClient().newCall(Request.Builder()
                .url("https://dss0.bdstatic.com/6Ox1bjeh1BF3odCf/it/u=4256581120,3161125441&fm=193")
                .get()
                .build())
                .execute().body()?.byteStream().use {
                    BitmapFactory.decodeStream(it)
                }
    }


非阻塞式挂起


首先什么是阻塞呢?


1,前面有障碍物,过不去了(线程卡主)


2,清除障碍物(等待耗时任务结束)


3,绕道而行(切到别的线程)


非阻塞式挂起并没有限定在一个线程中,因为挂起本来就涉及到多个线程。主线程执行的时候遇到耗时任务,然后将耗时任务挂起,这时主线程就自由了,可以继续做别的事了。所以非阻塞式挂起其实就是在讲 协程在挂起的时候切换线程这件事。


协程只是看起来会阻塞,但其实是非阻塞的,因为它可以切线程


协程与线程:在 Kotlin 中,协程就是基于线程实现的一种更上层的工具 API ,只不过他的用法非常简单。


协程是什么:基于线程的一个框架


协程的挂起:自动切换线程


非阻塞式挂起:可以用看起来阻塞的代码来实现非阻塞的操作


协程的具体使用


delay


fun main() {
    GlobalScope.launch {
        delay(1000L) //协程挂起,阻塞1秒
        println(" 2020")
    }
    print("hello")  //协程挂起时,主线程继续执行
    Thread.sleep(2000L)//延时:保证 主线程存活
}
//hello 2020


runBlocking


在讲创建协程的时候说过,runBlocking 是阻塞式的


fun main() {
    GlobalScope.launch {
        delay(1000L)
        println("2020 ")
    }
    println("Hello ")
    //表达式阻塞了主线程,调用了 runBlocking 的主线程会一直等到 runBlocking执行完毕
    runBlocking {
        delay(2000L) //延时:保证 主线程存活
    }
}
//Hello 2020


修改代码如下:


fun main() = runBlocking {//开始执行主协程
    println(Thread.currentThread().name)
    GlobalScope.launch {
        delay(1000L)
        println("2020 ")
    }
    print("Hello ")
    delay(2000L) //延时:保证 主线程存活
}
//main
//Hello 2020


其实还是主线程


等待一个任务


suspend fun main() {
    val job = GlobalScope.launch {
        delay(1000L)
        println(" 2020")
    }
    println("Hello")
    job.join() //等待子线程执行结束
}


注意 main 方法被 suspend 修饰了,因为 join 方法被 suspend 修饰过,suspend 本身不会挂起,挂起是因为 join 内部有挂起的代码。suspend 只是一个提示。只是这个提示必须写。


结构化的并发


fun main() = runBlocking {
    //开始执行主协程
    launch {
        delay(1000L)
        println(" 2020")
    }
    print("Hello")
}
//Hello 2020


注意:在最后并没有让主线程等待,也没有调用 join。为啥会打印出 2020 呢?


在 runBlocking 内的每个协程构建器中都将 CoruntineScope 的实例添加到代码块的作用域中。我们可以在这个作用域中启动协程而无需显示调用 join。因为外部协程(runBlocking)直到在其作用域中启动的所有协程执行完毕后才会结束。


相关文章
|
23天前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
17 1
|
3月前
|
调度 开发者 UED
Kotlin 中的协程是什么?
【8月更文挑战第31天】
173 0
|
5月前
|
存储 Java 调度
Android面试题之Kotlin 协程的挂起、执行和恢复过程
了解Kotlin协程的挂起、执行和恢复机制。挂起时,状态和上下文(局部变量、调用栈、调度器等)被保存;挂起点通过`Continuation`对象处理,释放线程控制权。当恢复条件满足,调度器重新分配线程,调用`resumeWith`恢复执行。关注公众号“AntDream”获取更多并发知识。
124 2
|
6月前
|
移动开发 Android开发 开发者
构建高效Android应用:Kotlin与协程的完美融合
【5月更文挑战第25天】 在移动开发的世界中,性能和响应性是衡量应用质量的关键指标。随着Kotlin的流行和协程的引入,Android开发者现在有了更强大的工具来提升应用的性能和用户体验。本文深入探讨了Kotlin语言如何与协程相结合,为Android应用开发带来异步处理能力的同时,保持代码的简洁性和可读性。我们将通过实际案例分析,展示如何在Android项目中实现协程,以及它们如何帮助开发者更有效地管理后台任务和用户界面的流畅交互。
|
6月前
|
移动开发 监控 Android开发
构建高效安卓应用:Kotlin 协程的实践与优化
【5月更文挑战第16天】 在移动开发领域,性能优化一直是开发者们追求的重要目标。特别是对于安卓平台来说,由于设备多样性和系统资源的限制,如何提升应用的响应性和流畅度成为了一个关键议题。近年来,Kotlin 语言因其简洁、安全和高效的特点,在安卓开发中得到了广泛的应用。其中,Kotlin 协程作为一种轻量级的并发解决方案,为异步编程提供了强大支持,成为提升安卓应用性能的有效手段。本文将深入探讨 Kotlin 协程在安卓开发中的应用实践,以及通过合理设计和使用协程来优化应用性能的策略。
63 8
|
6月前
|
移动开发 数据库 Android开发
构建高效Android应用:探究Kotlin的协程优势
【5月更文挑战第22天】随着移动开发技术的不断进步,Android平台的性能优化已经成为开发者关注的焦点。在众多提升应用性能的手段中,Kotlin语言提供的协程概念因其轻量级线程管理和异步编程能力而受到广泛关注。本文将深入探讨Kotlin协程在Android开发中的应用,以及它如何帮助开发者构建出更高效、响应更快的应用,同时保持代码的简洁性和可读性。
|
6月前
|
移动开发 Android开发 开发者
构建高效安卓应用:Kotlin 协程的实践指南
【5月更文挑战第18天】 随着移动开发技术的不断进步,安卓平台亟需一种高效的异步编程解决方案来应对日益复杂的应用需求。Kotlin 协程作为一种新兴的轻量级线程管理机制,以其简洁的语法和强大的功能,成为解决这一问题的关键。本文将深入探讨Kotlin协程在安卓开发中的实际应用,从基本概念到高级技巧,为开发者提供一份全面的实践指南,旨在帮助读者构建更加高效、稳定的安卓应用。
|
6月前
|
移动开发 安全 Android开发
构建高效Android应用:Kotlin与协程的完美结合
【5月更文挑战第17天】 在移动开发领域,性能优化和流畅的用户体验是关键。对于Android平台而言,Kotlin语言凭借其简洁性和功能安全性成为开发的首选。与此同时,协程作为一种新的并发处理方式,在简化异步编程方面展现出巨大潜力。本文将深入探讨如何通过Kotlin语言以及协程技术,提升Android应用的性能和响应能力,并确保用户界面的流畅性。
|
6月前
|
移动开发 数据库 Android开发
构建高效Android应用:Kotlin与协程的完美结合
【5月更文挑战第7天】 在移动开发领域,性能优化和资源管理始终是核心议题。随着Kotlin语言的普及,其提供的协程特性为Android开发者带来了异步编程的新范式。本文将深入探讨如何通过Kotlin协程来优化Android应用的性能,实现流畅的用户体验,并减少资源消耗。我们将分析协程的核心概念,并通过实际案例演示其在Android开发中的应用场景和优势。
|
6月前
|
移动开发 数据处理 Android开发
构建高效Android应用:Kotlin的协程与Flow的使用
【5月更文挑战第23天】 在移动开发领域,性能优化和异步编程一直是核心议题。随着Kotlin语言在Android开发中的普及,其提供的协程(coroutines)和流式编程(Flow)功能为开发者带来了革命性的工具,以更简洁、高效的方式处理异步任务和数据流。本文将深入探讨Kotlin协程和Flow在Android应用中的实际应用,以及它们如何帮助开发者编写更加响应迅速且不阻塞用户界面的应用程序。我们将通过具体案例分析这两种技术的优势,并展示如何在现有项目中实现这些功能。