Android内存泄漏:从内部类,Lambda到协程-没有魔法

1,820 阅读9分钟

没有魔法,也没有银弹

本文内容均为虚构,人名由ChatGPT给出。如有雷同。。。那肯定是你YY出来的

毕业季

烈日炎炎的17年夏天,21岁的林宇轩怀着激动的心情拖着行李箱,踏上了通往南方的旅程,迎来了他人生中的第一次飞行。经过了艰苦的军训和实习,到了九月,他终于坐在自己的工位前,打开了项目的代码,错综复杂的EventBus让他感到有些目不暇接,数千行的RecyclerView更是让他防不胜防。至于测试?测试忙着呢

在大大的花园(雾)里面了两个月以后,一张组长分下来的LeakCanary问题单落到了宇轩的头上,他一看,哦,源赖式佐田,有两个Activity leak了,但什么是leak呢?一头雾水的宇轩某度了一下午,什么静态内部类,弱引用之类的词语在网上比比皆是,但这又是什么意思呢?似懂非懂的他也来不及再学,晚上忙着封板,着急关问题单,谁有功夫让你学这个,在他照猫画虎地把网上的解决方案copy借鉴到项目中后,果然神奇地修好了这个问题,一看表已经11点半了,来不及多想,背着书包赶快回宿舍睡觉,只是迷迷糊糊中总觉得还是怪怪的,这弱引用这么神奇,以后new对象的时候都用它不就好了?带着疑惑,他最终还是进入了梦乡,明天还得早早起床上班呢

问题到底是不会自己消失,在后来的开发中,伴随着十分的纠结,宇轩写了一大堆的WeakReference,搜了搜项目里面好像也挺多这种写法的,但是到底为什么会有leak?WeakReference又为什么能解决呢?难道有什么黑魔法?不行,他觉得还是得搞明白这个问题,在一个大周末,经过多番的请教和搜索后,他好像在某个犄角旮旯的一句话找到了答案:

Longer lifecycle components reference shorter lifecycle components

回看当时leak的代码,虽然很长,说起来倒也并不复杂,只是一个AsyncTask下载图片而已

public class UserActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        new DownloadTask().execute("/image");
    }
    
    private class DownloadTask extends AsyncTask<String, Integer, String>{
        @Override
        protected String doInBackground(String... params) {
            return filePath;
        }

        @Override
        protected void onPostExecute(String filePath) {
            updateUI(filePath);
        }
    }
    
    private void updateUI(String path){
    }
}

这里面谁是longer,谁又是shorter呢?既然leak的是Activity,想必它就是那个本该被释放而没被释放的shorter了,又既然LeakCanary的trace和网上的信息里都有AsyncTask,那它就该是那个罪魁祸首longer了,好了,现在问题就缩小为longer的Task什么时候reference了shorter的Activity,它又为什么要reference呢?

项目代码回想不起来,自己写了个demo也没找见犯罪现场,看来源代码里是找不到答案了,回想起以前混酷安时候玩过的一些重打包之类的操作,宇轩决定打开Jadx反编译一下,看看会不会有什么发现

.class Lcom/yuxuan/leak/UserActivity$DownloadTask;
.super Landroid/os/AsyncTask;
.source "UserActivity.java"

# instance fields
.field final synthetic this$0:Lcom/yuxuan/leak/UserActivity;


# direct methods
.method constructor <init>(Lcom/yuxuan/leak/UserActivity;)V
    .registers 2
    .param p1, "this$0"    # Lcom/yuxuan/leak/UserActivity;

    .line 18
    iput-object p1, p0, Lcom/yuxuan/leak/UserActivity$DownloadTask;->this$0:Lcom/yuxuan/leak/UserActivity;

    invoke-direct {p0}, Landroid/os/AsyncTask;-><init>()V

    return-void
.end method


.method protected onPostExecute(Ljava/lang/String;)V
    .registers 3
    .param p1, "filePath"    # Ljava/lang/String;

    .line 26
    iget-object v0, p0, Lcom/yuxuan/leak/UserActivity$DownloadTask;->this$0:Lcom/yuxuan/leak/UserActivity;

    invoke-static {v0, p1}, Lcom/yuxuan/leak/UserActivity;->access$000(Lcom/yuxuan/leak/UserActivity;Ljava/lang/String;)V

    .line 27
    return-void
