本文想要尽可能简单的说一些有关协程的知识
上篇文章太过长,内容涉及较多,没有控制好:协程初探
希望这篇会更简易一些, 主要涉及到以下内容:
- 协程的基本使用方式;
- 协程的
suspend
实现原理; - 协程的部分实现原理初探
- 协程和
LiveData
结合使用 -
Android
中的协程调度器;
放入一张类继承图镇楼:
常见类继承图
1. 协程的基本使用方式
1.1 CoroutineScope
是什么?
CoroutineScope
是一个接口,它为协程定义了一个范围「或者称为 作用域
」,每一种协程创建的方式都是它的一个扩展「方法」。
目前常用的两种方式:
launch
async
重要!!,圈起来:
-
Kotlin
中规定协程必须在CoroutineScope
中运行; -
Kotlin
中规定协程只有在CoroutineScope
才能被创建
所以,当我们使用协程时,必须要通过 CoroutineScope
去实现。
1.2 使用CoroutineScope
实现协程的两种方式
常见有以下两种方式:
-
手动实现
CoroutineScope
接口 -
在
ViewModel
中实现协程
手动实现 CoroutineScope
接口
本质:我们需要在当前类中实现 CoroutineScope
接口.
在 Activity
里,你可以这么使用:
class MyActivity : AppCompatActivity(), CoroutineScope by MainScope() {
override fun onDestroy() {
cancel() // cancel is extension on CoroutineScope
}
// 实现接口后,便可以调用它的扩展方法去创建协程.
fun showSomeData() {
launch {
// <- extension on current activity, launched in the main thread
// ... here we can use suspending functions or coroutine builders with other dispatchers
draw(data) // draw in the main thread
}
}
}
或者:
class DemoTestActivity : AppCompatActivity(), CoroutineScope {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override val coroutineContext: CoroutineContext
get() = MainScope().coroutineContext
private fun test() {
launch {
// 新建一个协程,do something
}
}
}
实现该接口的注意事项:
- 当前的类,必须要是一个定义好的,带有生命周期的对象
->
便于我们释放协程。
有些时候,根据需求会需要你实现 CoroutineScope
, 在你定义的生命周期里,例如和 Application
的生命周期一致,在后台继续工作。
在 ViewModel
中实现协程
代码示例如下:
/**
* 有关协程测试的 demo
*
*/
class CoroutineDemoViewModel : ViewModel() {
/**
* 开启协程方式: 1. launch; 2. async
*/
fun startCoroutine() {
// viewModelScope 是 ViewModel 的一个成员变量「扩展而来」
viewModelScope.launch(Dispatchers.IO) {
delay(1000)
// async
val result = async {
delay(2000)
}
result.await()
}
}
suspend fun test() {
coroutineScope {
}
}
}
首先, viewModelScope
是 ViewModel
的一个扩展的成员变量,是 CoroutineScope
的一个对象实例。
也就是说,在 ViewModel
中,默认帮忙开发者创建了这么一个对象,也是为了便于在 ViewModel
中使用协程。
为什么推荐在 ViewModel
中使用呢
-
ViewModel
是具有生命周期的,跟随当前的Activity
或者Fragment
; -
ViewModel
本身是为了处理一些耗时操作设计的,从UI
中剥离出来; -
ViewModel
在销毁时,同时会销毁它里面所有正在运行的协程;
1.3 ViewModel
自动销毁 CoroutineScope
的逻辑
在 ViewModel
中是会自动释放协程的,那么是如何实现的呢?
viewModelScope()
源码如下:
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
}
其中 setTagIfAbsent(xxx)
会把当前 CloseableCoroutineScope
存放在 mBagOfTags
这个 HashMap
中。
当 ViewModel
被销毁时会走 clear()
方法:
MainThread
final void clear() {
mCleared = true;
// Since clear() is final, this method is still called on mock objects
// and in those cases, mBagOfTags is null. It'll always be empty though
// because setTagIfAbsent and getTag are not final so we can skip
// clearing it
if (mBagOfTags != null) {
synchronized (mBagOfTags) {
for (Object value : mBagOfTags.values()) {
// see comment for the similar call in setTagIfAbsent
closeWithRuntimeException(value);
}
}
}
onCleared();
}
这里,会把 mBagOfTags
这个 Map
中的所有 value
取出来,做一个 close
操作,也就是在这里,对我们的 coroutinesScope
做了 close()
操作,从而取消它以及取消它里面的所有协程。
2. 协程的代码运行过程分析
首先明确几个概念:
-
Coroutine
协程中是可以新建协程的「不断套娃」; - 什么时候协程算运行结束了呢?当它运行结束,并且它所有的子协程执行结束才算结束;
- 同时……当里面的某个子协程发生异常时,整个协程都会停止运行,抛出异常;
-
suspend
关键字标注的方法,只能被协程里或者另外一个suspend
方法调用;
关键字 suspend
的意义:「挂起」
- 当代码运行到这里时,会挂起当前的协程,不在继续向下执行;
- 直到该方法运行结束, 协程恢复,继续往运行。
下面我们使用具体的代码,看一看,具体的实现逻辑是怎样的。
2.1 协程示例代码运行
在 Fragment
中使用 ViewModel
, 代码如下:
testHandle()
并没有调用,因为println("MainFragment after startCoroutines...
已经能说明问题。
ViewModel
中的代码,运行代码如下:
class CoroutinesDemoViewModel : ViewModel() {
fun startCoroutines() {
println("startCoroutines start current thread is ${Thread.currentThread()}")
// viewModelScope 是 ViewModel 的一个成员变量「扩展而来」
viewModelScope.launch(Dispatchers.IO) {
println("start current thread is ${Thread.currentThread()}")
delay(1000)
println("end first delay current thread is ${Thread.currentThread()}")
// time1 为里面代码块运行的时间
val time1 = measureTimeMillis {
val answer1Result = getAnswer1()
val answer2Result = getAnswer2()
val answerLaunchFinal = answer1Result + answer2Result
println("answerLaunchFinal is $answerLaunchFinal")
withContext(Dispatchers.Main) {
println("withContext 第一次切换到主线程 current thread is ${Thread.currentThread()}")
// do something post value 之类, 处理 answerLaunchFinal
}
}
println(" time1 the result is $time1")
println("time1 end 串行调用结束 current thread is ${Thread.currentThread()}")
val time2 = measureTimeMillis {
println("time2 内部 answer1 前面 current thread is ${Thread.currentThread()}")
val answer1 = async {
println("async getAnswer1() start current thread is ${Thread.currentThread()}")
getAnswer1()
}
val answer2 = async {
println("async getAnswer2() start current thread is ${Thread.currentThread()}")
getAnswer2()
}
// 这种实现是并发的,更快速
val answerFinal = answer1.await() + answer2.await()
println("the answer final is $answerFinal")
withContext(Dispatchers.Main) {
println("withContext 第二次切换到主线程 current thread is ${Thread.currentThread()}")
// do something post value 之类, 处理 answerLaunchFinal
}
}
println(" time2 the result is $time2")
println("time2 end 并行调用结束 current thread is ${Thread.currentThread()}")
}
println("startCoroutines end current thread is ${Thread.currentThread()}")
}
....
}
为了避免大段大段的代码引起不适
其中代码 getAnswer1()
和 getAnswer2()
如下:
private suspend fun getAnswer1(): Int {
println("getAnswer1() start current thread is ${Thread.currentThread()}")
// do something
delay(1000)
println("getAnswer1() end current thread is ${Thread.currentThread()}")
return 11
}
private suspend fun getAnswer2(): Int {
println("getAnswer2() start current thread is ${Thread.currentThread()}")
// do something
delay(1000)
println("getAnswer2() end current thread is ${Thread.currentThread()}")
return 12
}
拷贝一下运行结果的代码:
I/System.out: MainFragment current thread is Thread[main,5,main]
startCoroutines start current thread is Thread[main,5,main]
startCoroutines end current thread is Thread[main,5,main]
I/System.out: MainFragment after startCoroutines current thread is Thread[main,5,main]
I/System.out: start current thread is Thread[DefaultDispatcher-worker-4,5,main]
I/System.out: end first delay current thread is Thread[DefaultDispatcher-worker-10,5,main]
getAnswer1() start current thread is Thread[DefaultDispatcher-worker-10,5,main]
I/System.out: getAnswer1() end current thread is Thread[DefaultDispatcher-worker-3,5,main]
I/System.out: getAnswer2() start current thread is Thread[DefaultDispatcher-worker-3,5,main]
I/System.out: getAnswer2() end current thread is Thread[DefaultDispatcher-worker-3,5,main]
answerLaunchFinal is 23
I/System.out: withContext 第一次切换到主线程 current thread is Thread[main,5,main]
I/System.out: time1 the result is 2026
time1 end 串行调用结束 current thread is Thread[DefaultDispatcher-worker-10,5,main]
time2 内部 answer1 前面 current thread is Thread[DefaultDispatcher-worker-10,5,main]
I/System.out: async getAnswer1() start current thread is Thread[DefaultDispatcher-worker-1,5,main]
getAnswer1() start current thread is Thread[DefaultDispatcher-worker-1,5,main]
I/System.out: async getAnswer2() start current thread is Thread[DefaultDispatcher-worker-8,5,main]
getAnswer2() start current thread is Thread[DefaultDispatcher-worker-8,5,main]
I/System.out: getAnswer1() end current thread is Thread[DefaultDispatcher-worker-4,5,main]
I/System.out: getAnswer2() end current thread is Thread[DefaultDispatcher-worker-10,5,main]
I/System.out: the answer final is 23
I/System.out: withContext 第二次切换到主线程 current thread is Thread[main,5,main]
I/System.out: time2 the result is 1020
I/System.out: time2 end 并行调用结束 current thread is Thread[DefaultDispatcher-worker-11,5,main]
运行结果原截图如下:
协程 log 信息上面的全部代码以及运行结果可以暂时不全部熟悉「太长不看……」,
下面我会按照 log
和对应的代码,来说明在协程中,我们需要注意的点。
2.2 协程代码运行结果分析
1.在协程中 suspend
标注的方法会在此处等待结果的返回
该协程中的剩余代码不会继续往下走,而是会在此处等待结果返回。
从上面的 log
的可以看出;
getAnswer1() start current thread is Thread[DefaultDispatcher-worker-10,5,main]
I/System.out: getAnswer1() end current thread is Thread[DefaultDispatcher-worker-3,5,main]
//注释: suspend 的 getAnswer1() 结束后,才会运行 getAnswer2() //
I/System.out: getAnswer2() start current thread is Thread[DefaultDispatcher-worker-3,5,main]
可以看到在协程中 suspend
标注的 getAnswer1()
,需要等到 getAnswer1()
结束,即 end
后,才会开始运行 getAnswer2()
的代码。
结论: suspend
的作用:把当前协程挂起,等待 suspend
运行结束后,协程恢复,继续运行。
2. aync
是并发的
从上面的 log 中可以看到:
// 截图部分 log 如下:
I/System.out: time1 the result is 2026
...
I/System.out: time2 the result is 1020
time1
和 time2
是两种方式所花费的时间,time2 远小于 2000
, 说明 async
是异步的;
结论: aync
是并发运行的
3. 协程不会堵塞主线程
从上面的 log 中可以看到:
// 主线程 log
I/System.out: MainFragment current thread is Thread[main,5,main]
startCoroutines start current thread is Thread[main,5,main]
startCoroutines end current thread is Thread[main,5,main]
// 主线程 log
I/System.out: MainFragment after startCoroutines current thread is Thread[main,5,main]
// 开启协程的地方
I/System.out: start current thread is Thread[DefaultDispatcher-worker-4,5,main]
代码中,start current thread
是在 startCoroutines end current
前面的;
从这里可以看出,协程的运行不会堵塞主线程的运行。
实际上,协程不会堵塞线程的运行
。
结论:协程的挂起与线程的执行状态没有任何关系
Thread-A
为执行协程coroutine-a
的线程名称,当该coroutine-a
协程被挂起时,Thread-A
可能会转去做其他事情,Thread-A
的状态与coroutine-a
的状态 没有关系。
下面我们就会说到 CoroutineDispatcher
协程调度器。
4. 调度器中 DefaultScheduler.IO
不止一个线程
首先打印的 log
中 Thread[DefaultDispatcher-worker-2,5,main]
这三项分别是什么
这是源码 Thread.toString()
方法中的返回值:
- 第一个参数
DefaultDispatcher-worker-2
代表的是当前线程的名字getName()
. - 第二个参数
5
代表的是当前线程的优先级getPriority()
默认是5
. - 第三个参数
main
代表的是当前线程属于哪个线程组。
从上面的 log
中会发现:
getAnswer2() start current thread is Thread[DefaultDispatcher-worker-8,5,main]
I/System.out: getAnswer1() end current thread is Thread[DefaultDispatcher-worker-4,5,main]
I/System.out: getAnswer2() end current thread is Thread[DefaultDispatcher-worker-10,5,main]
这三个 log
的第一项参数 getName()
均不同,分别为 DefaultDispatcher-worker-8
, DefaultDispatcher-worker-4
, DefaultDispatcher-worker-10
,它们是不同的子线程。
结论: 调度器中 DefaultScheduler.IO
里面不止一个线程。
每一次协程的挂起和恢复,都可能会伴随着
线程的切换
: 同一个调度池中线程的切换。
5. 每次协程挂起 「suspend
函数运行先后」,再次恢复时,会切换线程
首先 delay()
是个 suspend
挂起函数,可查看它的源码。
在如下 log
中:
getAnswer1() start current thread is Thread[DefaultDispatcher-worker-10,5,main]
I/System.out: getAnswer1() end current thread is Thread[DefaultDispatcher-worker-3,5,main]
在 getAnswer1()
中,在 delay()
函数前后,线程从 DefaultDispatcher-worker-10
切换为了 DefaultDispatcher-worker-3
;
6. suspend
函数在运行结束后,会自动切换到原来的协程调度器内
coroutine-a
协程被挂起,开启 coroutine-b
协程,本质上是,先切换为 coroutine-b
所在的 协程调度器内,然后在该调度器内调度一个线程给该协程运行,当再次恢复协程 coroutine-a
, 会在 coroutine-a
的调度器里面选择一个线程供协程运行。
下面这段代码:
代码withContext()运行的结果为:
I/System.out: getAnswer2() end current thread is Thread[DefaultDispatcher-worker-3,5,main]
answerLaunchFinal is 23
I/System.out: withContext 第一次切换到主线程 current thread is Thread[main,5,main]
I/System.out: time1 the result is 2026
time1 end 串行调用结束 current thread is Thread[DefaultDispatcher-worker-10,5,main]
画个图表说明一下:
代码 | 调度器 | 线程 | 协程 |
---|---|---|---|
getAnswer2() |
IO |
DefaultDispatcher-worker-3 |
coroutine-a |
withContext() 代码中 |
Main |
main thread |
coroutine-b |
withConxt() 代码后的 println
|
IO |
DefaultDispatcher-worker-10 |
coroutine-a |
可以看到在 withContext()
后,我们并没有手动指定该协程运行的 调度器
,但是,默认会切换回该协程原本的调度器。
结论:协程恢复后,会自动帮我们检测是否需要切换调度器,如果需要,则切换为原本协程的调度器,在其中线程池中选择一个线程,继续运行该协程
2.3 有关协程中重要运行节点的总结
总结一下,上述里面重要的点:
-
suspend
的作用:把当前协程挂起,等待suspend
运行结束后,协程恢复,继续运行; -
协程
aync
是并发的; -
协程的挂起与线程的执行状态没有任何关系;
-
调度器中
DefaultScheduler.IO
里面不止一个线程; -
每次协程挂起 「
suspend
函数运行先后」,再次恢复时,会切换线程; -
协程恢复后,会自动帮我们检测是否需要切换调度器,如果需要,则切换为原本协程的调度器,在其中线程池中选择一个线程,继续运行该协程。
3. 协程实现的原理
在第二部分中,我们分析了实际的协程代码运行,
同时也总结出了一些有关协程代码运行中重要的点,那么……
为什么呢?是怎么实现的呢?
希望能用最简单,朴实的话表达出来,不需要源码……
要看到上述的实现原理,需要 decompile to java
后才可以看清楚。
3.1 挂起函数 suspend
的实现原理
首先注意下一个变量:**COROUTINE_SUSPENDED
**
当返回值是它时,表示当前函数需要挂起,协程在这里会挂起,等待该函数运行结果返回。
首先看看 suspend
函数 decompile
后的样子
// $FF: synthetic method
@Nullable
final Object getAnswer2(@NotNull Continuation $completion) {
Object $continuation;
label20: {
if ($completion instanceof <undefinedtype>) {
$continuation = (<undefinedtype>)$completion;
if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
break label20;
}
}
$continuation = new ContinuationImpl($completion) {
// $FF: synthetic field
Object result;
int label;
Object L$0;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return TestDemo1.this.getAnswer2(this);
}
};
}
Object $result = ((<undefinedtype>)$continuation).result;
Object var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
String var2;
boolean var3;
switch(((<undefinedtype>)$continuation).label) {
case 0:
ResultKt.throwOnFailure($result);
var2 = "getAnswer2() start current thread is " + Thread.currentThread();
var3 = false;
System.out.println(var2);
((<undefinedtype>)$continuation).L$0 = this;
((<undefinedtype>)$continuation).label = 1;
if (DelayKt.delay(100L, (Continuation)$continuation) == var6) {
return var6;
}
break;
case 1:
TestDemo1 var7 = (TestDemo1)((<undefinedtype>)$continuation).L$0;
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
var2 = "getAnswer2() end current thread is " + Thread.currentThread();
var3 = false;
System.out.println(var2);
return Boxing.boxInt(12);
}
每一个 suspend
函数,在编译成 java
后,都会有一个 switch
的代码块:
-
在这里,如果你
suspend
函数中新增调用了suspend
函数,则会多出来一个case
; -
case 0
运行结束后,lable
会变成1
等待「下次运行」会走到switch case 1
这个里面; -
每次运行时
lable
都会不一样, 每次+1
操作; -
lable
是每次都会变化的,代表着接下来的switch
需要执行的语句块。
有种说法是:
suspend
方法编译之后,会将原来的方法体变为一个由switch
语句构成的状态机
根据每次的状态lable
去执行不同的代码。
那么「下次运行
」意味着什么呢?
意味着编译后的 getAnswer2()
会被多次调用,
而协程也正是使用了这种方式实现了「回调」。
结论:
1. 每个 suspend
方法在编译成java
后,它可能会被调用不止一次
2. 「挂起函数」的实现原理,仍然是我们熟悉的回调,只不是协程帮忙我们封装好了一套完整的回调流程
3.1.1 哪里触发了「挂起函数」的再次调用呢?
编译后的代码中,有这么一段:
$continuation = new ContinuationImpl($completion) {
/ $FF: synthetic field
Object result;
int label;
Object L$0;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return TestDemo1.this.getAnswer2(this);
}
};
每一个 continuation
「标志当前协程」都可以通过 invokeSuspend()
再次重新调用 getAnswer2()
方法;
invokeSuspend()
在哪里被调用的呢? 在 ContinuationImpl
的 resumeWith()
方法中被调用
协程的启动和再次恢复,都会调用 resume()
方法,
也是通过这种形式,实现了 getAnswer2()
在实际运行中会被调用多次,执行完它内部的逻辑;
也做到了让 getAnswer2()
看上去是顺序执行。
3.2 再来看一个问题: Continuation
是什么?
可以把它看做一个回调接口「自带了 回调环境 -> context: CoroutineContext
」而上文中,我们提到的 ContinuationImpl
正是 implement
了该接口。
Continuation
的源码如下:
public interface Continuation<in T> {
/**
* The context of the coroutine that corresponds to this continuation.
*/
public val context: CoroutineContext
/**
* Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
* return value of the last suspension point.
*/
public fun resumeWith(result: Result<T>)
}
Continuation
中关键的点:
-
context
可以记录当前协程需要在哪个回调环境中实现;通常为我们设置的「协程调度器」CoroutineDispatcher
CoroutineDispatcher
实际上是协程为什么会自动切换原来的调度器的关键。 -
resumeWith()
是它的关键方法,也是ContinuationImpl
主要重写的方法,在里面实现了协程的恢复操作
3.3 「挂起函数」A
调用「挂起函数」B
, 不一定 A
会挂起
在上面的代码中,其实,我们判断是否「 挂起函数」A
需要挂起,是根据:
// var6 的值是:COROUTINE_SUSPENDED
if (DelayKt.delay(100L, (Continuation)$continuation) == var6) {
return var6;
}
就是说,只有当 return COROUTINE_SUSPENDED
时才会挂起该函数;
如果返回值 DelayKt.delay()
的返回值不是 COROUTINE_SUSPENDED
, 则会顺序执行,继续执行下去。
结论:A
挂起的条件是:它调用的其他 suspend
函数返回值为 COROUTINE_SUSPENDED
, 否则不会挂起。
3.4 协程对象的传递和引用
上面 3.1~3.3
简单介绍了,suspend
挂起的相关知识。
当创建一个协程时,会通过调用 resume(Unit)
启动该协程, 得到 Continuation
当协程被挂起,再次恢复时,ContinuationImpl
调用 resumeWith()
恢复协程
在协程的创建中,还存在着 ContinuationInterceptor
,对协程进行拦截,而真正的拦截,是在 ContinuationImpl.intercepted()
中的拦截。
从上面的逻辑中,我们可以梳理出这样一段话:
- 在协程
A
中开启协程B
- 当协程
B
运行结束时,调用协程A
的resume
使得A
开始恢复 - 协程
A
是被谁持有了?,实际上是协程B
其实从下面这行代码中:
if (DelayKt.delay(100L, (Continuation)$continuation) == var6) {
return var6;
}
这里 continuation
是当前的协程对象, 在调用 delay(100)
函数时,会把它传递给 delay()
函数内部,也就是说 delay()
中持有调用它的协程的对象。
有关协程的调度问题
Continuation
加上 ContinuationInterceptor
拦截器,这是协程调度的关键;
ContinuationInterceptor
使用了 CoroutineDispatcher
的 interceptContinuation
拦截了所有协程运行操作。
CoroutineDispatcher
会使用 DispatchedContinuation
再次接管 Continuation
。
那么,是在哪里进行协程的切换,或者说 「协程调度器的切换的」?
看一个 DispatchedContinuation
的 resumeWith
方法:
override fun resumeWith(result: Result<T>) {
val context = continuation.context
val state = result.toState()
// isDispatchNeeded 方法至关重要,是判断是否需要切换「调度器的关键」
if (dispatcher.isDispatchNeeded(context)) {
_state = state
resumeMode = MODE_ATOMIC_DEFAULT
dispatcher.dispatch(context, this)
} else {
executeUnconfined(state, MODE_ATOMIC_DEFAULT) {
withCoroutineContext(this.context, countOrElement) {
continuation.resumeWith(result)
}
}
}
}
在 dispatcher
中有一个方法是 isDispatchNeeded(context)
, 参数为当前协程的 context
信息,根据我们给协程设置的调度器来决定要不要切换调度器。
continuation.context
里面是包含协程的CoroutineDispatcher
信息的
isDispatchNeeded()
方法至关重要, 它不仅在 DispatchedContinuation.resumeWith
中调用,同时也在 resumeCancellable
调用
4. 协程和 LiveData
的结合
上述代码中,只是简单的在 ViewModel
中使用了 LiveData
作为向 UI
层透传数据变化的方式。
其实可以更进一步,因为存在 lifecycle-livedata-ktx
这个库,我们可以通过它实现更多样的代码。
依赖库:
// livedata-ktx
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
liveData{}
包裹,基本使用如下:
注意事项:
- 使用
emit()
返回值
// 在 viewModel 里使用,
val bannerTest: LiveData<List<HomeBanner.BannerItemData>> = liveData(IO) {
val result = getBannerUseCase.getWanAndroidBanner()
if (result is Result.Success) {
emit(result.data)
}
}
当 bannerTest
被观察「viewModel.bannerTest.observe
」时,liveData()
里面的协程才会开始运行。
这种方式,只会让
liveData()
里面的协程运行一次,除非你再次手动监听才会触发;
因此适用于只需要单次请求的数据。
对于一个 LiveData
对象的数据,我们可以对它进行再次操作 switchMap
:
/**
* The current growZone selection.
*/
private val growZone = MutableLiveData<GrowZone>(NoGrowZone)
/**
* A list of plants that updates based on the current filter.
*/
val plants: LiveData<List<Plant>> = growZone.switchMap { growZone ->
if (growZone == NoGrowZone) {
plantRepository.plants
} else {
plantRepository.getPlantsWithGrowZone(growZone)
}
}
每当 growZone
发生变化时,会自动触发 growZone.switchMap
的执行, 去获取对应的信息,从而更新 plants
数据。
然后,我们在 UI
层观察该 liveData
的变化:
// 观察它的变化
viewModel.plants.observe(viewLifecycleOwner) { plants ->
// 刷新 UI,
adapter.submitList(plants)
}
参考资料:https://github.com/googlecodelabs/kotlin-coroutines
有时我们会在代码中这么写:
private val _bannerUILD = MutableLiveData<List<HomeBanner.BannerItemData>>()
// 对外暴露的 LD
val bannerUILD: LiveData<List<HomeBanner.BannerItemData>>
get() = _bannerUILD
为什么要同时存在 _bannerUILD
和 banerUILD
可以看到 :
-
_bannerUILD
是私有的,不对外暴露的,而banerUILD
是对UI
层真正暴露的LiveData
数据; -
_bannerUILD
是MutableLiveData
, 可以被修改数据的LiveData
; 而banerUILD
是LiveData
不可被修改数据的LiveData
, 保证了,我们向UI
层传递LiveData
的安全性,外部不可修改我们的数据信息。
5. CoroutineDispatcher
的种类
在最后,还是要提到一下,我们常用的 CoroutineDispatcher
协程调度器。
CoroutineDispatcher
是协程调度器, 它的种类,都在 Dispatchers
类里面,在 Android
中有一下四类:
-
Default: CoroutineDispatcher = createDefaultDispatcher()
默认的调度器, 在
Android
中对应的为「线程池」。
在新建的协程中,如果没有指定dispatcher
和ContinuationInterceptor
则默认会使用该dispatcher
。
线程池中会有多个线程。
适用场景:此调度程序经过了专门优化,适合在主线程之外执行占用大量CPU
资源的工作。用法示例包括对列表排序和解析JSON
-
Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
在主线程「
UI
线程」中的调度器。
只在主线程中, 单个线程。适用场景:使用此调度程序可在
Android
主线程上运行协程。此调度程序只能用于与界面交互和执行快速工作。示例包括调用suspend
函数、运行Android
界面框架操作,以及更新LiveData
对象。 -
Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
-
IO: CoroutineDispatcher = DefaultScheduler.IO
在
IO
线程的调度器,里面的执行逻辑会运行在IO
线程, 一般用于耗时的操作。
对应的是「线程池」,会有多个线程在里面。IO
和Default
共享了线程。适用场景: 此调度程序经过了专门优化,适合在主线程之外执行磁盘或网络
I/O
。示例包括使用Room
组件、从文件中读取数据或向文件中写入数据,以及运行任何网络操作。
6 总结
最大的遗憾是没能详细的说明协程的调度原理。
里面有很多是属于个人理解性质的结论,如果出现错误,或不妥之处,可直接说明。
期望不会对使用到协程的兄弟们造成知识的错误引导。
参考链接:
- https://ethanhua.github.io/2018/12/24/kotlin_coroutines/
- https://johnnyshieh.me/posts/kotlin-coroutine-deep-diving/
- https://www.jianshu.com/p/0aaf300ac0fe
- https://www.kotlincn.net/docs/reference/coroutines/coroutine-context-and-dispatchers.html
等……
2020.05.13 by chendroid
网友评论