首先,本文的代碼位置在github.com/marcosholga…中的kotlin-mem-leak
分支上。html
我是經過建立一個會致使內存泄漏的Activity
,而後觀察其使用Java
和Kotlin
編寫時的表現來進行測試的。 其中Java
代碼以下: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();
}
}
複製代碼
如上述代碼所示,咱們的button
點擊以後,執行了一個耗時任務。這樣若是咱們在20s以內關閉LeakActivity
的話就會產生內存泄漏,由於這個新開的線程持有對LeakActivity
的引用。若是咱們是在20s以後再關閉這個Activity
的話,就不會致使內存泄漏。android
而後咱們把這段代碼改爲Kotlin
版本:git
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()
}
}
複製代碼
咋一看,好像就只是在Runable
中使用lambda
表達式替換了原來的樣板代碼。而後我使用leakcanary
和我本身的@LeakTest
註釋寫了一個內存泄漏測試用例。github
class LeakTest {
@get:Rule
var mainActivityActivityTestRule = ActivityTestRule(KLeakActivity::class.java)
@Test
@LeakTest
fun testLeaks() {
onView(withId(R.id.button)).perform(click())
}
}
複製代碼
咱們使用這個用例分別對Java
寫的LeakActivity
和Kotlin
寫的KLeakActivity
進行測試。測試結果是Java
寫的出現內存泄漏,而Kotlin
寫的則沒有出現內存泄漏。 這個問題困擾了我很長時間,一度接近自閉。。 bash
Java
類產生的字節碼以下:ide
.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
複製代碼
咱們知道匿名內部類持有對外部類的引用,正是這個引用致使了內存泄漏的產生,接下來咱們就在字節碼中找出這個引用。測試
new-instance v0, Lcom/marcosholgado/performancetest/LeakActivity$2;
複製代碼
上述字節碼的含義是: 首先咱們建立了一個LeakActivity$2
的實例。。ui
奇怪的是咱們沒有建立這個類啊,那這個類應該是系統自動生成的,那它的做用是什麼啊? 咱們打開LeakActivity$2
的字節碼看下this
.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
複製代碼
第一個有意思的事是這個LeakActivity$2
實現了Runnable
接口。
# interfaces
.implements Ljava/lang/Runnable;
複製代碼
這就說明LeakActivity$2
就是那個持有LeakActivity
對象引用的匿名內部類的對象。
就像咱們前面說的,這個LeakActivity$2
應該持有LeakActivity
的引用,那咱們繼續找。
# instance fields
.field final synthetic
this$0:Lcom/marcosholgado/performancetest/LeakActivity;
複製代碼
果真,咱們發現了外部類LeakActivity的對象的引用。 那這個引用是何時傳入的呢?只有多是在構造器中傳入的,那咱們繼續找它的構造器。
.method constructor
<init>(Lcom/marcosholgado/performancetest/LeakActivity;)V
複製代碼
果真,在構造器中傳入了LeakActivity
對象的引用。 讓咱們回到LeakActivity
的字節碼中,看看這個LeakActivity$2
被初始化的時候。
new-instance v0, Lcom/marcosholgado/performancetest/LeakActivity$2;
invoke-direct {v0, p0},
Lcom/marcosholgado/performancetest/LeakActivity$2;-><init>
(Lcom/marcosholgado/performancetest/LeakActivity;)V
複製代碼
能夠看到,咱們使用LeakActivity
對象來初始化LeakActivity$2
對象,這樣就解釋了爲何LeakActivity.java
會出現內存泄漏的現象。
KLeakActivity.kt
中咱們關注startAsyncWork
這個方法的字節碼,由於其餘部分和Java
寫法是同樣的,只有這部分不同。 該方法的字節碼以下所示:
.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
複製代碼
能夠看出,與Java
字節碼中初始化一個包含Activity
引用的實現Runnable
接口對象不一樣的是,這個字節碼使用了靜態變量來執行靜態方法。
sget-object v0,
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1; ->
INSTANCE:Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;
複製代碼
咱們深刻KLeakActivity\$startAsyncWork\$work$1
的字節碼看下:
.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\$startAsyncWork\$work$1
實現了Runnable
接口,可是其擁有的是靜態方法,所以不須要外部類對象的引用。 因此Kotlin
不出現內存泄漏的緣由出來了,在Kotlin
中,咱們使用lambda
(其實是一個 SAM)來代替Java
中的匿名內部類。沒有Activity
對象的引用就不會發生內存泄漏。 固然並非說只有Kotlin
纔有這個功能,若是你使用Java8
中的lambda
的話,同樣不會發生內存泄漏。 若是你想對這部分作更深刻的瞭解,能夠參看這篇文章Translation of Lambda Expressions。
若是有須要翻譯的同窗能夠在評論裏面說就行啦。
如今把其中比較重要的一部分說下:上述段落中的Lamdba表達式能夠被認爲是靜態方法。由於它們沒有使用類中的實例屬性,例如使用super、this或者該類中的成員變量。 咱們把這種Lambda稱爲Non-instance-capturing lambdas(這裏我感受仍是不翻譯爲好,英文原文更原汁原味些)。而那些須要實例屬性的Lambda則稱爲instance-capturing lambdas。
Non-instance-capturing lambdas能夠被認爲是private、static方法。instance-capturing lambdas能夠被認爲是普通的private、instance方法。
這段話放在咱們這篇文章中是什麼意思呢?
由於咱們Kotlin
中的lambda
沒有使用實例屬性,因此其是一個non-instance-capturing lambda,能夠被當成靜態方法來看待,就不會產生內存泄漏。
若是咱們在其中添加一個外部類對象屬性的引用的話,這個lambda
就轉變成instance-capturing lambdas,就會產生內存泄漏。
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()
}
}
複製代碼
如上述代碼所示,咱們使用了test
這個實例屬性,就會致使內存泄漏。 startAsyncWork
方法的字節碼以下所示:
.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
複製代碼
很明顯,咱們傳入了KLeakActivity
的對象,所以就會致使內存泄漏。
啊,終於翻譯完了,能夠去睡覺了!!