天天看点

Kotlin |协程的理解和使用

文章目录

      • 协程是什么?
      • 协程的特性
      • 协程怎么使用
      • 开始使用协程
      • 案例1:
      • suspend
        • 线程
        • 协程
      • suspend 的挂起
      • suspend 的意义
      • 什么时候需要自定义 suspend 函数
        • 挂起函数的类型
      • 将回调写为挂起函数
      • 案例2
      • 非阻塞式挂起
      • 协程的具体使用
        • delay
        • runBlocking
        • 等待一个任务
        • 结构化的并发

协程是什么?

​ 其实就是一套由 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 的泛型参数是由函数的返回值来决定的

Kotlin |协程的理解和使用

上面的 挂起函数 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)直到在其作用域中启动的所有协程执行完毕后才会结束。

参考自慕课网视频

参考自扔物线

参考自官网