[譯]Kotlin是如何幫助你避免內存泄漏的?

首先,本文的代碼位置在github.com/marcosholga…中的kotlin-mem-leak分支上。html

使人困惑的現象

我是經過建立一個會致使內存泄漏的Activity,而後觀察其使用JavaKotlin編寫時的表現來進行測試的。 其中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寫的LeakActivityKotlin寫的KLeakActivity進行測試。測試結果是Java寫的出現內存泄漏,而Kotlin寫的則沒有出現內存泄漏。 這個問題困擾了我很長時間,一度接近自閉。。 bash

而後某天,我忽然靈光一現,感受應該和編譯後字節碼有關係。

分析LeakActivity.java的字節碼

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的字節碼

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的對象,所以就會致使內存泄漏。

啊,終於翻譯完了,能夠去睡覺了!!

原文地址

How Kotlin helps you avoid memory leaks

相關文章
相關標籤/搜索