前言
刚开始正式学协程原理的时候(以前只是学api怎么用),大概是20年6月,也就是bennyhuo大佬出书<深入理解Kotlin协程>的时候,我买了本然后细细研究,我的内心就一直有一个问题,协程只挂起不恢复会不会造成协程的内存和程序泄漏.
后来经过我debug和分析jvm字节码,终于找到了答案!
正文
首先放出结论:
- 协程只挂起不恢复只会造成一直挂起,后面的代码块得不到执行
- 协程只挂起不恢复不会造成协程泄漏(前提是你没有将协程体对象传出去且长时间引用)
- 挂起的协程体存在匿名内部类中
ps:主要的分析手段是靠分析jvm字节码,debug算是辅助手段
首先是需要分析的代码:
main {//启动一个主线程协程
"123".e2()//打印日志
suspendCoroutine<Unit> { post { it.resume(Unit) } }//挂起并post到主线程之后恢复协程
"5".e2()
suspendCoroutine<Unit> { }//只挂起不恢复协程
"456".e2()
}
编译后的jvm字节码再经过反编译得到的java代码(过滤不重要的代码):
启动的代码,封装了launch和scope:
public void init() {
//这里就是上面的入口代码
main(this, new MainActivity$init$1((Continuation) null));
}
public static Job main(BaseActive $this, Function2<? super CoroutineScope, ? super Continuation<? super Unit>, ? extends Object> run) {
//下面就是正常的launch流程了
return BuildersKt__Builders_commonKt.launch$default($this.getMainScope(), (CoroutineContext) null, (CoroutineStart) null, run, 3, (Object) null);
}
挂起函数生成的匿名内部类,对应main方法传入的lambda:
final class MainActivity$init$1 extends SuspendLambda implements Function2<CoroutineScope, Continuation<? super Unit>, Object> {
...
public final Object invokeSuspend(Object $result) {
MainActivity$init$1 mainActivity$init$1;
Object coroutine_suspended = IntrinsicsKt.getCOROUTINE_SUSPENDED();
int i = this.label;
if (i == 0) {
ResultKt.throwOnFailure($result);
mainActivity$init$1 = this;
LogUtil.e2$default("123", (String) null, 1, (Object) null);//对应打印日志的方法
mainActivity$init$1.L$0 = mainActivity$init$1;
mainActivity$init$1.label = 1;
//在挂起点将自身这个匿名内部类包一层,创建的一个包装协程体
SafeContinuation it = new SafeContinuation(IntrinsicsKt.intercepted(mainActivity$init$1));
//这里是对应post方法,这个MainActivity$init$1$1$1就是post方法传入的lambda生成的匿名内部类对象
HandlerPoolKt.post$default((String) null, new MainActivity$init$1$1$1(it), 1, (Object) null);
Object orThrow = it.getOrThrow();
if (orThrow == IntrinsicsKt.getCOROUTINE_SUSPENDED()) {
DebugProbesKt.probeCoroutineSuspended(mainActivity$init$1);
}
if (orThrow == coroutine_suspended) {
return coroutine_suspended;
}
} else if (i == 1) {
mainActivity$init$1 = this;
MainActivity$init$1 mainActivity$init$12 = (MainActivity$init$1) mainActivity$init$1.L$0;
ResultKt.throwOnFailure($result);
} else if (i == 2) {
MainActivity$init$1 mainActivity$init$13 = (MainActivity$init$1) this.L$0;
ResultKt.throwOnFailure($result);
LogUtil.e2$default("456", (String) null, 1, (Object) null);
return Unit.INSTANCE;
} else {
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
LogUtil.e2$default("5", (String) null, 1, (Object) null);
mainActivity$init$1.L$0 = mainActivity$init$1;
mainActivity$init$1.label = 2;
SafeContinuation safeContinuation = new SafeContinuation(IntrinsicsKt.intercepted(mainActivity$init$1));
Continuation continuation = safeContinuation;
Object orThrow2 = safeContinuation.getOrThrow();
if (orThrow2 == IntrinsicsKt.getCOROUTINE_SUSPENDED()) {
DebugProbesKt.probeCoroutineSuspended(mainActivity$init$1);
}
if (orThrow2 == coroutine_suspended) {
return coroutine_suspended;
}
MainActivity$init$1 mainActivity$init$14 = mainActivity$init$1;
LogUtil.e2$default("456", (String) null, 1, (Object) null);
return Unit.INSTANCE;
}
}
对应post方法传入的匿名内部类:
final class MainActivity$init$1$1$1 extends Lambda implements Function0<Unit> {
final /* synthetic */ Continuation $it;
/* JADX INFO: super call moved to the top of the method (can break code semantics) */
MainActivity$init$1$1$1(Continuation continuation) {
super(0);
this.$it = continuation;
}
public final void invoke() {
Continuation continuation = this.$it;
Unit unit = Unit.INSTANCE;
Result.Companion companion = Result.Companion;
continuation.resumeWith(Result.m4constructorimpl(unit));
}
}
- 在launch后,会经过一系列的协程内部调用,最后会走到MainActivity$init$1的invokeSuspend方法,第一次执行协程代码块时label会是0(label相当于走到了第几步,是通过挂起点来分割步骤的)
- 然后走到LogUtil.e2$default("123", (String) null, 1, (Object) null);去打印日志
- 接着遇到挂起点,会执行SafeContinuation it = new SafeContinuation(IntrinsicsKt.intercepted(mainActivity$init$1));将自身这个匿名内部类包装一层创建个新的协程体
- 然后执行HandlerPoolKt.post$default((String) null, new MainActivity$init$1$1$1(it), 1, (Object) null);,其会创建一个匿名内部类(也就是post的lambda)来在下个时机恢复协程
- 这时调用SafeContinuation的getOrThrow方法,会获取到返回值,也就是IntrinsicsKt.getCOROUTINE_SUSPENDED(),表示当前协程会被挂起,然后return了invokeSuspend方法
- 在某个时刻主线程相应了post事件,就会执行MainActivity$init$1$1$1对象的invoke方法,invoke方法就会调用匿名内部类的上层对象(jvm匿名内部类的特性是会隐式在构造中传入上层对象)的resume方法来恢复协程
- 这时协程会重新调用MainActivity$init$1的invokeSuspend方法,并且label变为了1,在label1里面会检查是否有异常,没有异常就会走到if else后面的代码块,打印日志"5"
- 然后在创建一个新的SafeContinuation对象,然后在getOrThrow()会返回IntrinsicsKt.getCOROUTINE_SUSPENDED(),然后协程函数就停止执行了,因为后面没有没有了新的匿名内部类来引用MainActivity$init$1对象,所以其会在调用栈中层层出栈,然后引用链也会断开,所以不会造成内存泄漏,只会造成协程一直挂起,后面的代码块得不到执行
结论
协程只挂起不恢复只会造成一直挂起,后面的代码块得不到执行
上面的几个步骤证明了该结论
挂起的协程体存在匿名内部类中
jvm的匿名内部类的特性之一是会在其隐式的构造函数中传入上层对象,所以协程体的对象会被传入到对应挂起点通过CPS转换得来的匿名内部类对象中,而这个对象如果将来某个时段会调用resume,就会被某个代码节点给引用着(可能是间接的main方法或线程run方法),相当于一层层匿名内部类串联引用保存了协程体的对象
协程只挂起不恢复不会造成协程泄漏
通过上面的代码发现,第二个挂起点处,也就是打印日志"5"的下方,代码只调用了getOrThrow但没有其他时机调用resume,也没有将MainActivity$init$1对象传到其他地方,而jvm内存回收是根据可达性分析算法决定对象可不可以被回收,此时invokeSuspend方法已经执行完毕,MainActivity$init$1对象也已不可达(没有被其他对象引用),该协程就随时有可能被gc给回收掉
本篇文章只是代表个人观察和分析所得,如有错误,欢迎大佬指出.
end