协程
ABSTRACT
协程相关
- 轻量:占用内存少
- 减少回调:以同步的方式执行异步代码(会将函数挂起等待结果)
状态机原理
suspend fun getData(): String {
delay(1000)
return "hello coroutines"
}
反编译
// suspend挂起函数,会主动在方法中添加Continuation类型参数
@Nullable
public final Object getData(@NotNull Continuation $completion) {
Continuation $continuation;
label20: {
/**
* 如果传入的Continuation是<undefinedtype>类型,说明是第一次调用
* 如果不是,则说明是恢复调用,从$continuation中获取状态机的状态
*/
if ($completion instanceof <undefinedtype>) {
$continuation = (<undefinedtype>)$completion;
if (($continuation.label & Integer.MIN_VALUE) != 0) {
$continuation.label -= Integer.MIN_VALUE;
break label20;
}
}
$continuation = new ContinuationImpl($completion) {
// $FF: synthetic field
Object result; // 存储中间结果
int label; // 标签状态
// 自己invoke方法,实现状态机恢复逻辑
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return MainActivity.this.getData((Continuation)this);
}
};
}
// 开始执行逻辑
Object $result = $continuation.result;
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
// 根据continuation的标签位,进行对应的操作
switch ($continuation.label) {
case 0:
// 检查结果是否是异常,如果是则抛出
ResultKt.throwOnFailure($result);
// 设置下一个标签
$continuation.label = 1;
// 执行delay挂起操作,delay操作里面会执行continuation.resumeWith(Result.success(Unit))操作,这里会调用continuation的invokeSuspend方法
if (DelayKt.delay(1000L, $continuation) == var4) {
return var4;
}
break;
case 1:
// 继续检查,无误跳出switch,执行后续逻辑
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
return "hello coroutines";
}
上述操作被称为挂起函数的CSP操作
协程依赖于底层的状态机实现挂起和恢复,当遇到挂起点时,会去将当前协程挂起,并更新其内部的状态,将continuation的label标签设置为下一个要执行的代码块编号,当要恢复的时候,协程内部会调用**continuation的resumWith方法,去恢复协程运行,状态机根据保存的label值跳转到对应位置继续执行后续代码。**
挂起函数
delay
delay(1000)
挂起1s
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
// if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
if (timeMillis < Long.MAX_VALUE) {
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
}
withContext
withContext(Dispatchers.IO) {
}
切换协程执行上下文(改变其运行的线程)
async和await
val dataDeferred = GlobalScope.async {
"xxx"
}
val data = dataDeferred .await()
开启并发协程并获取结果
launch
GlobalScope.launch {
}
并不是传统的挂起函数,可以设置协程上下文,以及协程启动方式
协程上下文
- Dispatchers.Main:Android专属,主线程,协程代码通过封装成Runnable执行
- Dispatchers.IO:IO密集,网络请求,数据库操作,文件读取等IO操作
- Dispatchers.Default:CPU密集,如大量计算,处理数据(注意:不能放到IO里面,会阻塞网络请求)
- Dispatchers.Unconfined:纯挂起逻辑、无耗时的协程调度
- Dispatchers.Main.immediate:Android专属,主线程内的挂起后立即执行逻辑
注意:协程中唯一推荐的切换上下文的方法就是
withContext
协程的启动方式
- DEFAULT - 立即调度
launch(start = CoroutineStart.DEFAULT) { // 创建后立即进入调度队列 }
- LAZY - 懒启动
val job = launch(start = CoroutineStart.LAZY) { // 需要手动启动 } job.start() // 启动协程,进入 ACTIVE 状态
- ATOMIC - 原子启动(不可取消)
launch(start = CoroutineStart.ATOMIC) { // 启动后立即执行,不可取消 }
- UNDISPATCHED - 立即在当前线程执行
launch(start = CoroutineStart.UNDISPATCHED) { // 立即在当前线程执行,直到第一个挂起点 }
runBlocking
runBlocking {
}
阻塞线程的挂起函数
生命周期
New (新建) → Active (活跃) → Completing (完成中) → Completed (已完成)
↓
Cancelling (取消中) → Cancelled (已取消)
suspend fun getData() {
val job = CoroutineScope(Dispatchers.IO).launch(
// 懒启动
start = CoroutineStart.LAZY
) {
Log.d(TAG, "getData() is running..." + Thread.currentThread().name)
}
// 来启动协程,此时其状态为NEW
val job1 = CoroutineScope(Dispatchers.IO).launch {
// 处于ACTIVE状态
Log.d(TAG, "getData() is running..." + Thread.currentThread().name)
delay(1000) // 挂起点,仍然处于ACTIVE状态
Log.d(TAG, "getData() is running..." + Thread.currentThread().name)
// 如果没有异常和取消,进入 COMPLETING状态
}
val job2 = CoroutineScope(Dispatchers.IO).launch {
try {
repeat(100) { i ->
Log.d(TAG, "job: I am $i")
delay(500)
}
} finally {
// 协程取消仍然会执行
Log.d(TAG, "job: I am running finally")
withContext(NonCancellable) {
delay(1000)
}
}
}
delay(1300)
job2.cancel() // 取消协程,进入 CANCELING 状态
job2.join() // 等待协程结束,进入 CANCELLED 状态
}
状态监听
val job = launch {
// 协程执行
}
// 1. 监听完成
job.invokeOnCompletion { cause ->
println("协程完成,原因: $cause")
}
// 2. 检查状态
println("是否活跃: ${job.isActive}")
println("是否完成: ${job.isCompleted}")
println("是否取消: ${job.isCancelled}")
// 3. 等待完成
runBlocking {
job.join() // 挂起直到协程完成
}
协程作用域
@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())
定义协程的生命周期以及执行的上下文
- 生命周期管理:作用域取消时,内部所有协程自动取消
- 结构化并发:组织协程的父子关系
- 上下文传播:提供默认的调度器、异常处理器等
GlobalScope
生命周期与应用一样,尽量避免使用,容易造成内存泄漏
MainScope
用于非Android环境的主线程作用域
private val scope = MainScope() // Dispatchers.Main + SupervisorJob()
ViewModelScope
适用于viewmodel的作用域
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0")
viewModelScope.launch {
// 里面的内容会随着viewmodel被clear而取消
}
class MainViewModel: ViewModel() {
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// 如果要自定义的话,记得取消作用域
override fun onCleared() {
super.onCleared()
scope.cancel()
}
}
LifecycleScope
适用于activity的作用域
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.0")
lifecycleScope.launch {
// 里面的内容会随着activity被destory而取消
}
// 以下方法在生命周期离开对应状态时会挂起协程
lifecycleScope.launchWhenCreated { } // CREATED状态
lifecycleScope.launchWhenStarted { } // STARTED状态
lifecycleScope.launchWhenResumed { } // RESUMED状态
自定义Scope
class MyCustomScope : CoroutineScope {
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job + CoroutineName("MyScope")
fun cleanup() {
job.cancel() // 取消所有子协程
}
}
SupervisorJob vs Job
// 1. 使用 Job(默认)- 子协程异常会传播
val scope1 = CoroutineScope(Job() + Dispatchers.Main)
scope1.launch {
launch {
throw Exception() // 会取消父协程和其他子协程
}
launch {
delay(1000) // 这个不会执行,因为被异常取消了
}
}
// 2. 使用 SupervisorJob - 子协程异常隔离
val scope2 = CoroutineScope(SupervisorJob() + Dispatchers.Main)
scope2.launch {
launch {
throw Exception() // 只影响自己
}
launch {
delay(1000) // 这个正常执行
println("我还活着")
}
}