LeakCanary官方文档翻译

本篇文章借助了Google翻译对square/leakcanary的官方文档Getting started部分和Fundamentals部分进行了翻译并加入了自己的理解。

LeakCanary版本:2.0-beta-4

Getting started

在app的build.gradle文件中添加依赖

dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-beta-4'
}

使用debugImplementation,因为LeakCanary只应该在调试版本中使用。

这就完了,使用老版本的LeakCanary的时候还需要在Application中在做一些初始化操作,现在完全不必要了。为啥呢?

在Android应用中,content providersApplication实例创建之后但是在ApplicationonCreate()方法调用之前被创建。leakcanary-object-watcher-android在其AndroidManifest.xml文件中定义了一个未公开的ContentProvider。当安装该ContentProvider后,它将向应用程序添加活动和片段生命周期侦听器。

ContentProvider.png

Fundamentals

什么是内存泄漏

在一个基于Java的运行环境中,内存泄漏是一个程序错误,该错误会导致应用保留不再需要的对象的引用。结果就是无法回收为该对象分配的内存,最终导致OutOfMemoryError崩溃。

内存泄漏的常见原因

大多数内存泄漏是由与对象生命周期相关的错误引起的。这里有几个Android中常见的错误。

  • 在一个对象中存储Activity的Context作为成员变量,那么当Activity由于屏幕旋转等配置改变导致Activity重新创建的时候,前一个Activity由于被持有而不能被回收。
  • 注册一个监听器,广播接收器或者RxJava订阅到一个具有生命周期的对象,但是当该对象生命周期结束的时候没有取消订阅导致该对象不能被回收。
  • 在一个静态成员变量中存储一个View,但是在Viewdetached的时候没有清除静态成员变量(将该静态成员变量赋值为null)。

为什么我应该使用LeakCanary

在Android应用中内存泄漏很常见,小的内存泄露不断积累导致应用内存耗尽最终导致OutOfMemoryError。使用LeakCanary可以发现修复许多内存泄漏问题,降低OutOfMemoryError的发生概率。

LeakCanary是怎么工作的?

检测保留的对象

LeakCanary的基础是一个叫做ObjectWatcher Android的library。它hook了Android的生命周期,当activity和fragment 被销毁并且应该被垃圾回收时候自动检测。这些被销毁的对象被传递给ObjectWatcherObjectWatcher持有这些被销毁对象的弱引用(weak references)。你也可以观察任何不再需要的对象,例如一个detached view, 一个销毁的presenter等等。

AppWatcher.objectWatcher.watch(myDetachedView)

如果弱引用在等待5秒钟并运行垃圾收集器后仍未被清除,那么被观察的对象就被认为是保留的(retained,在生命周期结束后仍然保留),并存在潜在的泄漏。LeakCanary会在Logcat中输出这些日志。

D LeakCanary: Watching instance of com.example.leakcanary.MainActivity 
// 5 seconds later...
D LeakCanary: Found 1 retained object

LeakCanary在堆转储之前会等待保留的对象到达一个阈值,并且会显示一个最新数量的一个通知。


retained-notification.png

注意:当应用可见的时候默认的阈值是5,应用不可见的时候阈值是1。如果你看到了保留的对象的通知然后将应用切换到后台(例如点击home键),那么阈值就会从5变到1,LeakCanary会立即进行堆转储。点击通知也可以强制LeakCanary立即进行堆转储。

堆转储(Dumping the heap)

当保留的对象数量达到阈值以后,LeakCanary会将Java heap信息存储到一个.hprof文件中,该文件存储在在Android的文件系统中。该过程会冻结应用很短的一段时间,并显示如下一个toast。

dumping-toast.png

冻结的原因,看源码是当前线程等待了5秒钟。

@Override public File dumpHeap() {
    File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();

    if (heapDumpFile == RETRY_LATER) {
      return RETRY_LATER;
    }

    FutureResult waitingForToast = new FutureResult<>();
    showToast(waitingForToast);
    //注释1处,FutureResult的 wait 方法。
    if (!waitingForToast.wait(5, SECONDS)) {
      CanaryLog.d("Did not dump heap, too much time waiting for Toast.");
      return RETRY_LATER;
    }

    Toast toast = waitingForToast.get();
    try {
      Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
      cancelToast(toast);
      return heapDumpFile;
    } catch (Exception e) {
      CanaryLog.d(e, "Could not dump heap");
      // Abort heap dump
      return RETRY_LATER;
    }
  }

