本篇文章借助了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 providers
在Application
实例创建之后但是在Application
的onCreate()
方法调用之前被创建。leakcanary-object-watcher-android
在其AndroidManifest.xml
文件中定义了一个未公开的ContentProvider
。当安装该ContentProvider后,它将向应用程序添加活动和片段生命周期侦听器。
Fundamentals
什么是内存泄漏
在一个基于Java的运行环境中,内存泄漏是一个程序错误,该错误会导致应用保留不再需要的对象的引用。结果就是无法回收为该对象分配的内存,最终导致OutOfMemoryError崩溃。
内存泄漏的常见原因
大多数内存泄漏是由与对象生命周期相关的错误引起的。这里有几个Android中常见的错误。
- 在一个对象中存储Activity的Context作为成员变量,那么当Activity由于屏幕旋转等配置改变导致Activity重新创建的时候,前一个Activity由于被持有而不能被回收。
- 注册一个监听器,广播接收器或者RxJava订阅到一个具有生命周期的对象,但是当该对象生命周期结束的时候没有取消订阅导致该对象不能被回收。
- 在一个静态成员变量中存储一个View,但是在View
detached
的时候没有清除静态成员变量(将该静态成员变量赋值为null)。
为什么我应该使用LeakCanary
在Android应用中内存泄漏很常见,小的内存泄露不断积累导致应用内存耗尽最终导致OutOfMemoryError。使用LeakCanary可以发现修复许多内存泄漏问题,降低OutOfMemoryError的发生概率。
LeakCanary是怎么工作的?
检测保留的对象
LeakCanary的基础是一个叫做ObjectWatcher Android的library。它hook了Android的生命周期,当activity和fragment 被销毁并且应该被垃圾回收时候自动检测。这些被销毁的对象被传递给ObjectWatcher
, ObjectWatcher
持有这些被销毁对象的弱引用(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在堆转储之前会等待保留的对象到达一个阈值,并且会显示一个最新数量的一个通知。
注意:当应用可见的时候默认的阈值是5,应用不可见的时候阈值是1。如果你看到了保留的对象的通知然后将应用切换到后台(例如点击home键),那么阈值就会从5变到1,LeakCanary会立即进行堆转储。点击通知也可以强制LeakCanary立即进行堆转储。
堆转储(Dumping the heap)
当保留的对象数量达到阈值以后,LeakCanary会将Java heap信息存储到一个.hprof
文件中,该文件存储在在Android的文件系统中。该过程会冻结应用很短的一段时间,并显示如下一个toast。
冻结的原因,看源码是当前线程等待了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堆中保留的对象。如果找不到保留的对象,那么它们很可能在堆转储的过程中被回收了。
对于每个被保留的对象,LeakCanary会找出阻止该保留对象被回收的引用链:泄漏路径。泄露路径就是从GC ROOTS
到保留对象的最短的强引用路径的别名。确定泄漏路径以后,LeakCanary使用它对Android框架的了解来找出在泄漏路径上是谁泄漏了。
当分析完毕以后,LeakCanary会显示一个通知,点击通知可以查看分析结果。
如何修复内存泄漏?
对于每个泄漏的对象,LeakCanary计算一个泄漏路径并在UI上展示出来。
泄漏路径也会在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 = null
和mParent != 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,那么我们就解决了泄漏问题。
参考链接