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<Toast> 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<T> {

  private final AtomicReference<T> 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<View>()
}

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

参考链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342