.end method

对着文档一顿翻译后,宇轩得出了这个结论:

DownloadTask的构造方法中,传入了UserActivity的对象this$0,在onPostExecute方法中,原本该调用的是来自MainActivity的成员方法updateUI,编译后却变成了静态方法access$000,那么这个access$000又在做什么呢?

.method static synthetic access$000(Lcom/yuxuan/leak/UserActivity;Ljava/lang/String;)V
    .registers 2
    .param p0, "x0"    # Lcom/yuxuan/leak/UserActivity;
    .param p1, "x1"    # Ljava/lang/String;

    .line 9
    invoke-direct {p0, p1}, Lcom/yuxuan/leak/UserActivity;->updateUI(Ljava/lang/String;)V

    return-void
.end method

它拿着UserActivity的对象x0以及updateUI方法原本的入参x1,去调用了一次UserActivity的成员方法updateUI,本质上只是个代理,将这段Smail手动反编译回Java后,原始的代码就变成了这样

public class UserActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        new DownloadTask(UserActivity.this).execute("/image");
    }

    class DownloadTask extends AsyncTask<String, Integer, String> {
        private UserActivity this$0;

        public DownloadTask(UserActivity this$0) {
            this.this$0 = this$0;
        }

        @Override
        protected String doInBackground(String... params) {
            return null;
        }

        @Override
        protected void onPostExecute(String filePath) {
            UserActivity.access$000(this$0, filePath);
        }
    }

    public static void access$000(UserActivity x0, String x1) {
        x0.updateUI(x1);
    }

    private void updateUI(String path) {
        //
    }
}

看起来编译器做了一些很多余的事情,本来直接访问了updateUI,非要绕这么个大圈子,悄摸摸的弄出了个引用搞出来了leak。。。正当宇轩准备口吐芬芳之时,又看了看Smail,感觉有点不对劲,问题好像出在这里

    public static void access$000(UserActivity x0, String x1) {
        x0.updateUI(x1);
    }

    private void updateUI(String path) {
        //
    }

仔细观察这段代码,它的作用看起来是将private方法转调为了public,难道是private方法不能调用,需要转调一次吗?再回过头去看这个内部类的定义

.class Lcom/yuxuan/leak/UserActivity$DownloadTask;
.super Landroid/os/AsyncTask;
.source "UserActivity.java"

UserActivity的private成员只能在类内访问,DownloadTask作为UserActivity的内部类,实际上被编译成了一个单独的类UserActivity$DownloadTask,在UserActivity$DownloadTask中要访问UserActivity的private方法,就会违反private成员不能在类外访问的原则,但Java又允许非静态内部类访问外部类的private成员,那就搞个public的代理出来让它访问,岂不美哉①(All problems in computer science can be solved by another level of indirection)

看来是编译器在这里的操作让内部类持有了外部类的引用,用来实现内部类对外部类private成员的访问,那么UserActivity$DownloadTask便持有了UserActivity,子线程的GC Root对Activity便可达,又因为AsyncTask的job不一定能在Activity#onDestroy前完成,在没完成的时候,自然就发生了leak。那么解决这种内部类问题的思路也很明确了

1:在shorter的对象销毁时,关掉longer中还能够持有shorter的job,清除掉引用(AsyncTask#cancel, Handler#removeCallbacksAndMessages)

2:如果不能保证及时关掉/清除,就让longer与shorter保持可被清除的引用关系,保证shorter被GC,当然,该释放的资源还得释放(Static Inner Class+WeakReference就是这么来的)

想明白了这些,宇轩总算能睡个踏实觉了。周一起了个大早去把自己之前写的各种莫名其妙的WeakReference清理掉,看着顺眼多了。


鸟枪换炮

一年后,作为一种取代匿名内部类的新语法,Lambda表达式正风靡于Android。可不是嘛,原来要写这么长的代码

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    }
});

现在只要这么点了

button.setOnClickListener(v -> {});

宇轩在自己的电脑上试了试,看着确实是挺爽的,配合上RxJava,原来那一大坨AsyncTask可以被写的很短,整体上优雅了许多。宇轩将引入Lambda的想法告诉了组长,组长倒也不是个保守的人,同意了宇轩的提议(那还得感谢公司不考核代码量),不过因为这个东西不太容易看懂,倒也不强制所有人使用。旋即宇轩便在gradle里加入了这两行代码,并引入了RxJava

sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8

美滋滋地用了一段时间后,宇轩又接到了例行的leak问题单(因为之前多次解决过leak,现在这种问题测试基本都会直接提给他),trace指向了一个Fragment,这次倒没那么容易看懂,不过这熟悉的this$0倒是让他虎躯一震:我也没写内部类啊,这里面就Lambda和RxJava

private void loadAvatar() {
    Observable.<String>create(emitter -> {
                emitter.onNext(loadImage());
                emitter.onComplete();
            })
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(this::updateUI, e -> {
            });
}

private String loadImage() throws InterruptedException {
    return null;
}

private void updateUI(String path) {
}

追查下去,trace中SyntheticLambda的字样让他产生了一丝警觉,难道是你这浓眉大眼的Lambda搞的鬼?网上不是说Lambda用了一个魔法invokeDynamic,不会内存泄露么?

时间有限,在loadImage中把没结束的job处理掉后,问题单可以关了。但是宇轩总觉得这事没那么简单,在又一个大周末来临之际,简单构建了一个demo,看看你这Lambda有些什么花活③

private void simpleLambda() {
    Runnable runnable = () -> updateUI();
    runnable.run();
}

private void updateUI() {
}

有了上次的经验,宇轩驾轻就熟地进行了反编译,看看这次的UserActivity又有了些什么新变化

###### Class com.yuxuan.leak.UserActivity (com.yuxuan.leak.UserActivity)
.class public Lcom/yuxuan/leak/UserActivity;
.super Landroidx/appcompat/app/AppCompatActivity;
.source "UserActivity.java"

.method private simpleLambda()V
    .registers 2

    .line 19
    new-instance v0, Lcom/yuxuan/leak/UserActivity$$ExternalSyntheticLambda0;

    invoke-direct {v0, p0}, Lcom/yuxuan/leak/UserActivity$$ExternalSyntheticLambda0;-><init>(Lcom/yuxuan/leak/UserActivity;)V

    .line 20
    .local v0, "runnable":Ljava/lang/Runnable;
    invoke-interface {v0}, Ljava/lang/Runnable;->run()V

    .line 21
    return-void
.end method

# virtual methods
.method synthetic lambda$simpleLambda$0$com-yuxuan-leak-UserActivity()V
    .registers 1

    .line 19
    invoke-direct {p0}, Lcom/yuxuan/leak/UserActivity;->updateUI()V

    return-void
.end method

果然有猫腻,simpleLambda方法构造了一个类型为RunnableUserActivity$$ExternalSyntheticLambda0的对象,并将UserActivitythis传入了其构造器中,最终调用了该对象的run方法,那么打开这个UserActivity$$ExternalSyntheticLambda0就能得到答案了

###### Class com.yuxuan.leak.UserActivity$$ExternalSyntheticLambda0 (com.yuxuan.leak.UserActivity$$ExternalSyntheticLambda0)
.class public final synthetic Lcom/yuxuan/leak/UserActivity$$ExternalSyntheticLambda0;
.super Ljava/lang/Object;
.source "D8$$SyntheticClass"

# interfaces
.implements Ljava/lang/Runnable;


# instance fields
.field public final synthetic f$0:Lcom/yuxuan/leak/UserActivity;


# direct methods
.method public synthetic constructor <init>(Lcom/yuxuan/leak/UserActivity;)V
    .registers 2

    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    iput-object p1, p0, Lcom/yuxuan/leak/UserActivity$$ExternalSyntheticLambda0;->f$0:Lcom/yuxuan/leak/UserActivity;

    return-void
.end method


# virtual methods
.method public final run()V
    .registers 2

    iget-object v0, p0, Lcom/yuxuan/leak/UserActivity$$ExternalSyntheticLambda0;->f$0:Lcom/yuxuan/leak/UserActivity;

    invoke-virtual {v0}, Lcom/yuxuan/leak/UserActivity;->lambda$simpleLambda$0$com-yuxuan-leak-UserActivity()V

    return-void
.end method

