[译]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

相关文章
相关标签/搜索