揭开 kotlin 中协程的神秘面纱

文章目录

  • 前言
  • 为什么要有协程
  • 协程背后的魔法
  • 总结

一、前言

android 开发中,kotlin 是作为官方第一语言,kotlin 提供了很多的语法糖,还有高阶函数,另外最神奇的,要属协程,让开发人员可以通过"同步代码",轻松写出异步代码。

这里有个很关键的关键字 suspend,一旦被这个修饰的函数,就不是普通函数了,属于挂起函数。

也正是因为有这个挂起函数,才让"同步代码",能够神奇的做一些异步任务。

当然这背后都是编译器的功劳,当你加上关键字之后,编译器会帮你生成状态机类,并通过状态机,来完成这一整套魔幻的执行过程。

理解了背后的逻辑,写代码才不会心慌,才不会总觉得加个关键字,咋就能干异步任务了。

接下来,我们就一步步分析,suspend 背后的故事。

二、为什么要有协程

1、没有suspend 的情况

糟糕的回调地狱

cpp 复制代码
fun fetchData(callback: (Result)-> Unit){
	fetchUser{ user->
		 fetchProfile(user){ profile ->
		 		saveToDatabase(profile){ result->
		 			callback(result) //嵌套越来越深
		 		}
		}
	}
}
2、suspend 的情况

舒爽的同步风格

cpp 复制代码
suspend fun fetchData(): Result{
	val user = fetchUser()  //这里实际是异步的
	val profile = fetchProfile(user)
	return savaToDatabase(profile)
}

这里我们就带着以下疑问,来探究整个过程: 1、协程怎么做到,执行耗时任务,却又不阻塞线程 2、被挂起的函数,又怎么重新恢复 3、恢复后,怎么自动去执行后面的挂起函数

三、协程背后的魔法(suspend 原理分析)

1、简单的例子

代码逻辑很简单: 就是在协程内部,执行两个耗时的挂起函数

cpp 复制代码
package com.ssz.kotlindemo.grammar

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.ssz.kotlindemo.R
import kotlinx.coroutines.*

class CoroutineActivity : AppCompatActivity(){
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_coroutine);

       CoroutineScope(Dispatchers.IO).launch {
           Log.d("sszLog", "开始")
           delay(2000) //耗时任务
           delay(1000) //耗时任务
           Log.d("sszLog:", "结束")
       }

   }

}

用android studio 的查看kotlin 生成的字节码。也就是我们写的代码被编译之后的样子。操作步骤:菜单栏-> Tools -> Kotlin -> Show Kotlin Bytecode

字节码还是很不好看的,我们将它反编译成java。操作步骤: 在字节码 上方点击 Decompile,就能看到 java 代码如下

cpp 复制代码
package com.ssz.kotlindemo.grammar;

import android.os.Bundle;
....忽略...

public final class CoroutineActivity extends AppCompatActivity {
   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(1300025);
      BuildersKt.launch$default(CoroutineScopeKt.CoroutineScope((CoroutineContext)Dispatchers.getIO()), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
         int label;

         @Nullable
         public final Object invokeSuspend(@NotNull Object $result) {
            label17: {
               Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
               switch(this.label) {
               case 0:
                  ResultKt.throwOnFailure($result);
                  Log.d("sszLog", "开始");
                  this.label = 1;
                  if (DelayKt.delay(2000L, this) == var2) {
                     return var2;
                  }
                  break;
               case 1:
                  ResultKt.throwOnFailure($result);//主要是用于检查上一个挂起函数是否正常执行
                  break;
               case 2:
                  ResultKt.throwOnFailure($result);
                  break label17;
               default:
                  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
               }

               this.label = 2;
               if (DelayKt.delay(1000L, this) == var2) {
                  return var2;
               }
            }

            Log.d("sszLog:", "结束");
            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);
         }
      }), 3, (Object)null);
   }
}
2、上面代码是怎么执行的呢?
cpp 复制代码
协程框架 → invoke() → create() → 新状态机实例 → invokeSuspend()

调用栈如下:

at BuildersKt.launch$default(BuildersKt.kt:-1) // 生成的桥接方法

at BuildersKt.launch(Builders.kt:60) // 实际的launch方法 at AbstractCoroutine.start(AbstractCoroutine.kt:112) // 启动协程 at CoroutineStart.invoke(CoroutineStart.kt:145) // 启动模式

cpp 复制代码
BuildersKt.launch 会通过协程,CoroutineStart 去调用 invoke(Object var1, Object var2) ,
然后执行 ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);  

