环境配置
//依赖协程核心库 ,提供Android UI调度器
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
开始使用
先看第一个例子:
GlobalScope.launch { // 在后台启动一个新的协程并继续
delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
Log.e("hyh","thread-->${Thread.currentThread().name}")// 在延迟后打印输出
//输出:thread-->DefaultDispatcher-worker-1
}
咱们一点一点来解析,先看GlobalScope
GlobalScope
public object GlobalScope : CoroutineScope {
/**
* Returns [EmptyCoroutineContext].
*/
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
他就是一个静态类,继承了CoroutineScope
public interface CoroutineScope {
/**
* The context of this scope.
* Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
* Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
*
* By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
*/
public val coroutineContext: CoroutineContext
}
里面就一个CoroutineContext变量,而是还是空的,这个CoroutineContext现在可以理解为当前协程环境,还是回到GlobalScope 类,先初始化一个EmptyCoroutineContext,大家记住这个coroutineContext变量就行,具体什么用处后面会讲解。
launch
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
这是CoroutineScope的扩展函数,大家就会问那为什么CoroutineScope.launch这样调用呢,看CoroutineScope源码,CoroutineScope就是一个接口类,不能new又不是静态类,所以没办法直接调用,那为什么不把扩展函数写到GlobalScope 里面,因为实现CoroutineScope接口类不止这一个类,还有其他开启协程的方式,后续会讲解。
有三个参数分别为:
- CoroutineContext:当前协程环境,设置线程环境
- CoroutineStart:是一个枚举变量,先不关心
- suspend CoroutineScope.() -> Unit:一个回调函数
这三个参数可以先初步这样理解,后续讲解源码时,会着重介绍,这样就开启了一个协程,从打印里面的线程号可以得到,当前协程是运行在一个子线程里面,开启一个协程就是这样方便,上一章咱们讲过,协程最大的优势是简化了并发操作,不使用回调就能做到线程切换,看下面例子:
GlobalScope.launch(Dispatchers.Main) { // 在后台启动一个新的协程并继续
Log.e("hyh", "thread-1->${Thread.currentThread().name}")// 在延迟后打印输出
val result = withContext(Dispatchers.IO) {
task1()
Log.e("hyh", "thread-2->${Thread.currentThread().name}")// 在延迟后打印输出
}
Log.e("hyh", "thread-3->${Thread.currentThread().name}")// 在延迟后打印输出
}
private fun task1(): String {
Thread.sleep(1000)
return "111"
}
打印日志:
: thread-1->main
: thread-2->DefaultDispatcher-worker-1
: thread-3->main
看到这里是不是就有疑惑,“3”怎么会延迟打印呢,这不相当于把主线程阻塞了,主要操作是withContext函数里面的操作,实际上直接讲这个函数里面有很多函数难理解,咱们换个写法:
GlobalScope.launch(Dispatchers.Main) { // 在后台启动一个新的协程并继续
Log.e("hyh", "thread-1->${Thread.currentThread().name}")// 在延迟后打印输出
task1()
Log.e("hyh", "thread-3->${Thread.currentThread().name}")// 在延迟后打印输出
}
private suspend fun task1(): String {
delay(1000)
Log.e("hyh", "thread-2->${Thread.currentThread().name}")// 在延迟后打印输出
return "111"
}
下面就开始讲解协程最主要的设计理念,协程的挂起和恢复。
挂起与恢复
挂起和恢复定义:
在协程中,当我们的代码执行到某个位置时,可以使用特定的关键字来暂停函数的执行,同时保存函数的执行状态,这个过程叫做 [挂起],挂起操作会将控制器交还给调用方,调用方可以继续执行其他任务。
当再次调用被挂起的函数时,它会从上一次暂停的位置开始继续执行,这个过程称为 [恢复]。在恢复操作之后,被挂起的函数会继续执行之前保存的状态,从而可以在不重新计算的情况下继续执行之前的逻辑。
在kotlin中,被suspend 修饰符标记的函数就是挂起函数,它可能会通过调用其他挂起函数挂起执行代码,而不阻塞当前执行线程。挂起函数不能在常规代码中被调用,只能在其他挂起函数。但并不是说加了这个关键字就一定会挂起,suspend 只是作为一个标记,用于告诉编译器,该函数可能会挂起并暂停执行,具体是否挂起就看函数里面的是否有挂起操作。
从上面的例子可以看到,delay函数也是一个挂起函数,里面有具体的挂起动作。先看一下挂起函数的java实现:
BuildersKt.launch$default((CoroutineScope)GlobalScope.INSTANCE, (CoroutineContext)Dispatchers.getIO(), (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
StringBuilder var10001;
Thread var10002;
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
var10001 = (new StringBuilder()).append("launch--1--thread-1->");
var10002 = Thread.currentThread();
Intrinsics.checkNotNullExpressionValue(var10002, "Thread.currentThread()");
Log.e("hyh", var10001.append(var10002.getName()).toString());
MainActivity var10000 = MainActivity.this;
this.label = 1;
if (var10000.task1(this) == var2) {
return var2;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
var10001 = (new StringBuilder()).append("launch--1--thread-3->");
var10002 = Thread.currentThread();
Intrinsics.checkNotNullExpressionValue(var10002, "Thread.currentThread()");
Log.e("hyh", var10001.append(var10002.getName()).toString());
return Unit.INSTANCE;
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), 2, (Object)null);
private final Object task1(Continuation var1) {
Object $continuation;
label20: {
if (var1 instanceof <undefinedtype>) {
$continuation = (<undefinedtype>)var1;
if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
break label20;
}
}
$continuation = new ContinuationImpl(var1) {
// $FF: synthetic field
Object result;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return MainActivity.this.task1(this);
}
};
}
Object $result = ((<undefinedtype>)$continuation).result;
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(((<undefinedtype>)$continuation).label) {
case 0:
ResultKt.throwOnFailure($result);
((<undefinedtype>)$continuation).label = 1;
if (DelayKt.delay(2000L, (Continuation)$continuation) == var4) {
return var4;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
StringBuilder var10001 = (new StringBuilder()).append("launch--1--thread-2->");
Thread var10002 = Thread.currentThread();
Intrinsics.checkNotNullExpressionValue(var10002, "Thread.currentThread()");
Log.e("hyh", var10001.append(var10002.getName()).toString());
return "111";
}
从上面代码可以看到suspend修饰的方法会加入一个Continuation 类(续体)信息,关于怎么创建Continuation类的,Kotlin源码可读性不是太好,这个地方后续会进行专门讲解,大家可以先理解成,Continuation 就是一个callback类,执行步骤为:
- 先配置一些环境参数,并且创建Continuation(这是个接口,内部有具体的实现类),从外部(内部调用逻辑很复杂,可以先不操心)看调用流程是:invoke-》create-》invokeSuspend,协程体内的代码都在invokeSuspend,相当于协程内执行的代码是在invokeSuspend里面
2:执行到invokeSuspend里面,里面有一个switch用来判断状态,这个就是状态机,根据不同的状态执行不懂的代码逻辑,刚开始时状态为0,先执行挂起点之前的代码,执行完以后,进入挂起函数task1
3.task1形参里面会让传入Continuation,执行到delay之前时,还没有进行挂起,所以状态码为0,开始执行delay,由于delay是真正能挂起的函数,会返回一个状态码为:IntrinsicsKt.getCOROUTINE_SUSPENDED(),代表已挂起,然后返回,这个时候会再次进入第一个挂起函数
if (var10000.task1(this) == var2) {
return var2;
}
,协程方法已经结束,当前协程方法已经挂起,大家可以看到当前方法里面还有代码没执行,所以说协程的挂起本质上是方法的挂起,而方法的挂起本质是return。
4.当真正挂起函数执行完后,会通过Continuation调用到invokeSuspend方法,由于当前的状态码为1,所以就会执行剩余的代码,所以说协程的恢复本质上方法的恢复,而恢复的本质是 callback 回调。
关于是怎么实现的,后续会讲解具体的源码实现,学到这里基本上已经知道协程的挂起和恢复基本理念了。
我当时学到这里的时候,还发现一个问题,协程体是运行在线程里面的,当挂起时,当前线程在干什么,恢复时是恢复到哪个线程里面?
先讲解IO线程:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
GlobalScope.launch(Dispatchers.IO) { // 在后台启动一个新的协程并继续
Log.e("hyh", "launch--1--thread-1->${Thread.currentThread().name}")// 在延迟后打印输出
task1()
Log.e("hyh", "launch--1--thread-3->${Thread.currentThread().name}")// 在延迟后打印输出
}
Thread.sleep(1000)
GlobalScope.launch(Dispatchers.IO) { // 在后台启动一个新的协程并继续
Log.e("hyh", "launch--2--thread-1->${Thread.currentThread().name}")// 在延迟后打印输出
Thread.sleep(3000)
}
}
private suspend fun task1(): String {
delay(2000)
Log.e("hyh", "launch--1--thread-2->${Thread.currentThread().name}")// 在延迟后打印输出
return "111"
}
上面是我写的的测试代码,执行结果为:
第一次运行结果:
launch--1--thread-1->DefaultDispatcher-worker-1
launch--2--thread-1->DefaultDispatcher-worker-3
launch--1--thread-2->DefaultDispatcher-worker-1
launch--1--thread-3->DefaultDispatcher-worker-1
第二次运行结果:
launch--1--thread-1->DefaultDispatcher-worker-1
launch--2--thread-1->DefaultDispatcher-worker-1
launch--1--thread-2->DefaultDispatcher-worker-3
launch--1--thread-3->DefaultDispatcher-worker-3
从这两次结果接可以看到,当前协程所在的线程会空置出来,就进入到闲置线程队列里面,如果还有一个线程需要执行协程代码,就可以使用当前闲置的线程,恢复时,也是会检测到哪个线程闲置,就会使用闲置线程来执行,这里要注意,主线程恢复逻辑不是这样的,挂起点是在主线程时,恢复时也会在主线程恢复。