揭开 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...

相关推荐
vivo高启强2 小时前
如何简单 hack agp 执行过程中的某个类
android
沐怡旸2 小时前
【底层机制】 Android ION内存分配器深度解析
android·面试
你听得到113 小时前
肝了半个月,我用 Flutter 写了个功能强大的图片编辑器,告别image_cropper
android·前端·flutter
KevinWang_3 小时前
Android 原生 app 和 WebView 如何交互?
android
用户69371750013843 小时前
Android Studio中Gradle、AGP、Java 版本关系:不再被构建折磨!
android·android studio
杨筱毅3 小时前
【底层机制】Android低内存管理机制深度解析
android·底层机制
二流小码农4 小时前
鸿蒙开发:this的指向问题
android·ios·harmonyos
循环不息优化不止4 小时前
Jetpack Compose 状态管理
android
友人.2276 小时前
Android 底部导航栏 (BottomNavigationView) 制作教程
android