当走到这里,有两个关键点:
1、通过 create(var1, (Continuation) var2)  创建状态机实例. (这个在后面很有用)
2、调用 invokeSuspend 

当invokeSuspend 被调用,这样就会走到 switch(this.label) 的条件分支里面代码。

cpp 复制代码
 this.label = 1;
 if (DelayKt.delay(2000L, this) == var2) {
     return var2;
 }

这里有两件事情很重要,一个是 label 加1,这样等到耗时任务执行完,才能进入下一条件分支代码。

另外一个是 DelayKt.delay(2000L, this) ,这个会把this,也就是把状态机 丢给delay中,这样等到 delay 这个挂起函数执行完之后,会去调用 resumeWith ,最后再通过状态机调用到 invokeSuspend 这样,又能执行后面的挂起函数了,直到没有挂起函数,最终 return。

那还有一点 DelayKt.delay(2000L, this) == var2 这个怎么回事呢,这个其实就是当发现这个delay 是一个suspend 修饰的函数,也就是识别到是挂起函数,那么说明这里可能要耗时了,所以就直接跳出。也就是说,这里有耗时的代码,要先跳出去,避免线程阻塞。

cpp 复制代码
if (DelayKt.delay(2000L, this) == var2) {//这里var2其实是COROUTINE_SUSPENDED
     return var2;
 }

重新梳理一下:

cpp 复制代码
1、协程怎么做到,执行耗时任务,却又不阻塞线程?

delay(2000L, this) == var2 如果发现是挂起函数,会直接 return var2,
这也就是所谓的释放当前的线程,这也就是不会阻塞线程的关键。

2、被挂起的函数,又怎么重新恢复?

DelayKt.delay(2000L, this) 会把状态机,传递给耗时任务,
执行完再次回调invokeSuspend,回到原来挂起的地方。

3、恢复后,怎么自动去执行后面的挂起函数?

就是主要是通过label 这个状态,通过每执行完一次挂起函数,状态就递增,
当 invokeSuspend 再次被调用,就可以执行下一个挂起函数。
3、这里为了加深印象,我们简化一下:

核心流程: 挂起 和 恢复

cpp 复制代码
怎么挂起的呢:
状态机{ //实现了 Continuation
	invokeSuspend(){
		case 0: 
			this.label = 1 //设置下一个状态
			if(delay(2000L, this) == COROUTINE_SUSPENDED){ //传递this(也就是 Continuation)
				return COROUTINE_SUSPENDED; //挂起 (有耗时的任务,先不去理会它,这是挂起)
			}
		case 1:
		...
	}
	resumeWith(){//这个是Continuation 中的方法,因为状态机实际上是实现了 Continuation,自然就有这个方法了
	}
}


怎么恢复的呢:
//恢复时
delay(long milliseconds, Continuation continuation){//执行完耗时,就会调用resumeWith
	continuation.resumeWith(Result.success(Unit)) 
}	
resumeWith(){
	//this其实就是状态机,也就是进行了回调,这就是为什么,挂起为什么能够恢复的关键。
	this.invokeSuspend()//此时label = 1,就会去执行下一个挂起函数
}
4、如果还不是很清楚,再看看下面就豁然开朗了

更简单的理解:

cpp 复制代码
// 我们的状态机类可以这么去理解,其实是实现了Continuation接口
public class CoroutineStateMachine implements Continuation {
    int label;
    
    public void resumeWith(Result result) { //因为实现 Continuation,也就有了这个方法
        Object outcome = this.invokeSuspend(result);
        // ...
    }
    
    // 状态机逻辑
    public Object invokeSuspend(Object result) {
        switch(this.label) {
            // 状态逻辑...
        }
    }
}

也就是协程内部的代码,会被编译器转化成上面的代码,当识别到挂起函数,就会return 先挂起,避免阻塞线程,然后等到执行完毕,重新调用 resumeWith,也就是 invokeSuspend,这样就恢复到原来执行的位置了,因为label 递增了,就会执行下一个挂起函数,经过不断递增,及多次回调,把所有挂起执行完毕,就退出。

所以真的脏活,累活,是编译器帮忙处理了,本质上还是通过回调,来完成。

总结

1、介绍协程在kotlin 中的重要性

2、通过简单例子,揭开协程背后的魔法

3、简化流程,更深刻理解核心部分

如果对你有一点点帮助,那是值得高兴的事情。:)

我的csdn:blog.csdn.net/shenshizhon...

我的掘金:juejin.cn/user/428855...

相关推荐
阿巴斯甜6 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker7 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95278 小时前
Andorid Google 登录接入文档
android
黄林晴9 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android