美文网首页
Kotlin协程

Kotlin协程

作者: Itachi001 | 来源:发表于2021-01-17 23:11 被阅读0次

协程是什么?

官方定义:协程是一种并发设计模式,我们可以在 Android 平台上使用它来简化异步执行的代码。
官方说协程是一种设计模式是因为协程不是Kotlin独有的,Go和Python也有协程,它是一种设计思想。而Kotlin的协程就是一套Kotlin官方提供的线程API,它类似于Java的ExecutorsAndroid的AsyncTask,实际上它就是对Thread API的一套封装。

// Thread
Thread({
    ...
}).start()
// Executor
val executor = Executors.newCachedThreadPool()
executor.execute({
    ...
})
// 协程
launch({
    ...
})

那么Java有Executors,Android又增加了Handler和AsyncTask,而且我们还有RxJava这种开发神器,为什么我们还要学习协程呢?

协程的优势

  • Kotlin语言优势,比起基于JAVA的方案更方便,更简洁
  • 非阻塞式挂起,它可以用看起来同步的方式写出异步代码
launch(Dispatchers.Main) {   // 在主线程开启协程
    val user = api.getUser() // IO 线程执行网络请求
    nameTv.text = user.name  // 主线程更新 UI
}

而以前Java的写法是这样的:

api.getUser(new Callback<User>() {
    @Override
    public void success(User user) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                nameTv.setText(user.name);
            }
        })
    }
    
    @Override
    public void failure(Exception e) {
        ...
    }
});

这种回调式的写法,读起来相当难受。
可能我们习惯了这种写法会觉得还好,但是需要发起多个网络请求后再统一更新UI的情况下,我们可能会这样写:

api.getAvatar(user) { avatar ->
    api.getCompanyLogo(user) { logo ->
        show(merge(avatar, logo))
    }
}

以上是垃圾代码,没有同时请求,浪费了性能,于是考虑这样写:

api.getAvatar(user, callback)
api.getCompanyLogo(user, callback)

然后可以在callback中通过responseCount变量做判断是否都再合并数据,最后更新UI。这样的方式很不优雅!
Kotlin协程中实现的话会是这样的:

coroutineScope.launch(Dispatchers.Main) {
    //              async 函数之后再讲
    val avatar = async { api.getAvatar(user) }    // 获取用户头像
    val logo = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo
    val merged = suspendingMerge(avatar, logo)    // 合并结果
    //                  
    show(merged) // 更新 UI
}

基本使用

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}

// 方法一,使用 runBlocking 顶层函数
runBlocking {
    getImage(imageId)
}

// 方法二,使用 GlobalScope 单例对象
//             可以直接调用 launch 开启协程
GlobalScope.launch {
    getImage(imageId)
}

// 方法三,自行通过 CoroutineContext 创建一个 CoroutineScope 对象
//                                     需要一个类型为 CoroutineContext 的参数
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
    getImage(imageId)
}
  • 方法一是线程阻塞的,适用于单元测试,业务开发中不常用
  • 方法二不阻塞线程,但是生命周期和App一致,且不能取消,所以Android开发中也不推荐使用
  • 方法三不阻塞线程,且生命周期可控,因此Android开发中推荐使用这种方式

协程最简单的使用就是切线程,切换到IO线程:

launch(Dispatchers.IO) {
    ...
}

切换到主线程:

launch(Dispatchers.Main) {
    ...
}

我们来看看发起一个简单的网络请求并更新UI的需求在协程中的实现

coroutineScope.launch(Dispatchers.IO) {
    val image = getImage(imageId)
    launch(Dispatchers.Main) {
        avatarIv.setImageBitmap(image)
    }
}

这种方式比回调更优雅,但是这种嵌套也同样会影响代码的可阅读性。
如果只是使用 launch 函数,协程并不能比线程做更多的事。不过协程中却有一个很实用的函数:withContext 。这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行。那么可以将上面的代码写成这样:

coroutineScope.launch(Dispatchers.Main) {      //  在 UI 线程开始
    val image = withContext(Dispatchers.IO) {  //  切换到 IO 线程,并在执行完成后切回 UI 线程
        getImage(imageId)                      //  将会运行在 IO 线程
    }
    avatarIv.setImageBitmap(image)             //  回到 UI 线程更新 UI
} 

由于消除了嵌套关系,我们甚至可以把 withContext 放进一个单独的函数里面:

launch(Dispatchers.Main) {              //  在 UI 线程开始
    val image = getImage(imageId)
    avatarIv.setImageBitmap(image)     //  执行结束后,自动切换回 UI 线程
}
//                                      
suspend fun getImage(imageId: Int) = withContext(Dispatchers.IO) {
    ...
}

这就是之前说的「用同步的方式写异步的代码」了。

fun getImage(imageId: Int) = withContext(Dispatchers.IO) {
    // IDE 报错 Suspend function'withContext' should be called only from a coroutine or another suspend funcion
}

意思是说,withContext 是一个 suspend 函数,它需要在协程或者是另一个 suspend 函数中调用。

suspend

suspend 是 Kotlin 协程最核心的关键字,它的中文意思是「暂停」或者「可挂起」。如果你去看一些技术博客或官方文档的时候,大概可以了解到:「代码执行到 suspend 函数的时候会『挂起』,并且这个『挂起』是非阻塞式的,它不会阻塞你当前的线程。

「挂起」的本质

协程中「挂起」的对象是协程。
启动一个协程可以使用 launch函数,协程其实就是这个函数中闭包的代码块
「挂起」就是这个协程从正在执行它的线程上脱离,在其他线程继续执行去了
怎么「挂起」的?

suspend fun suspendingPrint() {
  println("Thread: ${Thread.currentThread().name}")
}

I/System.out: Thread: main

没有切线程,输出的结果还是在主线程,因为它不知道往哪切,需要我们告诉它。

suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {
  ...
}

挂起,就是一个稍后会被自动切回来的线程调度操作

suspend 的意义

这个 suspend 关键字,既然它并不是真正实现挂起,那它的作用是什么?
它其实是一个提醒。
如果你的某个函数比较耗时,也就是要等的操作,那就把它写成 suspend 函数
给函数加上 suspend 关键字,然后在 withContext 把函数的内容包住就可以了
withContext 可以把线程自动切走和切回。

相关文章

网友评论

      本文标题:Kotlin协程

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