分析Kotlin协程只挂起不恢复会怎样(是否存在协程泄漏),以及挂起的协程存在哪里?

前言

刚开始正式学协程原理的时候(以前只是学api怎么用),大概是20年6月,也就是bennyhuo大佬出书<深入理解Kotlin协程>的时候,我买了本然后细细研究,我的内心就一直有一个问题,协程只挂起不恢复会不会造成协程的内存和程序泄漏.

后来经过我debug和分析jvm字节码,终于找到了答案!

正文

首先放出结论:

  1. 协程只挂起不恢复只会造成一直挂起,后面的代码块得不到执行
  2. 协程只挂起不恢复不会造成协程泄漏(前提是你没有将协程体对象传出去且长时间引用)
  3. 挂起的协程体存在匿名内部类中

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));
    }
}
  1. 在launch后,会经过一系列的协程内部调用,最后会走到MainActivity$init$1的invokeSuspend方法,第一次执行协程代码块时label会是0(label相当于走到了第几步,是通过挂起点来分割步骤的)
  2. 然后走到LogUtil.e2$default("123", (String) null, 1, (Object) null);去打印日志
  3. 接着遇到挂起点,会执行SafeContinuation it = new SafeContinuation(IntrinsicsKt.intercepted(mainActivity$init$1));将自身这个匿名内部类包装一层创建个新的协程体
  4. 然后执行HandlerPoolKt.post$default((String) null, new MainActivity$init$1$1$1(it), 1, (Object) null);,其会创建一个匿名内部类(也就是post的lambda)来在下个时机恢复协程
  5. 这时调用SafeContinuation的getOrThrow方法,会获取到返回值,也就是IntrinsicsKt.getCOROUTINE_SUSPENDED(),表示当前协程会被挂起,然后return了invokeSuspend方法
  6. 在某个时刻主线程相应了post事件,就会执行MainActivity$init$1$1$1对象的invoke方法,invoke方法就会调用匿名内部类的上层对象(jvm匿名内部类的特性是会隐式在构造中传入上层对象)的resume方法来恢复协程
  7. 这时协程会重新调用MainActivity$init$1的invokeSuspend方法,并且label变为了1,在label1里面会检查是否有异常,没有异常就会走到if else后面的代码块,打印日志"5"
  8. 然后在创建一个新的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

 

 

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值