UserActivity$$ExternalSyntheticLambda0是一个继承自Runnable的类,其构造器中传入了UserActivity的对象f$0run方法调用了UserActivitylambda$simpleLambda$0$com-yuxuan-leak-UserActivity方法,该方法的实现为

.method synthetic lambda$simpleLambda$0$com-yuxuan-leak-UserActivity()V
    .registers 1

    .line 19
    invoke-direct {p0}, Lcom/yuxuan/leak/UserActivity;->updateUI()V

    return-void
.end method

就是调用了一下updateUI罢了

将Smali手动反编译回Java,就变成了这样

public class UserActivity extends Activity {
    private void simpleLambda() {
        Runnable runnable = new UserActivity$$ExternalSyntheticLambda0(this);
        runnable.run();
    }

    public void lambda$simpleLambda$0$com-yuxuan-leak-UserActivity(){
        updateUI();
    }

    private void updateUI() {
    }
}
public class UserActivity$$ExternalSyntheticLambda0 implements Runnable {
    private final UserActivity f$0;

    public UserActivity$$ExternalSyntheticLambda0(UserActivity f$0) {
        this.f$0 = f$0;
    }

    @Override
    public void run() {
        f$0.lambda$simpleLambda$0$com-yuxuan-leak-UserActivity();
    }
}

生成了一个新的类并构造出来,在UserActivity中新增了一个public方法用来转调private方法,虽与内部类不完全相同,但却异曲同工。既然有了对shorter的引用,那么longer未将其及时释放之时当然会leak,哪里有什么魔法。自然,解决Lambda中leak问题的思路,与之前别无二致。④⑤


军备竞赛

2020年前后,Android与iOS开发都迎来的自己的又一春,Kotlin+Jetpack Compose及Swift+SwiftUI组合的风头一时无两。此时宇轩也已工作三年,成为了一个开发小组的组长,爱追潮流的他也决定去研究研究,评估一下能否引入项目中,看到Kotlin中遍地开花的高阶函数和Lambda,一时兴起,决定如法炮制地将其扒开看看

class KotlinUserActivity : AppCompatActivity() {
    private fun simpleLambda() {
        Runnable { updateUI() }.run()
    }
    
    private fun updateUI() {
    }
}

KotlinUserActivity的bytecode为

###### Class com.yuxuan.leak.KotlinUserActivity (com.yuxuan.leak.KotlinUserActivity)
.class public final Lcom/yuxuan/leak/KotlinUserActivity;
.super Landroidx/appcompat/app/AppCompatActivity;
.source "KotlinUserActivity.kt"

.method private final simpleLambda()V
    .registers 2

    .line 14
    new-instance v0, Lcom/yuxuan/leak/KotlinUserActivity$$ExternalSyntheticLambda0;

    invoke-direct {v0, p0}, Lcom/yuxuan/leak/KotlinUserActivity$$ExternalSyntheticLambda0;-><init>(Lcom/yuxuan/leak/KotlinUserActivity;)V

    invoke-interface {v0}, Ljava/lang/Runnable;->run()V

    .line 15
    return-void
.end method

simpleLambda入手,它构造了一个类型为RunnableKotlinUserActivity$$ExternalSyntheticLambda0的对象,并在其构造器中传入了KotlinUserActivity的this,调用了run方法,打开KotlinUserActivity$$ExternalSyntheticLambda0看看

###### Class com.yuxuan.leak.KotlinUserActivity$$ExternalSyntheticLambda0 (com.yuxuan.leak.KotlinUserActivity$$ExternalSyntheticLambda0)
.class public final synthetic Lcom/yuxuan/leak/KotlinUserActivity$$ExternalSyntheticLambda0;
.super Ljava/lang/Object;
.source "D8$$SyntheticClass"

# interfaces
.implements Ljava/lang/Runnable;


# instance fields
.field public final synthetic f$0:Lcom/yuxuan/leak/KotlinUserActivity;


# direct methods
.method public synthetic constructor <init>(Lcom/yuxuan/leak/KotlinUserActivity;)V
    .registers 2

    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    iput-object p1, p0, Lcom/yuxuan/leak/KotlinUserActivity$$ExternalSyntheticLambda0;->f$0:Lcom/yuxuan/leak/KotlinUserActivity;

    return-void
.end method


