当前位置: 首页 > 科技观察

下面说说协程的Suspend其实是挂了什么

时间:2023-03-13 07:05:38 科技观察

Kotlin编译器会为每个suspend函数生成一个状态机来管理协程的执行。协同程序简化了Android上的异步操作。如文档中所述,我们可以使用它们来管理异步任务,否则这些任务可能会阻塞主线程并导致应用程序崩溃。协程还有助于用命令式代码替换基于回调的API。作为示例,让我们首先看一下使用回调的异步代码。//只考虑快乐路径的简化代码funloginUser(userId:String,password:String,userResult:Callback){//AsyncallbacksuserRemoteDataSource.logUserIn{user->//SuccessfulnetworkrequestuserLocalDataSource.logUserIn(user){userDb->//ResultsavedinDBuserResult.success(userDb)}}}这些回调可以使用协程转换为顺序函数调用。suspendfunloginUser(userId:String,password:String):User{valuser=userRemoteDataSource.logUserIn(userId,password)valuserDb=userLocalDataSource.logUserIn(user)returnuserDb}在协程代码中,我们为函数添加了suspend修饰符。这将告诉编译器该函数需要在协程内执行。作为开发人员,您可以将挂起函数视为一个普通函数,但它的执行可能会在某个时刻暂停和恢复。简而言之,suspend是编译器生成的回调。与回调不同,协程提供了一种在线程之间切换和处理异常的简单方法。但是,当我们将一个函数标记为挂起时,编译器实际上在幕后做什么呢?Suspend到底做了什么回到loginUser的suspend函数,注意它调用的其他函数也是suspend函数。suspendfunloginUser(userId:String,password:String):User{valuser=userRemoteDataSource.logUserIn(userId,password)valuserDb=userLocalDataSource.logUserIn(user)returnuserDb}//UserRemoteDataSource.ktsuspendfunlogUserIn(userId:String,password:String):User//UserLocalDataSource.ktsuspendfunlogUserIn(userId:String):UserDb简而言之,Kotlin编译器将使用有限状态机(我们将在后面介绍)将挂起函数转换为回调实现的优化版本。没错,编译器会帮你写这些回调,它们的本质还是回调!Continuation的真面目挂起函数之间的通信方式是使用Continuation对象。Continuation只是一个带有一些额外信息的通用回调接口。正如我们稍后将看到的,它将代表挂起函数的衍生状态机。让我们看看它的定义。interfaceContinuation{publicvalcontext:CoroutineContextpublicfunresumeWith(value:Result)}context是延续中使用的CoroutineContext。resumeWith使用结果恢复协程的执行,结果可以包含导致暂停计算或异常的值。注意:从Kotlin1.3开始,您还可以使用扩展函数resume(value:T)和resumeWithException(exception:Throwable),它们是resumeWith调用的特殊版本。编译器将在函数签名中用一个额外的参数完成(类型为Continuation)替换suspend修饰符,它将用于将suspend函数的结果传递给调用它的协程。funloginUser(userId:String,password:String,completion:Continuation){valuser=userRemoteDataSource.logUserIn(userId,password)valuserDb=userLocalDataSource.logUserIn(user)completion.resume(userDb)}为简单起见,我们的示例将返回单位而不是用??户。User对象将在添加的Continuation参数中“返回”。suspend函数的字节码实际上返回Any?因为它是(T|COROUTINE_SUSPENDED)的联合类型。这允许函数在可能的时候同步返回。注意:如果你用suspend修饰符标记一个不调用其他挂起函数的函数,编译器也会添加一个额外的Continuation参数,但不会对其做任何处理,函数体的字节码看起来就像一个普通的功能。您还可以在其他地方看到Continuation界面。当使用suspendCoroutine或suspendCancellableCoroutine(你应该总是喜欢)将基于回调的API转换为协程时,你直接与Continuation对象交互以恢复作为参数传递的代码块,该代码块在运行时被挂起。您可以在挂起函数上使用startCoroutine扩展来启动协程。它接收一个Continuation对象作为参数,并在新协程完成时调用,无论是有结果还是异常。在不同的Dispatcher之间切换您可以在不同的Dispatcher之间切换以在不同的线程上执行计算。那么Kotlin如何知道在哪里恢复暂停的计算呢?Continuation有一个名为DispatchedContinuation的子类型,其resume函数可以将调用分派给CoroutineContext中可用的Dispatcher。除了Dispatchers.Unconfined的isDispatchNeeded函数重写(在调度之前调用)总是返回false,所有Dispatcher都会调用调度。在协程中,有个不成文的约定,就是suspend函数默认不阻塞线程,也就是说suspend函数的调用者不需要关心suspend函数运行在哪个线程上,suspend函数会自己处理,大部分时候,它工作的线程是通过withContext进行切换的。生成状态机免责声明:本文其余部分显示的代码不会与编译器生成的字节码完全对应。它将是足够准确的Kotlin代码,您可以了解内部真正发生的事情。此表示法由Coroutines1.3.3版生成,可能会在库的未来版本中更改。Kotlin编译器将识别何时可以在内部挂起函数。每个挂起点都将表示为有限状态机中的一个状态。这些状态由编译器用标签表示。上例中的suspend函数编译后,会生成类似如下的伪代码。funloginUser(userId:String,password:String,completion:Continuation){//Label0->firstexecutionvaluser=userRemoteDataSource.logUserIn(userId,password)//Label1->resumesfromuserRemoteDataSourcevaluserDb=userLocalDataSource.logUserIn(user)//Label2->resumesfromuserLocalDataSourcecompletion.resume(userDb)}为了更好的表示状态机,编译器会使用一个when语句来实现不同的状态。funloginUser(userId:String,password:String,completion:Continuation){when(label){0->{//Label0->firstexecutionuserRemoteDataSource.logUserIn(userId,password)}1->{//Label1->resumesfromuserRemoteDataSourceuserLocalDataSource.logUserIn(user)}2->{//Label2->resumesfromuserLocalDataSourcecompletion.resume(userDb)}else->throwIllegalStateException(...)}}编译器将suspend函数编译成一个名为CPS的Continuation参数的方法(Continuation-Passing-Style)转换。这段代码是不完整的,因为不同的州无法共享信息。编译器将在函数中使用相同的Continuation对象来执行此操作。这就是为什么Continuation的泛型类型是Any?而不是原始函数(即用户)的返回类型。此外,编译器将创建一个私有类,它1)保存所需的数据,以及2)递归调用loginUser函数以恢复执行。您可以在下面查看此生成类的近似值。免责声明:注释不是由编译器生成的。我添加它们是为了解释它们的作用,并使代码更易于理解。funloginUser(userId:String?,password:String?,completion:Continuation){classLoginUserStateMachine(//completionparameteristhecallbacktothefunction//thatcalledloginUsercompletion:Continuation):CoroutineImpl(completion){//Localvariablesoftthesuspendfunctionvaruser:User?=nullvaruserDb:UserDb?=null//CommonobjectsforallCoroutineImplsvarresult:Any?=nullvarlabel:Int=0//这个函数调用loginUseragaintotrigger//statemachine(labelwillbealreadyinthenextstate)and//resultwillbetheresofthepreviousstate'scomputationoverridedefuninvokeSullspend(this:Any?){)}}...}uspendSinvokeSinvokeSinvokeSince将仅使用Continuation对象的信息再次调用loginUser,loginUser函数签名中的其余参数变为空值。此时,编译器只需要添加有关如何在状态之间移动的信息。它需要做的第一件事是知道1)这是函数第一次被调用,或者2)函数已经从以前的状态恢复。它通过检查传入的延续是否属于LoginUserStateMachine类型来执行此操作。funloginUser(userId:String?,password:String?,completion:Continuation){...valcontinuation=completionas?LoginUserStateMachine?:LoginUserStateMachine(completion)...}如果是第一次,它会创建一个新的LoginUserStateMachine实例并将接收到的完成实例存储为参数,以便它可以记住如何恢复调用实例的函数。如果不是,它将继续执行状态机(暂停功能)。现在,让我们看看编译器生成的用于在状态之间移动和在状态之间共享信息的代码。/*Copyright2019GoogleLLC.SPDX-License-Identifier:Apache-2.0*/funloginUser(userId:String?,password:String?,completion:Continuation){...valcontinuation=completionas?LoginUserStateMachine?:LoginUserStateMachine(完成)when(continuation.label){0->{//ChecksforfailuresthrowOnFailure(continuation.result)//Nexttimethiscontinuationiscalled,itshouldgotostate1continuation.label=1//ThecontinuationobjectispassedtologUserIntoresume//thisstatemachine'sexecutionwhenitfinishesuserRemoteDataSource.log!userId!continuation),word(1->{//ChecksforfailuresthrowOnFailure(continuation.result)//获取上一个状态的结果continuation.user=continuation.resultasUser//下次调用这个continuation时,它应该转到state2continuation.label=2//continuation对象被传递到logUserIntoresume//这个状态机的执行当它完成userLocalDataSource.logUserIn.continuation/userIn(continuation).user.故意省略最后一个状态}}花点时间浏览一下让我们看看编译器生成了什么。when语句的参数是LoginUserStateMachine实例中的Label。每次处理一个新状态时,都会检查以防在该函数挂起时发生异常。在调用下一个挂起函数(即logUserIn)之前,LoginUserStateMachine实例的Label将更新为下一个状态。当调用此状态机内的另一个挂起函数时,延续实例(类型为LoginUserStateMachine)作为参数传递。要调用的suspend函数也被编译器改造过了,又是一个这样的状态机,同样以一个continuation对象为参数!当那个挂起函数的状态机完成后,它会恢复这个状态机的执行。最后一个状态是不同的,因为它必须恢复调用此函数的执行,正如您在代码中看到的那样,它对存储在LoginUserStateMachine中的cont变量调用resume(在构造时)。/*Copyright2019GoogleLLC.SPDX-License-Identifier:Apache-2.0*/funloginUser(userId:String?,password:String?,completion:Continuation){...valcontinuation=completionas?LoginUserStateMachine?:LoginUserStateMachine(完成)when(continuation.label){...2->{//ChecksforfailuresthrowOnFailure(continuation.result)//Getstheresultofthepreviousstatecontinuation.userDb=continuation.resultasUserDb//Resumestheexecutionofthefunctionthatcalledthisonecontinuation.cont.resume(continuation.userDb)}else->throwI..)}}如您所见,Kotlin编译器为我们做了很多!以暂停功能为例。suspendfunloginUser(userId:String,password:String):User{valuser=userRemoteDataSource.logUserIn(userId,password)valuserDb=userLocalDataSource.logUserIn(user)returnuserDb}编译器为我们生成以下所有内容。/*Copyright2019GoogleLLC.SPDX-License-Identifier:Apache-2.0*/funloginUser(userId:String?,password:String?,completion:Continuation){classLoginUserStateMachine(//completionparameteristhecallbacktothefunctionthatcalledloginUsercompletion:Continuation):CoroutineImpl(completion){//objectstostoreacrossthesuspendfunctionvaruser:User?=nullvaruserDb:UserDb?=null//CommonobjectsforallCoroutineImplvarresult:Any?=nullvarlabel:Int=0//这个函数调用loginUseragaintotrigger//statemachine(labelwillbealreadyinthenextstate)and//resultwillbetheresultofthepreviousstate'scomputationrevokeultusridefudin(any)penultusoverridefudin(any){this.result=resultloginUser(null,null,this)}}valcontinuation=completionas?LoginUserStateMachine?:LoginUserStateMachine(completion)when(continuation.label){0->{//ChecksforfailuresthrowOnFailure(continuation.result)//Nexttimethiscontinuationiscalled,itshouldgotostate1continuation.label=1//continuationobjectispassedtologUserIntoresume//这个状态机在完成时的执行userRemoteDataSource.logUserIn(userId!!,password!!,continuation)}1->{//ChecksforfailuresthrowOnFailure(continuation.result)//获取上一个状态的结果continuation.user=continuation.resultasUser//Nexttimethiscontinuationiscald2//ThecontinuationobjectispassedtologUserIntoresume//thisstatemachine'sexecutionwhenitfinishesuserLocalDataSource.logUserIn(continuation.user,continuation)}2->{//ChecksforfailuresthrowOnFailure(continuation.result)//获取上一个状态的结果else->throwIllegalStateException(...)}}Kotlin编译器将每个挂起函数变成一个状态机,每次函数需要挂起时用回调进行优化现在你确切地知道编译器在编译时做了什么,你可以更好理解为什么挂起函数不会返回un直到它完成了所有的工作。此外,你还会知道代码是如何在不阻塞线程的情况下挂起的——这是因为Continuation对象中保存了函数恢复时需要执行的信息!推荐我的网站https://xuyisheng.top/关注Android-Kotlin-Flutter欢迎大家访问