注释1处,FutureResult的 wait 方法。

public final class FutureResult {

  private final AtomicReference resultHolder;
  private final CountDownLatch latch;

  public FutureResult() {
    resultHolder = new AtomicReference<>();
    latch = new CountDownLatch(1);
  }

  public boolean wait(long timeout, TimeUnit unit) {
    try {
      return latch.await(timeout, unit);
    } catch (InterruptedException e) {
      throw new RuntimeException("Did not expect thread to be interrupted", e);
    }
  }
  //...
}

分析堆信息

LeakCanary使用shark来转换.hprof文件并定位Java堆中保留的对象。如果找不到保留的对象,那么它们很可能在堆转储的过程中被回收了。

collected.png

对于每个被保留的对象,LeakCanary会找出阻止该保留对象被回收的引用链:泄漏路径。泄露路径就是从GC ROOTS到保留对象的最短的强引用路径的别名。确定泄漏路径以后,LeakCanary使用它对Android框架的了解来找出在泄漏路径上是谁泄漏了。

当分析完毕以后,LeakCanary会显示一个通知,点击通知可以查看分析结果。

analysis-done.png

如何修复内存泄漏?

对于每个泄漏的对象,LeakCanary计算一个泄漏路径并在UI上展示出来。


leaktrace.png

泄漏路径也会在Logcat中输出:

    ┬
    ├─ leakcanary.internal.InternalLeakCanary
    │    Leaking: NO (it's a GC root and a class is never leaking)
    │    ↓ static InternalLeakCanary.application
    ├─ com.example.leakcanary.ExampleApplication
    │    Leaking: NO (Application is a singleton)
    │    ↓ ExampleApplication.leakedViews
    │                         ~~~~~~~~~~~
    ├─ java.util.ArrayList
    │    Leaking: UNKNOWN
    │    ↓ ArrayList.elementData
    │                ~~~~~~~~~~~
    ├─ java.lang.Object[]
    │    Leaking: UNKNOWN
    │    ↓ array Object[].[0]
    │                     ~~~
    ├─ android.widget.TextView
    │    Leaking: YES (View detached and has parent)
    │    View#mAttachInfo is null (view detached)
    │    View#mParent is set
    │    View.mWindowAttachCount=1
    │    ↓ TextView.mContext
    ╰→ com.example.leakcanary.MainActivity
         Leaking: YES (RefWatcher was watching this and MainActivity#mDestroyed
is true)

对象和引用

├─ android.widget.TextView

泄漏路径中的每个节点是一个Java对象。对象的类型可能是一个class对象,一个对象数组或者一个普通的对象。

│    ↓ TextView.mContext

GC ROOTS向下,每个节点都有对下一个节点的引用。在UI上,引用是紫色的。在Logcat中,引用在以向下箭头开头的行上。

GC Root

┬
├─ leakcanary.internal.InternalLeakCanary
│    Leaking: NO (it's a GC root and a class is never leaking)

在泄漏路径的顶部是GC Root。GC Root是一些总是可达的特殊对象。这里有四种GC Root值得一提:

  • 局部变量(Local variables),属于线程栈中的变量。
  • 活动的Java线程实例。
  • 类(Class)对象,永远不会再Android上卸载。
  • 本地引用(Native references),由本地代码控制。

泄漏的对象

╰→ com.example.leakcanary.MainActivity
     Leaking: YES (RefWatcher was watching this and MainActivity#mDestroyed
is true)

在泄漏路径的底部是泄漏的对象。该对象已传递给AppWatcher.objectWatcher以确认将被垃圾回收,并且最终没有被垃圾回收,从而触发了LeakCanary。

引用链

...
    │    ↓ static InternalLeakCanary.application
...
    │    ↓ ExampleApplication.leakedViews
...
    │    ↓ ArrayList.elementData
...
    │    ↓ array Object[].[0]
...
    │    ↓ TextView.mContext
...

从GC ROOTS到泄漏对象之间的引用链阻止了泄漏对象被垃圾回收。如果你可以确定某个引用在某个时间点不应该存在,那么你可以弄清楚为什么它仍然存在并修复内存泄漏。

启发式和标签(Heuristics and labels)

├─ android.widget.TextView
│    Leaking: YES (View detached and has parent)
│    View#mAttachInfo is null (view detached)
│    View#mParent is set
│    View.mWindowAttachCount=1

LeakCanary使用启发式的方式来确定泄漏路径上的节点的生命周期状态,从而确定它们是否泄漏。例如,如果一个View显示View#mAttachInfo = nullmParent != null,那么这个View就是处于View detached and has parent的状态,那么这个View可能泄漏了。在泄漏路径上,每一个节点都会有一个Leaking状态Leaking: YES / NO / UNKNOWN并在括号中解释为什么这个节点泄漏了。LeakCanary还可以显示有关节点状态的额外信息,例如View.mWindowAttachCount=1。LeakCanary带有一组默认启发式方法AndroidObjectInspectors。你可以添加你自己的启发式方法通过更改LeakCanary.Config.objectInspectors。

疑问:啥是启发式?在好多地方都看到heuristics这个单词。

缩小泄漏原因

    ┬
    ├─ android.provider.FontsContract
    │    Leaking: NO (ExampleApplication↓ is not leaking and a class is never leaking)
    │    GC Root: System class
    │    ↓ static FontsContract.sContext
    ├─ com.example.leakcanary.ExampleApplication
    │    Leaking: NO (Application is a singleton)
    │    ExampleApplication does not wrap an activity context
    │    ↓ ExampleApplication.leakedViews
    │                         ~~~~~~~~~~~
    ├─ java.util.ArrayList
    │    Leaking: UNKNOWN
    │    ↓ ArrayList.elementData
    │                ~~~~~~~~~~~
    ├─ java.lang.Object[]
    │    Leaking: UNKNOWN
    │    ↓ array Object[].[1]
    │                     ~~~
    ├─ android.widget.TextView
    │    Leaking: YES (View.mContext references a destroyed activity)
    │    ↓ TextView.mContext
    ╰→ com.example.leakcanary.MainActivity
         Leaking: YES (TextView↑ is leaking and Activity#mDestroyed is true and ObjectWatcher was watching this)

如果一个节点没有泄漏,那么指向该节点的任何先前的引用都不是泄漏的来源,也不会泄漏。相似的,如果一个节点泄漏了,那么该节点下面的所有节点也泄漏了。据此,我们可以推断泄漏是由最后一个没有泄漏的节点(Leaking: NO )和第一个泄漏的节点(Leaking: YES)之间的引用导致的。

LeakCanary使用在UI上使用红色下划线标记这些引用,在在Logcat中使用 ~~~~ 标记。这些被标记的引用只可能是造成泄漏的原因。这些引用你应该花时间来调查。

在这个例子中,最后一个没有泄漏(Leaking: NO)的节点是com.example.leakcanary.ExampleApplication,第一个泄漏的节点是android.widget.TextView。所以泄漏是由这两个节点之间的三个引用之一导致的。

...
    │    ↓ ExampleApplication.leakedViews
    │                         ~~~~~~~~~~~
...
    │    ↓ ArrayList.elementData
    │                ~~~~~~~~~~~
...
    │    ↓ array Object[].[0]
    │                     ~~~
...

查看源代码可以看到ExampleApplication有一个列表成员变量:

open class ExampleApplication : Application() {
  val leakedViews = mutableListOf()
}

由于ArrayList自身实现的bug导致内存泄漏是不太可能的,所以泄漏发生是因为我们向ExampleApplication.leakedViews中添加View。如果我们停止向ExampleApplication.leakedViews中添加View,那么我们就解决了泄漏问题。

参考链接

  • square/leakcanary

  • Getting started

  • Fundamentals

  • How does LeakCanary get installed by only adding a dependency?

你可能感兴趣的:(LeakCanary官方文档翻译)