LeakCanary——Android和Java的内存泄露检测库


前言

简介

Squar LeakCanary :https://github.com/square/leakcanary

A memory leak detection library for Android and Java.
一个Android和Java的内存泄露检测库。

“A small leak will sink a great ship.” - Benjamin Franklin
小漏不补沉大船。——本杰明 富兰克林

为什么要做内存泄漏检测

当内存吃紧的时候,到处都有可能引发 OOM,OOM更深层次的问题可能是: 内存泄露。

一些对象有着有限的生命周期。当这些对象所要做的事情完成了,我们希望他们会被回收掉。但是如果有一系列对这个对象的引用,那么在我们期待这个对象生命周期结束的时候被收回的时候,它是不会被回收的。它还会占用内存,这就造成了内存泄露。持续累加,内存很快被耗尽。

比如,当 Activity.onDestroy 被调用之后,activity 以及它涉及到的 view 和相关的 bitmap 都应该被回收。但是,如果有一个后台线程持有这个 activity 的引用,那么 activity 对应的内存就不能被回收。这最终将会导致内存耗尽,然后因为 OOM 而 crash。

因为Java从语言上保证,任何对象都是可达的,所有的不可达对象都由GC管理。

Android中常见的内存泄漏汇总

1、集合类泄漏
集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。比如上面的典型例子就是其中一种情况,当然实际上我们在项目中肯定不会写这么 2B 的代码,但稍不注意还是很容易出现这种情况,比如我们都喜欢通过 HashMap 做一些缓存之类的事,这种情况就要多留一些心眼。

2、单例造成的内存泄漏
由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。

3、匿名内部类/非静态内部类和异步线程

  • 非静态内部类创建静态实例造成的内存泄漏
  • 匿名内部类
  • Handler 造成的内存泄漏

解决:推荐使用静态内部类 + WeakReference 这种方式。每次使用前注意判空。

4、尽量避免使用 static 成员变量

从前对战内存泄露

排查内存泄露是一个全手工的过程,以下几个关键步骤:

1.通过某某统计平台,了解 OutOfMemoryError 情况。

2.重现问题。为了重现问题,机型非常重要,因为一些问题只在特定的设备上会出现。 当然,为了确定复现步骤,你需要一遍一遍地去尝试。

3.在发生内存泄露的时候,把内存 Dump 出来。

4.然后,你需要在 MAT 之类的内存分析工具中反复查看,找到那些原本该被回收掉的对象。

5.计算这个对象到 GC roots 的最短强引用路径。

6.确定引用路径中的哪个引用是不该有的,然后修复问题。


正文

开始使用

build.gradle 中加入引用,不同的编译使用不同的引用:

1
2
3
4
5
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
}

In your Application class:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ExampleApplication extends Application {

@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
// Normal app init code...
}
}

当在你的debug构建过程中出现内存泄露时,LeakCanary将会自动展示一个通知栏。
当点击该通知时,会跳转到具体的页面,展示出Leak的引用路径,如下图所示:

如何使用

使用RefWatcher监控那些本该被GC回收的对象。

1
2
3
4
RefWatcher refWatcher = {...};

//监控
refWatcher.watch(xxxx);

LeakCanary.install() 会返回一个预定义的RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用Activity.onDestroy()之后泄露的 activity。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ExampleApplication extends Application {

public static RefWatcher getRefWatcher(Context context) {
ExampleApplication application = (ExampleApplication) context.getApplicationContext();
return application.refWatcher;
}

private RefWatcher refWatcher;

@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
refWatcher = LeakCanary.install(this);
}
}

使用RefWatcher 监控Fragment

1
2
3
4
5
6
7
8
public abstract class BaseFragment extends Fragment {

@Override public void onDestroyView() {
super.onDestroyView();
RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
refWatcher.watch(this);
}
}

工作机制

1.RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。

2.然后在后台线程检查引用是否被清除,如果没有,调用GC。

3.如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。

4.在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。

5.得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄露。

6.HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。

7.引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
  private final Set<String> retainedKeys;
private final ReferenceQueue<Object> queue;


/**
* Watches the provided references and checks if it can be GCed. This method is non blocking,
* the check is done on the {@link Executor} this {@link RefWatcher} has been constructed with.
*
* @param referenceName An logical identifier for the watched object.
*/
public void watch(Object watchedReference, String referenceName) {
checkNotNull(watchedReference, "watchedReference");
checkNotNull(referenceName, "referenceName");
if (debuggerControl.isDebuggerAttached()) {
return;
}
final long watchStartNanoTime = System.nanoTime();
String key = UUID.randomUUID().toString();
retainedKeys.add(key);
final KeyedWeakReference reference =
new KeyedWeakReference(watchedReference, key, referenceName, queue);

watchExecutor.execute(new Runnable() {
@Override public void run() {
ensureGone(reference, watchStartNanoTime);
}
});
}

void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();

long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
removeWeaklyReachableReferences();
if (gone(reference) || debuggerControl.isDebuggerAttached()) {
return;
}
gcTrigger.runGc();
removeWeaklyReachableReferences();
if (!gone(reference)) {
long startDumpHeap = System.nanoTime();
long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

File heapDumpFile = heapDumper.dumpHeap();

if (heapDumpFile == HeapDumper.NO_DUMP) {
// Could not dump the heap, abort.
return;
}
long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
heapdumpListener.analyze(
new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
gcDurationMs, heapDumpDurationMs));
}
}

private boolean gone(KeyedWeakReference reference) {
return !retainedKeys.contains(reference.key);
}

private void removeWeaklyReachableReferences() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
KeyedWeakReference ref;
while ((ref = (KeyedWeakReference) queue.poll()) != null) {
retainedKeys.remove(ref.key);
}
}

RefWatcher 就是通过弱引用及其队列来实现监控的:

有两个很重要的结构: retainedKeys 和 queue

retainedKeys 代表没被gc 回收的对象,

queue中的弱引用代表的是被gc了的对象,

通过对比两个结构就可以监控对象是不是被回收了;

retainedKeys存放了RefWatcher为每个被监控的对象生成的唯一key;

同时每个被监控对象的弱引用(KeyedWeakReference)关联了 其对应的key 和 queue,这样对象若被回收,则其对应的弱引用会被入队到queue中;

removeWeaklyReachableReferences(..)

所做的就是把存在与queue中的弱引用的key 从 retainedKeys 中删除。

如何复制 leak trace?

看查看复制,也可以通过分享按钮把这些东西分享出去。

leak trace 之外

有时,leak trace 不够清晰,你需要通过 MAT 或者 YourKit 深挖 dump 文件。
通过以下方法,你能找到问题所在:

1.查找所有的 com.squareup.leakcanary.KeyedWeakReference 实例。
2.检查 key 字段
3.Find the KeyedWeakReference that has a key field equal to the reference key reported by LeakCanary.
4.找到 key 和 和 logcat 输出的 key 值一样的 KeyedWeakReference。
5.referent 字段对应的就是泄露的对象。
6.剩下的,就是动手修复了。最好是检查到 GC root 的最短强引用路径开始。


参考

  1. LeakCanary开源项目
  2. LeakCanary: 让内存泄露无所遁形

欢迎转载,请注明本文的链接地址: http://blog.neday.cn/2018/03/02/LeakCanary——Android和Java的内存泄露检测库/

苏晟, nEdAy wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
坚持原创技术分享,您的支持将鼓励我继续创作!