# virtual methods
.method public final run()V
    .registers 2

    iget-object v0, p0, Lcom/yuxuan/leak/KotlinUserActivity$$ExternalSyntheticLambda0;->f$0:Lcom/yuxuan/leak/KotlinUserActivity;

    invoke-static {v0}, Lcom/yuxuan/leak/KotlinUserActivity;->$r8$lambda$-3GZDt_QPwKG2FgBRX9E3cVb5Uc(Lcom/yuxuan/leak/KotlinUserActivity;)V

    return-void
.end method

构造器中保存了KotlinUserActivity的对象f$0,在run方法中调用了KotlinUserActivity的静态方法$r8$lambda$-3GZDt_QPwKG2FgBRX9E3cVb5Uc

.method public static synthetic $r8$lambda$-3GZDt_QPwKG2FgBRX9E3cVb5Uc(Lcom/yuxuan/leak/KotlinUserActivity;)V
    .registers 1

    invoke-static {p0}, Lcom/yuxuan/leak/KotlinUserActivity;->simpleLambda$lambda$0(Lcom/yuxuan/leak/KotlinUserActivity;)V

    return-void
.end method

调用了静态方法simpleLambda$lambda$0

.method private static final simpleLambda$lambda$0(Lcom/yuxuan/leak/KotlinUserActivity;)V
    .registers 2
    .param p0, "this$0"    # Lcom/yuxuan/leak/KotlinUserActivity;

    const-string v0, "this$0"

    invoke-static {p0, v0}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullParameter(Ljava/lang/Object;Ljava/lang/String;)V

    .line 14
    invoke-direct {p0}, Lcom/yuxuan/leak/KotlinUserActivity;->updateUI()V

    return-void
.end method

调用了Kotlin的null-safe方法checkNotNullParameter,忽略,最终使用KotlinUserActivity的对象this调用了updateUI()方法,倒是与Java的内部类有些相似了

将Smali手动反编译回Kotlin,就变成了这样

class KotlinUserActivity : AppCompatActivity() {
    companion object {
        fun `$r8$lambda$-3GZDt_QPwKG2FgBRX9E3cVb5Uc`(`this$0`: KotlinUserActivity) {
            `simpleLambda$lambda$0`(`this$0`);
        }

        private fun `simpleLambda$lambda$0`(`this$0`: KotlinUserActivity) {
            Intrinsics.checkNotNullParameter(`this$0`)
            `this$0`.updateUI()
        }
    }

    private fun simpleLambda() {
        `KotlinUserActivity$$ExternalSyntheticLambda0`(this).run()
    }

