【译】Kotlin如何帮助避免内存泄漏
Kotlin如何避免内存泄漏
本文翻译自马科斯·霍尔加多(Marcos Holgado)
发表的《How Kotlin helps you avoid memory leaks》, 感兴趣的可以查看原文,链接可能打不开,但对于会魔法的终极魔法师应该不是什么问题。
下面是正文:
上周,我在MobOS上发表了有关在Android中编写和自动化性能测试的演讲。作为演讲的一部分,我想演示如何在集成测试期间检测内存泄漏。为了证明这一点,我使用Kotlin创建了一个Activity,该Activity应该会泄漏内存,但是由于某种原因却没有。Kotlin是在不知不觉中帮助我吗?
在开始之前,本文的代码可在kotlin-mem-leak
我的性能测试存储库的分支中找到:
https://github.com/marcosholgado/performance-test/tree/kotlin-mem-leak
整个前提很简单,我想编写一个会泄漏内存的Activity,以便在集成测试中可以检测到该Activity。因为我已经在使用leacanary,所以我复制了他们的示例Activity来重新创建内存泄漏。我从示例中删除了一些代码,并得到了以下Java类。
public class LeakActivity extends Activity {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak);
View button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startAsyncWork();
}
});
}
@SuppressLint("StaticFieldLeak")
void startAsyncWork() {
Runnable work = new Runnable() {
@Override public void run() {
SystemClock.sleep(20000);
}
};
new Thread(work).start();
}
}
该LeakActivity有一个按钮,按下时,将创建一个新的Runnable是运行20秒。由于Runnable是一个匿名类,因此它持有外部类LeakActivity的匿名引用,如果LeakActivity在线程完成之前(按钮按下后20秒内)被销毁,则LeakActivity将泄漏。不过,它不会永远泄漏,在那20秒之后,将可以再次进行垃圾收集。
然后我用Kotlin编写代码,将该Java类转换为Kotlin代码,如下所示:
class KLeakActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_leak)
button.setOnClickListener { startAsyncWork() }
}
private fun startAsyncWork() {
val work = Runnable { SystemClock.sleep(20000) }
Thread(work).start()
}
}
这里的代码并没有特别之处,我利用了lambda的优点优化Runnable的写法,从理论上讲,一切都应该是一毛一样的,对吗?然后,我使用LeakCanary和自己构造的@LeakTest注解编写了以下测试代码,本测试仅进行了内存分析。
class LeakTest {
@get:Rule
var mainActivityActivityTestRule = ActivityTestRule(KLeakActivity::class.java)
@Test
@LeakTest
fun testLeaks() {
onView(withId(R.id.button)).perform(click())
}
}
该测试将执行一次按钮单击操作,因为这是我们唯一要做的事情,Activity会立即销毁并造成泄漏,因为我们没有等待20秒再关闭Activity。
如果我们执行testLeaks
的测试,将会看到MyKLeakTest
的测试通过,这意味着我们未检测到任何内存泄漏。
这个结果使我很困惑。
我感觉自己如此愚蠢,甚至于我在推特上写道:
How Kotlin helps you avoid memory leaks并得到了让我笑的答复。我希望我的技能达到那个水平:D
How Kotlin helps you avoid memory leaks人们很容易陷入“总觉得哪里不对劲,但就是不知道哪里有问题”的死循环中,于是我决定从头再来。
我编写了一个新Activity,使用相同的代码,但是这次我将其保存在Java中。我将测试更改为指向此新Activity,然后运行它,这次…测试用例没通过。现在事情开始变得更有意义了。Kotlin代码肯定与Java代码不同,想知道有什么不同,只有一个地方可以找到它,那就是字节码。
分析LeakActivity.java
首先,我分析了Java Activity的Dalvik字节码。为此,您可以通过分析apk Build/Analyze APK...
,然后从classes.dex
文件中选择要分析的类。
我们右键单击该类,然后选择Show Bytecode以获取该类的Dalvik字节码。我将只关注该startAsyncWork方法,因为我们知道它是发生内存泄漏的地方。
.method startAsyncWork()V
.registers 3
.annotation build Landroid/annotation/SuppressLint;
value = {
"StaticFieldLeak"
}
.end annotation
.line 29
new-instance v0, Lcom/marcosholgado/performancetest/LeakActivity$2;
invoke-direct {v0, p0}, Lcom/marcosholgado/performancetest/LeakActivity$2;-><init>
(Lcom/marcosholgado/performancetest/LeakActivity;)V
.line 34
.local v0, "work":Ljava/lang/Runnable;
new-instance v1, Ljava/lang/Thread;
invoke-direct {v1, v0}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V
invoke-virtual {v1}, Ljava/lang/Thread;->start()V
.line 35
return-void
.end method
我们知道匿名类会保留对外部类的引用,因此我们应该先找到该类。在上面的字节码中,可以看到创建了一个新实例LeakActivity$2
并将其存储在v0(第10行)中。
new-instance v0, Lcom/marcosholgado/performancetest/LeakActivity$2;
但LeakActivity$2
是什么呢?如果我们继续查看我们的classes.dex文件,您将在此处找到它。
因此,让我们看看该类的Dalvik字节码。我从结果中删除了一些我们不太关心的代码。
.class Lcom/marcosholgado/performancetest/LeakActivity$2;
.super Ljava/lang/Object;
.source "LeakActivity.java"
# interfaces
.implements Ljava/lang/Runnable;
# instance fields
.field final synthetic this$0:Lcom/marcosholgado/performancetest/LeakActivity;
# direct methods
.method constructor <init>(Lcom/marcosholgado/performancetest/LeakActivity;)V
.registers 2
.param p1, "this$0" # Lcom/marcosholgado/performancetest/LeakActivity;
.line 29
iput-object p1, p0, Lcom/marcosholgado/performancetest/LeakActivity$2;
->this$0:Lcom/marcosholgado/performancetest/LeakActivity;
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
您可以看到的第一个有趣的事情是该类实现了Runnable。
# interfaces
.implements Ljava/lang/Runnable;
就像我之前说过的,该类应该引用外部类,所以它在哪里?在界面下方,有一个LeakActivity类型的成员变量。
# instance fields
.field final synthetic
this$0:Lcom/marcosholgado/performancetest/LeakActivity;
如果我们看一下Runnable的构造函数,您会看到它带有一个LeakActivity参数。
.method 构造函数
<init> (Lcom / marcosholgado / performancetest / LeakActivity;) V
回到LeakActivity的字节码,您可以看到创建LeakActivity$2实例后(存储在v0中),它在初始化构造方法时传入了LeakActivity的实例。
new-instance v0, Lcom/marcosholgado/performancetest/LeakActivity$2;
invoke-direct {v0, p0},
Lcom/marcosholgado/performancetest/LeakActivity$2;-><init>
(Lcom/marcosholgado/performancetest/LeakActivity;)V
因此,如果我们的LeakActivity.java类在Runnable完成之前被杀死,则确实会泄漏,因为它持有LeakActivity的引用,并且此时不会被垃圾回收。
分析KLeakActivity.kt
如果现在查看KLeakActivity.kt的Dalvik字节码,然后只看startAsyncWork方法,我们将获得以下字节码。
.method private final startAsyncWork()V
.registers 3
.line 20
sget-object v0,
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;
->INSTANCE:Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;
check-cast v0, Ljava/lang/Runnable;
.line 24
.local v0, "work":Ljava/lang/Runnable;
new-instance v1, Ljava/lang/Thread;
invoke-direct {v1, v0}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V
invoke-virtual {v1}, Ljava/lang/Thread;->start()V
.line 25
return-void
.end method
可以看到,这里的字节码没有在创建新实例时传入Activity的引用,而是在sget-object执行操作,该操作使用static标识把Runable标记成静态字段。
sget-object v0,
Lcom / marcosholgado / performancetest / KLeakActivity$startAsyncWork$work$1; ->
INSTANCE:Lcom / marcosholgado / performancetest / KLeakActivity$startAsyncWork$work$1;
更深入地查看KLeakActivity$startAsyncWork$work$1
字节码,我们可以看到,像以前一样,该类实现了Runnable,但是现在它具有一个静态方法,不需要外部类的实例。
.class final Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;
.super Ljava/lang/Object;
.source "KLeakActivity.kt"
# interfaces
.implements Ljava/lang/Runnable;
.method static constructor <clinit>()V
.registers 1
new-instance v0,
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;
invoke-direct {v0},
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;-><init>()V
sput-object v0,
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;
->INSTANCE:Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;
return-void
.end method
.method constructor <init>()V
.registers 1
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
这就是为什么KLeakActivity
没有真正泄漏任何东西的原因,通过使用lambda(实际上是SAM)而不是匿名内部类,我没有保留对我外部Activity的引用。但是,这不能地说这是Kotlin特有的,如果您使用的是Java8 lambda,则结果是完全相同的。
如果您想了解更多有关此的内容,我强烈建议您阅读有关lambda翻译的本文,但我将为您重点介绍。
像那些在上面的部分lambda表达式可以转换为静态方法,因为它们不以任何方式使用封闭对象实例(
enclosing object instance
)(不是指this,super或封闭实例的成员。)总之,我们将把lambda表达式是使用this,super或将封闭实例的成员捕获为实例捕获lambdas。非实例捕获(non-instance-capturing
)的lambda转换为私有的静态方法。捕获实例(instance-capturing
)的lambda转换为私有实例方法
那是什么意思呢?我们的Kotlin Lambda是一个非实例捕获的Lambda,因为未使用封闭对象实例。但是,如果我们使用来自外部类的字段,那么我们的lambda将持有对外部类的引用和造成泄漏。
class KLeakActivity : Activity() {
private var test: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_leak)
button.setOnClickListener { startAsyncWork() }
}
private fun startAsyncWork() {
val work = Runnable {
test = 1 // comment this line to pass the test
SystemClock.sleep(20000)
}
Thread(work).start()
}
}
在上面的示例中,我们看到Runnable引用了test字段,因此它持有了外部类Activity的引用并造成了内存泄漏。再次查看字节码,您会发现它如何将KLeakActivity实例传递给我们的Runnable(第9行),我们现在使用的是实例捕获lambda。
.method private final startAsyncWork()V
.registers 3
.line 20
new-instance v0, Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;
invoke-direct {v0, p0},
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;
-><init>(Lcom/marcosholgado/performancetest/KLeakActivity;)V
check-cast v0, Ljava/lang/Runnable;
.line 24
.local v0, "work":Ljava/lang/Runnable;
new-instance v1, Ljava/lang/Thread;
invoke-direct {v1, v0}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V
invoke-virtual {v1}, Ljava/lang/Thread;->start()V
.line 25
return-void
.end method
以上就是所有内容,我希望本文能帮助您更多地了解SAM,lambda转换以及如何安全地使用非捕获的lambda,而不必担心内存泄漏。
请记住,如果您想尝试此操作,可以在此github代码仓库获得本文的所有代码。
我意识到这不是一个非常简单的话题,因此如果您有任何疑问或认为我在某个地方搞砸了,请在Twitter上发表评论或联系。(作者的Twitter:orbycius)