史上最详 Android 版 kotlin 协程入门进阶实战(一),一线互联网公司面经总结
public interface Deferred<out T> : Job {
//返回结果值,或者如果延迟被取消,则抛出相应的异常
public suspend fun await(): T
public val onAwait: SelectClause1<T>
public fun getCompleted(): T
public fun getCompletionExceptionOrNull(): Throwable?
}
我们需要重点关注await()
方法,可以看到await()
方法返回结果是T
,说明我们可以通过await()
方法获取执行流的返回值,当然如果出现异常或者被取消执行,则会抛出相对应的异常。
[](
)什么是作用域
协程作用域(Coroutine Scope
)是协程运行的作用范围。launch
、async
都是CoroutineScope
的[扩展函数](
),CoroutineScope
定义了新启动的协程作用范围,同时会继承了他的coroutineContext
自动传播其所有的 elements
和取消操作。换句话说,如果这个作用域销毁了,那么里面的协程也随之失效。就好比变量的作用域,如下面test
方法中的money
变量
private fun test(){ // scope start
int money = 100;
println(money)
} // scope end
// println(money)
此时money
是不能被调用,因为 AS 会提示 Unresolved reference: money
。协程作用域也是这样一个作用,可以用来确保里面的协程都有一个作用域的限制。我们开发过程中最常见的场景就内存泄露,协程同样存在这样的问题,后面我们再细细讲解协程作用域CoroutineScope
的相关知识,这里只是作为基础点讲解,不继续往下延伸。
[](
)Kotlin 协程的基础用法
============================================================================
现在我们开始使用协程,首先我们在MainActivity
的 xml 布局中新建一个Button
按钮然后设置好点击事件,然后创建一个start()
方法,通过Button
的点击事件执行。现在我们开始在start
方法中使用协程。
刚才我们上面提到启动协程有三种方式,接下来我们先看看如何通过runBlocking
、launch
和async
启动协程,我们直接在 start 方法中使用,但是由于我们的launch
和async
启动,只能在协程的作用域下启动,那我们又该怎么办呢?
[](
)运行第一个协程
在 Android 中有一个名为GlobalScope
全局顶级协程,这个协程是在整个应用程序生命周期内运行的。我们就以此协程来使用launch
和async
启动,代码如下:
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.Group
import androidx.viewpager.widget.ViewPager
import kotlinx.coroutines.*
import java.lang.NullPointerException
class MainActivity : AppCompatActivity() {
private lateinit var btn:Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btn = findViewById(R.id.btn)
btn.setOnClickListener {
start()
}
}
private fun star
t(){
runBlocking {
Log.d("runBlocking", "启动一个协程")
}
GlobalScope.launch{
Log.d("launch", "启动一个协程")
}
GlobalScope.async{
Log.d("async", "启动一个协程")
}
}
}
然后运行 app,点击按钮执行start()
方法。我们就可以在控制台上看到如下输出:
D/runBlocking: 启动一个协程
D/launch: 启动一个协程
D/async: 启动一个协程
哇
,so easy。协程原来这么简单,那我们接着继续往下走。上面提到过三种启动方式分别会的得到各自的返回信息。我们现在增加三个变量然后分别用协程进行赋值,同时进行输出:
private fun start(){
val runBlockingJob = runBlocking {
Log.d("runBlocking", "启动一个协程")
}
Log.d("runBlockingJob", "$runBlockingJob")
val launchJob = GlobalScope.launch{
Log.d("launch", "启动一个协程")
}
Log.d("launchJob", "$launchJob")
val asyncJob = GlobalScope.async{
Log.d("async", "启动一个协程")
"我是返回值"
}
Log.d("asyncJob", "$asyncJob")
}
然后运行,我们可以在控制台上看到如下输出:
D/runBlocking: 启动一个协程
D/runBlockingJob: 41
D/launchJob: StandaloneCoroutine{Active}@3b8b871
D/launch: 启动一个协程
D/async: 启动一个协程
D/asyncJob: DeferredCoroutine{Active}@63f265
也有可能是
D/runBlocking: 启动一个协程
D/runBlockingJob: 41
D/launchJob: StandaloneCoroutine{Active}@1344515
D/asyncJob: DeferredCoroutine{Active}@38c002a
D/async: 启动一个协程
D/launch: 启动一个协程
还有可能是
D/runBlocking: 启动一个协程
D/runBlockingJob: 41
D/launch: 启动一个协程
D/launchJob: StandaloneCoroutine{Active}@b94e973
D/async: 启动一个协程
D/ asyncJob: DeferredCoroutine{Active}@f7aa030
嗯哼,什么情况
怎么后面 4 条日志顺序还是随机的。没有看懂的童鞋,说明你没有仔细看上面的文字。
知识点来了,赶紧拿出你的小本本记下来,我们一个一个的来分析。
我们在上面提到过runBlocking
启动的是一个新的协程并阻塞调用它的线程,我们对比输出日志可以看到前两条runBlocking
的相关输出日志的位置顺序是不会变化的,这就证明我们之前所说的runBlocking
会阻塞调用它的线程,直到runBlocking
运行结束才继续往下执行。
接下来我们再继续往下看,我们看到后面四条日志是无序的,但是launchJob
始终在asyncJob
前面。而launch
和async
协程体内的日志输出是无序的。每执行一次看到的顺序都有可能跟之前的不一样。我们前面提到过launch
和async
都是启动一个协程但不会阻塞调用线程,所以launchJob
始终在asyncJob
前面(2 个协程之间不是很明显,你们自己在尝试的时候,可以同时启动 5 个甚至更多协程去看日志输出)
而launch
和async
协程体内的日志是无序的,这是因为协程采用的是并发设计模式,所以launch
和async
的协程体内的 log 日志输出是无序方式,这就解释了launch
和async
都是启动一个协程但不会阻塞调用线程,同时也解释了 log 日志之间输出顺序之间的关系(这里描述是不严谨的,后面会补充
)。
难道就这样结束了吗?那你想的可就太简单了。刚才我们提到协程采用的是并发设计模式,多个协程并发执行的。那如果这个时候,我们把启动协程放在同一协程作用域下启动的是顺序又该是怎么样的呢? 大家可以先思考一下这个问题,回头我们再来看这个问题。
[](
)runBlocking 的返回值
现在我们回到之前的话题,我们看到输出的日志信息中runBlockingJob
的输出结果是 41,为什么是这么一个数值,其实他默认返回是一个该协程作业的当前状态
我们通过runBlocking
方法可以看到,其返回值是调用了joinBlocking
方法,而在joinBlocking
方法中
我们看到joinBlocking
方法返回了一个 state 强转成泛型T
类型。我们现在大概知道runBlocking
返回的是个什么东西了。如果在runBlocking
协程最后一行增加一个返回值:
val runBlockingJob = runBlocking {
Log.d("Coroutine", "runBlocking 启动一个协程")
"我是 runBlockingJob 协程的返回值"
}
我们将会看到如下输出:
D/Coroutine: runBlocking 启动一个协程
D/runBlockingJob: 我是 runBlockingJob 协程的返回值
runBlocking
它的设计目的是将常规的阻塞代码连接到一起,主要用于main
函数和测试中。根据本文章的目标我们后续将不再往下扩展。
继续往下走,我们看到launchJob
输出的是一个StandaloneCoroutine
对象,为什么会是一个StandaloneCoroutine
对象呢,不是说好的返回一个 Job 吗?
别慌,稳住!继续往下看
[](
)launch 函数
我们看到launch
函数中有 3 个参数context
、start
和block
,同时都带有默认值,虽然我们不知道这三个参数是干什么用的,但是我们可以看名知其意,不妨先大胆的猜测一下,我们这里先跳过,后面再针对这三个参数做一些基本讲解。我们看到launch
方法最终返回的是一个coroutine
对象,由于我们没有传入值其最后返回的是一个StandaloneCoroutine
对象,跟我们输出的日志结果一致。那为什么笔者会说launch
返回的是一个 Job 呢。我们再继续看看StandaloneCoroutine
又是一个什么鬼,通过查找继承关系我们可以看到,StandaloneCoroutine
就是一个 Job,现在就一目了然了。
private open class StandaloneCoroutine(...) : AbstractCoroutine<Unit>(parentContext, active){
//此处省略.....
}
public abstract class AbstractCoroutine<in T>(...) : JobSupport(active), Job, Continuation<T>, CoroutineScope {
//此处省略.....
}
[](
)async 函数
同理我们也看看async
函数,和launch
拥有同样的 3 个参数context
、start
和block
,默认值都是一样的,最终返回的是也是一个coroutine
对象。只是async
返回的DeferredCoroutine
对象。
private open class DeferredCoroutine<T>(...) : AbstractCoroutine<T>(parentContext, active), Deferred<T>, SelectClause1<T> {
//此处省略.....
}
同样的都是继承 AbstractCoroutine<Unit>
类,但是DeferredCoroutine
同时也继承Deferred<T>
接口。这么看来DeferredCoroutine
就是一个Deferred<T>
,一个携带有返回值Job
。那么问题来了,我们要怎么获取到这个Deferred<T>
携带的返回值T
呢。
我们在一开始的时候提到需要重点关注Deferred
的await()
方法,我们可以通过返回Deferred
对象,调用await()
方法来获取返回值,我们看到await()
前面有个suspend
关键字,这又是个额啥玩意。
public suspend fun await(): T
[](
)挂起函数
suspend
是协程的关键字,表示这个一个挂起函数,每一个被suspend
饰的方法只能在suspend
方法或者在协程中调用。现在我们修改之前的代码,同时多增加几条输出日志:
private fun start(){
GlobalScope.launch{
val launchJob = launch{
Log.d("launch", "启动一个协程")
}
Log.d("launchJob", "$launchJob")
val asyncJob = async{
Log.d("async", "启动一个协程")
"我是 async 返回值"
}
Log.d("asyncJob.await", ":${asyncJob.await()}")
Log.d("asyncJob", "$asyncJob")
}
}
现在我们通过GlobalScope.launch
启动里一个协程,同时在协程体里面通过launch
直接又启动了 2 个协程。为什么我们没有在协程体使用GlobalScope.launch
启动,而是使用launch
直接启动。前面我们提到过调用launch
必须要在协程作用域(Coroutine Scope
)中才能调用,因为通过runBlocking
、launch
和async
启动的协程体等同于协程作用域,所以这里我们就可以直接使用launch
启动一个协程。我们运行一下,接着看看日志输出:
D/launchJob: StandaloneCoroutine{Active}@f3d8da3
D/launch: 启动一个协程
D/async: 启动一个协程
D/await: :我是 async 返回值
D/asyncJob: DeferredCoroutine{Completed}@d6f28a0
也有可能是这样的
D/launchJob: StandaloneCoroutine{Active}@f3d8da3
D/async: 启动一个协程
D/launch: 启动一个协程
D/asyncJob.await: :我是 async 返回值
D/asyncJob: DeferredCoroutine{Completed}@d6f28a0
现在我们看到asyncJob.await
也是输出我们之前定义好的返回值,同时DeferredCoroutine
的状态变成了{Completed}
,这是因为await()
是在不阻塞线程的情况下等待该值的完成并继续执行,当deferred
计算完成后返回结果值,或者如果deferred
被取消,则抛出相应的异常CancellationException
。但是又因为await()
是挂起函数,他会挂起调用他的协程。所以我们看到的DeferredCoroutine
的状态是{Completed}
,同时输出的await
日志也是在最后面。
好了,至此。我们对runBlocking
、launch
、async
的相关介绍就到这里了。
[](
)Android 中的协程并发与同步
现在我们回过头来看,我们在上面提到过:“因为协程采用的是并发设计模式,所以导致launch
和async
的协程体内的 log 日志输出是无序方式(这样说是不严谨
)”。
因为协程是采用就是并发的设计模式,这句话的大多数环境下是没有问题。但是,但是,但是,这里需要注意的小细节来了。如果某个协程满足以下几点,那它里面的子协程将会是同步执行的:
父协程的协程调度器是处于
Dispatchers.Main
情况下启动。同时子协程在不修改协程调度器下的情况下启动。
private fun start() {
GlobalScope.launch(Dispatchers.Main) {
for (index in 1 until 10) {
//同步执行
launch {
Log.d("launch$index", "启动一个协程")
}
}
}
}
D/launch1: 启动一个协程
D/launch2: 启动一个协程
D/launch3: 启动一个协程
D/launch4: 启动一个协程
D/launch5: 启动一个协程
D/launch6: 启动一个协程
D/launch7: 启动一个协程
D/launch8: 启动一个协程
D/launch9: 启动一个协程
private fun start() {
GlobalScope.launch {
for (index in 1 until 10) {
//并发执行
launch {
Log.d("launch$index", "启动一个协程")
}
}
}
}
D/launch1: 启动一个协程
D/launch2: 启动一个协程
D/launch3: 启动一个协程
D/launch4: 启动一个协程
D/launch5: 启动一个协程
D/launch6: 启动一个协程
D/launch9: 启动一个协程
D/launch7: 启动一个协程
D/launch8: 启动一个协程
那么子协程将是同步执行的,这是在 Android 平台上如果协程处于Dispatchers.Main
调度器,它会将协程调度到 UI 事件循环中执行,即通常在主线程上执行,这样就能理解为什么是同步执行了吧。如果是不同步的话,那我在操作 UI 刷新的时候,就会出现各种问题啦!
如果其中的某一个子协程将他的协程调度器修改为非Dispatchers.Main
,那么这个子协程将会与其他子协程并发执行,这里我们就不在演示,各位看官可以自己动手试验一下。毕竟知识光看不动手,是很难将知识吸收到位的。
下一本章节我们将在会对以下知识点做初步讲解,包含上文提到的launch
和async
函数中的 3 个参数作用。清单如下:
协程调度器
CoroutineDispatcher
协程下上文
CoroutineContext
作用协程启动模式
CoroutineStart
协程作用域
CoroutineScope
挂起函数以及
suspend
关键字的作用
如果你想学习 kotlin 又缺少学习资料,我正好薅到这本谷歌内部大佬根据实战编写的 Kotlin 宝典,从入门到精通,教程通俗易懂,实例丰富,既有基础知识,也有进阶技能,能够帮助读者快速入门,是你学习 Kotlin 的葵花宝典,快收藏起来!!!
今天分享的一共分为两部分:【字节跳动厂内部超高质量 Kotlin 笔记】、【谷歌大佬编写高级 Kotlin 强化实战(附 Demo)】。
评论