前言
内存泄漏是一个开发不得不面对的问题,在 Android 中有许多工具都可以用来检测内存泄漏,比如:MAT、Profile等。
但是这些工具都需要手动操作,比较麻烦,所以我们需要一个能够自动检测的工具,它就是LeakCanary。
今天就来了解一下原理,毕竟面试也是常问的问题。
前置知识
在学习之前,先来了解一下相关的知识。
内存泄漏:内存泄漏指的是该释放的内存没有被释放,导致可用内存减少,严重会引起内存溢出。
由于内存泄漏有可能会造成严重的后果,所以我们要对它进行检测,以便及时发现问题,排除隐患。
从内存泄漏的定义上可以知道,我们需要检测对象的释放,那要怎么检测对象有没有释放呢?
在 Java
中有四种引用,强引用、软引用、弱引用、虚引用四种。通常我们写的就是强引用。
检测对象的释放就要用到其中的弱引用。
弱引用的构造函数里可以传递一个 ReferenceQueue
,当弱引用的指向的对象被回收的时候,会把它放进 ReferenceQueue
中,所以依据这个我们就能实现内存泄漏的检测。
我们来写一个例子看看
1
2
3
4
5
6
7
8
9
10
11
12
|
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
class LeakDetect {
public static void main(String[] args) throws InterruptedException {
ReferenceQueue<Object> queue = new ReferenceQueue();
WeakReference<Object> ref = new WeakReference<>(new Object(), queue);
System.out.println("before gc: ref get: " + ref.get() + ", queue: " + queue.poll());
System.gc();
Thread.sleep(1000);
System.out.println("after gc: ref get: " + ref.get() + ", queue: " + queue.poll());
}
}
|
1
2
|
before gc: ref get: java.lang.Object@2a139a55, queue: null
after gc: ref get: null, queue: java.lang.ref.WeakReference@15db9742
|
可以看到 Object
被回收后会放到 ReferenceQueue
中,而原来的 WeakReference
中已经获取不到了。
接下来写一个内存泄漏的场景,让 Object
保持强引用
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
class LeakDetect {
public static void main(String[] args) throws InterruptedException {
ReferenceQueue<Object> queue = new ReferenceQueue();
Object obj = new Object();
WeakReference<Object> ref = new WeakReference<>(obj, queue);
System.out.println("before gc: ref get: " + ref.get() + ", queue: " + queue.poll());
System.gc();
Thread.sleep(1000);
System.out.println("after gc: ref get: " + ref.get() + ", queue: " + queue.poll());
}
}
|
1
2
|
before gc: ref get: java.lang.Object@2a139a55, queue: null
after gc: ref get: java.lang.Object@2a139a55, queue: null
|
在这里 obj
由于一直持有强引用,所以在 GC
的时候并不会回收,所以通过 WeakReference
还是可以获取到,而 ReferenceQueue
里面则没有,所以这里就发生了泄漏。
Activity泄漏检测
有了之前的知识做铺垫,就可以来看下 LeakCanary
是怎么实现内存泄漏检测的。我们以 Activity
的泄漏检测为例。
在 Android
中 Activity
的销毁时机我们是不太确定的,但是我们可以确定的是在 Activity
的生命周期结束的时候就认为 Activity
要被销毁掉,也就是在 onDestroy
调用的时候, Activity
应该就将要被回收了。
Android
可以使用 ActivityLifecycleCallbacks
来监听所有 Activity
的回调,可以很方便的检测 Activity
的泄漏
1
2
3
4
5
6
7
8
9
10
11
12
|
public final class ActivityRefWatcher {
private final ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacksAdapter() {
public void onActivityDestroyed(Activity activity) {
ActivityRefWatcher.this.refWatcher.watch(activity);
}
};
public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
Application application = (Application)context.getApplicationContext();
ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
}
}
|
在 ActivityRefWatcher
中把 Activity
的 Destroy
交给了 RefWatcher
的 watch
处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public final class RefWatcher {
// ...
private final Set<String> retainedKeys;
private final ReferenceQueue<Object> queue;
public void watch(Object watchedReference) {
this.watch(watchedReference, "");
}
public void watch(Object watchedReference, String referenceName) {
if (this != DISABLED) {
long watchStartNanoTime = System.nanoTime();
String key = UUID.randomUUID().toString();
this.retainedKeys.add(key);
KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, this.queue);
this.ensureGoneAsync(watchStartNanoTime, reference);
}
}
}
|
在 watch
中用 UUID
生成了一个随机字符串,然后把它放到了 Set
中,这样就可以把它和引用对象保持一个关联,相当于打了标签。使用 UUID
可以避免对象重复。
接着创建了 KeyedWeakReference
,它就是继承了 WeakReference
然后添加了 key
和 name
而已,如下所示。
1
2
3
4
5
6
7
8
9
10
11
|
final class KeyedWeakReference extends WeakReference<Object> {
public final String key;
public final String name;
KeyedWeakReference(Object referent, String key, String name,
ReferenceQueue<Object> referenceQueue) {
super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue"));
this.key = checkNotNull(key, "key");
this.name = checkNotNull(name, "name");
}
}
|
在 watch
的最后调用了 ensureGoneAsync
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
|
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
this.watchExecutor.execute(new Retryable() {
public Result run() {
return RefWatcher.this.ensureGone(reference, watchStartNanoTime);
}
});
}
Result ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
long watchDurationMs = TimeUnit.NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
this.removeWeaklyReachableReferences();
if (this.debuggerControl.isDebuggerAttached()) {
return Result.RETRY;
} else if (this.gone(reference)) {
return Result.DONE;
} else {
this.gcTrigger.runGc();
this.removeWeaklyReachableReferences();
if (!this.gone(reference)) {
long startDumpHeap = System.nanoTime();
long gcDurationMs = TimeUnit.NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
File heapDumpFile = this.heapDumper.dumpHeap();
if (heapDumpFile == HeapDumper.RETRY_LATER) {
return Result.RETRY;
}
long heapDumpDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
HeapDump heapDump = this.heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key).referenceName(reference.name).watchDurationMs(watchDurationMs).gcDurationMs(gcDurationMs).heapDumpDurationMs(heapDumpDurationMs).build();
this.heapdumpListener.analyze(heapDump);
}
return Result.DONE;
}
}
|
在 ensureGone
里会先调用 removeWeaklyReachableReferences
来把 queue
里的对象从 Set
中移除掉,根据前置知识我们知道这里的对象都是被释放掉的,所以在这里我们要从 Set
中移除掉对应的 key
,表示这个对象被释放了,没有泄漏。
removeWeaklyReachableReferences
如下所示
1
2
3
4
5
6
7
|
private void removeWeaklyReachableReferences() {
KeyedWeakReference ref;
while((ref = (KeyedWeakReference)this.queue.poll()) != null) {
this.retainedKeys.remove(ref.key);
}
}
|
接着我们看 gone
方法
1
2
3
|
private boolean gone(KeyedWeakReference reference) {
return !this.retainedKeys.contains(reference.key);
}
|
这里的逻辑比较简单,如果 retainedKeys
这个 Set
中包含了 KeyedWeakReference
中的 key
说明就没有释放。如果不包含就说明释放了,也就是在上一步 removeWeaklyReachableReferences
会把释放的移除掉。
如果释放了就没有泄漏,直接返回。接着往下就是 retainedKeys
中还有对应的 key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
this.gcTrigger.runGc();
this.removeWeaklyReachableReferences();
if (!this.gone(reference)) {
long startDumpHeap = System.nanoTime();
long gcDurationMs = TimeUnit.NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
File heapDumpFile = this.heapDumper.dumpHeap();
if (heapDumpFile == HeapDumper.RETRY_LATER) {
return Result.RETRY;
}
long heapDumpDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
HeapDump heapDump = this.heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key).referenceName(reference.name).watchDurationMs(watchDurationMs).gcDurationMs(gcDurationMs).heapDumpDurationMs(heapDumpDurationMs).build();
this.heapdumpListener.analyze(heapDump);
}
|
在这里会先触发一次 GC
,为了确保触发了 GC
,然后再次调用 removeWeaklyReachableReferences
来移除被回收的 key
。
如果这次 key
还存在就说明发生了泄漏。
接着就是把堆栈信息保存起来,生成泄漏报告到文件中。
检测时机
在上一小结中当 Activity
的 onDestory
调用后会执行检测任务,而检测是会耗性能的,在主线程检测是有可能引发 ANR
的,所以我们来看看这里在什么时机进行检测的。
我们在源码中发现在 watch
方法中最后会调用 ensureGoneAsync
,从名字上看像是异步的,来具体看下
1
2
3
4
5
6
7
8
9
|
private final WatchExecutor watchExecutor;
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
this.watchExecutor.execute(new Retryable() {
public Result run() {
return RefWatcher.this.ensureGone(reference, watchStartNanoTime);
}
});
}
|
在这里可以看到这里使用了 watchExecutor
来执行这个任务,而 watchExecutor
的类型是 WatchExecutor
是通过构造函数传进来的。
经过跟踪发现 WatchExecutor
是个接口,在 Android
的实现为 AndroidWatchExecutor
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
|
public final class AndroidWatchExecutor implements WatchExecutor {
private final Handler mainHandler = new Handler(Looper.getMainLooper());
private final Handler backgroundHandler;
public AndroidWatchExecutor(long initialDelayMillis) {
HandlerThread handlerThread = new HandlerThread("LeakCanary-Heap-Dump");
handlerThread.start();
this.backgroundHandler = new Handler(handlerThread.getLooper());
}
public void execute(@NonNull Retryable retryable) {
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
this.waitForIdle(retryable, 0);
} else {
this.postWaitForIdle(retryable, 0);
}
}
private void postWaitForIdle(final Retryable retryable, final int failedAttempts) {
this.mainHandler.post(new Runnable() {
public void run() {
AndroidWatchExecutor.this.waitForIdle(retryable, failedAttempts);
}
});
}
private void waitForIdle(final Retryable retryable, final int failedAttempts) {
Looper.myQueue().addIdleHandler(new IdleHandler() {
public boolean queueIdle() {
AndroidWatchExecutor.this.postToBackgroundWithDelay(retryable, failedAttempts);
return false;
}
});
}
private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
long exponentialBackoffFactor = (long)Math.min(Math.pow(2.0D, (double)failedAttempts), (double)this.maxBackoffFactor);
long delayMillis = this.initialDelayMillis * exponentialBackoffFactor;
this.backgroundHandler.postDelayed(new Runnable() {
public void run() {
Result result = retryable.run();
if (result == Result.RETRY) {
AndroidWatchExecutor.this.postWaitForIdle(retryable, failedAttempts + 1);
}
}
}, delayMillis);
}
}
|
可以看到在 execute
中会最终调用 waitForIdle
,在这里会调用主线程的 addIdleHandler
,只有在主线程空闲的时间才会执行,避免与主线程抢占 CPU
资源。
然后在 queueIdld
中又把任务提交到了自己创建的 HandlerThread
线程中去执行,这样就不会导致主线程 ANR
。
过滤一些系统内存泄漏
我们的系统自身也可能存在泄漏的情况,这些就不是开发者能够控制的,所以我们要把这些情况排除在外, LeakCanary
也考虑到了,值的我们借鉴和学习
1
2
3
4
5
6
|
public final class LeakCanary {
@NonNull
public static RefWatcher install(@NonNull Application application) {
return ((AndroidRefWatcherBuilder)refWatcher(application).listenerServiceClass(DisplayLeakService.class).excludedRefs(AndroidExcludedRefs.createAppDefaults().build())).buildAndInstall();
}
}
|
在这里调用了 excludedRefs
把一些常见的场景给排除在外了。
它们定义在 AndroidExcludedRefs
来看一些例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public enum AndroidExcludedRefs {
ACTIVITY_CLIENT_RECORD__NEXT_IDLE(VERSION.SDK_INT >= 19 && VERSION.SDK_INT <= 21) {
void add(Builder excluded) {
excluded.instanceField("android.app.ActivityThread$ActivityClientRecord", "nextIdle").reason("Android AOSP sometimes keeps a reference to a destroyed activity as a nextIdle client record in the android.app.ActivityThread.mActivities map. Not sure what's going on there, input welcome.");
}
},
SPAN_CONTROLLER(VERSION.SDK_INT <= 19) {
void add(Builder excluded) {
String reason = "Editor inserts a special span, which has a reference to the EditText. That span is a NoCopySpan, which makes sure it gets dropped when creating a new SpannableStringBuilder from a given CharSequence. TextView.onSaveInstanceState() does a copy of its mText before saving it in the bundle. Prior to KitKat, that copy was done using the SpannableString constructor, instead of SpannableStringBuilder. The SpannableString constructor does not drop NoCopySpan spans. So we end up with a saved state that holds a reference to the textview and therefore the entire view hierarchy & activity context. Fix: https://github.com/android/platform_frameworks_base/commit/af7dcdf35a37d7a7dbaad7d9869c1c91bce2272b . To fix this, you could override TextView.onSaveInstanceState(), and then use reflection to access TextView.SavedState.mText and clear the NoCopySpan spans.";
excluded.instanceField("android.widget.Editor$EasyEditSpanController", "this$0").reason(reason);
excluded.instanceField("android.widget.Editor$SpanController", "this$0").reason(reason);
}
},
}
|
自定义检测
LeakCanary
帮我们处理了常见的情况,如果我们想要自己检测一些类有没有泄漏该怎么做?
根据上面的分析 watch
中传入的是个 Object
,所以我们把要检测的对象传给 watch
方法就行了
1
2
3
4
5
6
7
8
9
10
11
12
|
public class App extends Application {
private RefWatcher refWatcher;
@Override
public void onCreate() {
super.onCreate();
refWatcher = LeakCanary.install(this);
}
public RefWatcher getRefWatcher() {
return refWatcher;
}
}
|
接着调用 watch
方法就行了
1
2
3
4
5
6
7
8
|
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
((App)getApplication()).getRefWatcher().watch(new Object());
}
}
|
总结
可以看到使用 LeakCanary
还是非常简单的,比 MAT
、 Profile
这种手动的方式简单了许多。
内存泄漏检测的原理还是比较简单的,没了解之前以为很难,被自己吓到了。
参考