    private fun updateUI() {
    }
}
class `KotlinUserActivity$$ExternalSyntheticLambda0`(val `f$0`: KotlinUserActivity) : Runnable {
    override fun run() {
        KotlinUserActivity.`$r8$lambda$-3GZDt_QPwKG2FgBRX9E3cVb5Uc`(`f$0``)
    }
}

除了Lambda之外,协程也是Kotlin带来的一项重要特性,回想起Android的异步请求,从最早的AsyncTaskRxJava,再到现在被称为黑魔法的协程,代码是越来越少,看起来也越来越像魔法,那么所谓的黑魔法,真的有那么神奇吗?协程会不会导致leak呢?想到这里,宇轩捣鼓出了这样一段demo

class KotlinUserActivity : AppCompatActivity() {
    private fun simpleCoroutine() {
        GlobalScope.launch {
            val path = withContext(Dispatchers.IO) {
                loadImage()
            }
            withContext(Dispatchers.Main) {
                updateUI(path)
            }
        }
    }

    private fun loadImage(): String = ""

    private fun updateUI(path: String) {
    }
}

反编译来看看

.method private final simpleCoroutine()V
    .registers 8

    .line 22
    sget-object v0, Lkotlinx/coroutines/GlobalScope;->INSTANCE:Lkotlinx/coroutines/GlobalScope;

    move-object v1, v0

    check-cast v1, Lkotlinx/coroutines/CoroutineScope;

    const/4 v2, 0x0

    const/4 v3, 0x0

    new-instance v0, Lcom/yuxuan/leak/KotlinUserActivity$simpleCoroutine$1;

    const/4 v4, 0x0

    invoke-direct {v0, p0, v4}, Lcom/yuxuan/leak/KotlinUserActivity$simpleCoroutine$1;-><init>(Lcom/yuxuan/leak/KotlinUserActivity;Lkotlin/coroutines/Continuation;)V

    move-object v4, v0

    check-cast v4, Lkotlin/jvm/functions/Function2;

    const/4 v5, 0x3

    const/4 v6, 0x0

    invoke-static/range {v1 .. v6}, Lkotlinx/coroutines/BuildersKt;->launch$default(Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;Lkotlinx/coroutines/CoroutineStart;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/Job;

    .line 30
    return-void
.end method

构造了KotlinUserActivity$simpleCoroutine$1的对象

.class final Lcom/yuxuan/leak/KotlinUserActivity$simpleCoroutine$1;
.super Lkotlin/coroutines/jvm/internal/SuspendLambda;
.source "KotlinUserActivity.kt"

.class final Lcom/yuxuan/leak/KotlinUserActivity$simpleCoroutine$1$1;
.super Lkotlin/coroutines/jvm/internal/SuspendLambda;
.source "KotlinUserActivity.kt"


.method constructor <init>(Lcom/yuxuan/leak/KotlinUserActivity;Ljava/lang/String;Lkotlin/coroutines/Continuation;)V
    .registers 5
    .annotation system Ldalvik/annotation/Signature;
        value = {
            "(",
            "Lcom/yuxuan/leak/KotlinUserActivity;",
            "Ljava/lang/String;",
            "Lkotlin/coroutines/Continuation<",
            "-",
            "Lcom/yuxuan/leak/KotlinUserActivity$simpleCoroutine$1$1;",
            ">;)V"
        }
    .end annotation

    iput-object p1, p0, Lcom/yuxuan/leak/KotlinUserActivity$simpleCoroutine$1$1;->this$0:Lcom/yuxuan/leak/KotlinUserActivity;

    iput-object p2, p0, Lcom/yuxuan/leak/KotlinUserActivity$simpleCoroutine$1$1;->$path:Ljava/lang/String;

    const/4 v0, 0x2

    invoke-direct {p0, v0, p3}, Lkotlin/coroutines/jvm/internal/SuspendLambda;-><init>(ILkotlin/coroutines/Continuation;)V

    return-void
.end method

.method public final invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
    .registers 5

    invoke-static {}, Lkotlin/coroutines/intrinsics/IntrinsicsKt;->getCOROUTINE_SUSPENDED()Ljava/lang/Object;

    .line 26
    iget v0, p0, Lcom/yuxuan/leak/KotlinUserActivity$simpleCoroutine$1$1;->label:I

    packed-switch v0, :pswitch_data_1e

    new-instance p1, Ljava/lang/IllegalStateException;

    const-string v0, "call to 'resume' before 'invoke' with coroutine"

    invoke-direct {p1, v0}, Ljava/lang/IllegalStateException;-><init>(Ljava/lang/String;)V

    throw p1

    :pswitch_10
    invoke-static {p1}, Lkotlin/ResultKt;->throwOnFailure(Ljava/lang/Object;)V

    move-object v0, p0

    .line 27
    .local v0, "this":Lcom/yuxuan/leak/KotlinUserActivity$simpleCoroutine$1$1;
    .local p1, "$result":Ljava/lang/Object;
    iget-object v1, v0, Lcom/yuxuan/leak/KotlinUserActivity$simpleCoroutine$1$1;->this$0:Lcom/yuxuan/leak/KotlinUserActivity;

    iget-object v2, v0, Lcom/yuxuan/leak/KotlinUserActivity$simpleCoroutine$1$1;->$path:Ljava/lang/String;

    invoke-static {v1, v2}, Lcom/yuxuan/leak/KotlinUserActivity;->access$updateUI(Lcom/yuxuan/leak/KotlinUserActivity;Ljava/lang/String;)V

    .line 28
    sget-object v1, Lkotlin/Unit;->INSTANCE:Lkotlin/Unit;

    return-object v1

    :pswitch_data_1e
    .packed-switch 0x0
        :pswitch_10
    .end packed-switch
.end method

协程框架调度线程后⑥,生成KotlinUserActivity$simpleCoroutine$1$1,构造传入KotlinUserActivity的对象this$0,调用KotlinUserActivity的public static方法access$updateUI

.method public static final synthetic access$updateUI(Lcom/yuxuan/leak/KotlinUserActivity;Ljava/lang/String;)V
    .registers 2
    .param p0, "$this"    # Lcom/yuxuan/leak/KotlinUserActivity;
    .param p1, "path"    # Ljava/lang/String;

    .line 12
    invoke-direct {p0, p1}, Lcom/yuxuan/leak/KotlinUserActivity;->updateUI(Ljava/lang/String;)V

    return-void
.end method

传入KotlinUserActivity的对象,调用updateUI,得,还得是$this

将Smali手动反编译回Kotlin看看

class KotlinUserActivity : AppCompatActivity() {
    companion object {
        fun `access$updateUI`(path: String, `$this`: KotlinUserActivity) {
            `$this`.updateUI(path)
        }
    }

    private fun simpleCoroutine() {
        BuildersKt.`launch$default`(
            GlobalScope,
            null,
            null,
            `KotlinUserActivity$simpleCoroutine$1`(this, null),
            3,
            null
        );
    }

    private fun loadImage(): String = ""

    private fun updateUI(path: String) {
    }
}
class `KotlinUserActivity$simpleCoroutine$1`(
    val `this$0`: KotlinUserActivity,
    val continuation: Continuation<`KotlinUserActivity$simpleCoroutine$1`>
) : SuspendLambda, Function2<CoroutineScope, Continuation<Unit>, Any> {
    fun invokeSuspend(any: Any): Any {
        //略去协程调度,直接进入invokeSuspend
        `KotlinUserActivity$simpleCoroutine$1$1`(`this$0`, path, null).invokeSuspend()
    }
}
class `KotlinUserActivity$simpleCoroutine$1$1`(
    val `this$0`: KotlinUserActivity,
    val path: String,
    val continuation: Continuation<`KotlinUserActivity$simpleCoroutine$1$1`>
) : SuspendLambda, Function2<CoroutineScope, Continuation<Unit>, Any> {
    fun invokeSuspend(any: Any): Any {
        //略去协程调度,直接进入invokeSuspend
        IntrinsicsKt.getCOROUTINE_SUSPENDED()
        KotlinUserActivity.`access$updateUI`(path, `this$0`)
    }
}

此时,宇轩已经明白,所谓leak,与内部类,Lambda,协程并没什么关系,那些都只是表象,背后也并没有什么魔法,你要调用一个类的成员方法,你就得拿着它的对象,当这个对象要被GC之时,有GC Root拿着不放,那可不就leak了⑦,无需纠结于具体的实现,只需谨记

Longer lifecycle components reference shorter lifecycle components


①参考资料:

https://docs.oracle.com/javase/specs/jls/se7/html/jls-6.html#jls-6.6

https://jcp.org/aboutJava/communityprocess/maintenance/JLS/innerclasses.pdf

②本篇扩展思考:

成员内部类会持有外部类引用,那么匿名内部类呢?

持有外部类引用是为了访问外部类private成员,那要是不访问呢?

③声明

篇幅有限,且本文重点不在RxJava的内部实现,分析会引入干扰,故而重新构建无RxJava的demo版本

④补充

Android上Lambda的情况较为复杂,请注意ART并不是标准的JVM,早期在无官方支持时多使用RetroLambda来进行desugar。Android N之后Android SDK实现了部分Java8特性,对minSDK在24以下的Java7版本进行了desugar,但在N之后,也并未采用与JDK相同的实现,依然是desugar处理。网上传的到处都是的bytecodeinvokeDynamic在Davlik bytecode中并不存在,Android O之后Davlik bytecode提供了invoke-polymorphicinvoke-custom,但他们并未被应用到Lambda中(截止2023.8)

⑤本篇扩展思考:

持有外部类引用是为了访问外部类private成员,那要是不访问呢?(本问题与②中并不重复,请自行验证)

⑥注意

篇幅有限,且本文重点不在协程的内部实现,故而此处将其调度忽略,直接跳转到调度结束点

⑦声明

本文中讨论的leak主要应用于Android中常见的组件被持有未释放情况(大部分情况下是子线程处理业务后更新UI),至于资源未关闭等问题不在讨论范围中,但也请加以关注