LeakCanary github地址:https://square.github.io/leakcanary/
開始使用
目前為止最新的版本是2.3版本,相比于2.0之前的版本,2.0之后的版本在使用上簡潔了很多,只需要在dependencies中加入LeakCanary的依賴即可。而且debugImplementation只在debug模式下有效,所以不用擔(dān)心用戶在正式環(huán)境下也會(huì)出現(xiàn)LeakCanary收集。
dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'
}
在項(xiàng)目中加入LeakCanary之后就可以開始檢測項(xiàng)目的內(nèi)存泄露了,把項(xiàng)目運(yùn)行起來之后, 開始隨便點(diǎn)自己的項(xiàng)目,下面以一個(gè)Demo項(xiàng)目為例,來聊一下LeakCanary記錄內(nèi)存泄露的過程以及我如何解決內(nèi)存泄露的。
項(xiàng)目運(yùn)行起來之后,在控制臺(tái)可以看到LeakCanary的打印信息:
D/LeakCanary: Check for retained object found no objects remaining
D/LeakCanary: Scheduling check for retained objects in 5000ms because app became invisible
D/LeakCanary: Check for retained object found no objects remaining
D/LeakCanary: Rescheduling check for retained objects in 2000ms because found only 3 retained objects (< 5 while app visible)
這說明LeakCanary正在不斷的檢測項(xiàng)目中是否有剩余對(duì)象。那么LeakCanary是如何工作的呢?LeakCanary的基礎(chǔ)是一個(gè)叫做ObjectWatcher Android的library。它hook了Android的生命周期,當(dāng)activity和fragment 被銷毀并且應(yīng)該被垃圾回收時(shí)候自動(dòng)檢測。這些被銷毀的對(duì)象被傳遞給ObjectWatcher, ObjectWatcher持有這些被銷毀對(duì)象的弱引用(weak references)。如果弱引用在等待5秒鐘并運(yùn)行垃圾收集器后仍未被清除,那么被觀察的對(duì)象就被認(rèn)為是保留的(retained,在生命周期結(jié)束后仍然保留),并存在潛在的泄漏。LeakCanary會(huì)在Logcat中輸出這些日志。
D LeakCanary: Watching instance of com.example.leakcanary.MainActivity
// 5 seconds later...
D LeakCanary: Found 1 retained object
在我一頓瞎點(diǎn)之后, 在手機(jī)通知欄上面出現(xiàn)了這樣的提示:

堆轉(zhuǎn)儲(chǔ)是什么一回事呢?
堆轉(zhuǎn)儲(chǔ)
在我點(diǎn)了上面的通知之后, 控制臺(tái)打印出了下面的語句:
D/LeakCanary: Check for retained objects found 3 objects, dumping the heap
D/LeakCanary: WRITE_EXTERNAL_STORAGE permission not granted, ignoring
I/testapplicatio: hprof: heap dump "/data/user/0/com.example.leakcaneraytestapplication/files/leakcanary/2020-05-28_16-35-28_155.hprof" starting...
I/testapplicatio: hprof: heap dump completed (22MB) in 2.963s objects 374548 objects with stack traces 0
這里開始進(jìn)行堆轉(zhuǎn)儲(chǔ),同時(shí)生成.hprof文件,LeakCanary將java heap的信息存到該文件中。同時(shí)在應(yīng)用程序中也會(huì)出現(xiàn)一個(gè)提示。
LeakCanary是使用shark來轉(zhuǎn)換.hprof文件并定位Java堆中保留的對(duì)象。如果找不到保留的對(duì)象,那么它們很可能在堆轉(zhuǎn)儲(chǔ)的過程中回收了。

解決內(nèi)存泄露
打開生成的Leaks應(yīng)用,界面就類似下面這樣?jì)饍旱?。LeakCanary會(huì)計(jì)算一個(gè)泄漏路徑并在UI上展示出來。這就是LeakCanary很友好的地方,通過UI展示,可以很直接的看到內(nèi)存泄漏的過程。相對(duì)于mat和android studio 自帶的profiler分析工具,這個(gè)簡直太直觀清晰了!
同時(shí)泄漏路徑也在logcat中展示了出來:
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS
References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.
111729 bytes retained by leaking objects
Signature: e030ebe81011d69c7a43074e799951b65ea73a
┬───
│ GC Root: Local variable in native code
│
├─ android.os.HandlerThread instance
│ Leaking: NO (PathClassLoader↓ is not leaking)
│ Thread name: 'LeakCanary-Heap-Dump'
│ ↓ HandlerThread.contextClassLoader
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (ToastUtil↓ is not leaking and A ClassLoader is never leaking)
│ ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (ToastUtil↓ is not leaking)
│ ↓ Object[].[871]
├─ com.example.leakcaneraytestapplication.ToastUtil class
│ Leaking: NO (a class is never leaking)
│ ↓ static ToastUtil.mToast
│ ~~~~~~
├─ android.widget.Toast instance
│ Leaking: YES (This toast is done showing (Toast.mTN.mWM != null && Toast.mTN.mView == null))
│ ↓ Toast.mContext
╰→ com.example.leakcaneraytestapplication.LeakActivity instance
? Leaking: YES (ObjectWatcher was watching this because com.example.leakcaneraytestapplication.LeakActivity received Activity#onDestroy() callback and Activity#mDestroyed is true)
? key = c1de58ad-30d8-444c-8a40-16a3813f3593
? watchDurationMillis = 40541
? retainedDurationMillis = 35535
====================================
0 LIBRARY LEAKS
路徑中的每一個(gè)節(jié)點(diǎn)都對(duì)應(yīng)著一個(gè)java對(duì)象。熟悉java內(nèi)存回收機(jī)制的同學(xué)都應(yīng)該知道”可達(dá)性分析算法“,LeakCanary就是用可達(dá)性分析算法,從GC ROOTS向下搜索,一直去找引用鏈,如果某一個(gè)對(duì)象跟GC Roots沒有任何引用鏈相連時(shí),就證明對(duì)象是”不可達(dá)“的,可以被回收。
我們從上往下看:
GC Root: Local variable in native code
在泄漏路徑的頂部是GC Root。GC Root是一些總是可達(dá)的特殊對(duì)象。
接著是:
├─ android.os.HandlerThread instance
│ Leaking: NO (PathClassLoader↓ is not leaking)
│ Thread name: 'LeakCanary-Heap-Dump'
│ ↓ HandlerThread.contextClassLoader
這里先看一下Leaking的狀態(tài)(YES、NO、UNKNOWN),NO表示沒泄露。那我們還得接著向下看。
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (ToastUtil↓ is not leaking and A ClassLoader is never leaking)
│ ↓ PathClassLoader.runtimeInternalObjects
上面的節(jié)點(diǎn)告訴我們Leaking的狀態(tài)還是NO,那再往下看。
├─ android.widget.Toast instance
│ Leaking: YES (This toast is done showing (Toast.mTN.mWM != null && Toast.mTN.mView == null))
│ ↓ Toast.mContext
中間Leaking是NO狀態(tài)的我就不再貼出來,我們看看Leaking是YES的這一條,這里說明發(fā)生了內(nèi)存泄露。
”This toast is done showing (Toast.mTN.mWM != null && Toast.mTN.mView == null)“,這里說明Toast發(fā)生了泄露,android.widget.Toast 這是系統(tǒng)的Toast控件,這說明我們在使用Toast的過程中極有可能創(chuàng)建了Toast對(duì)象,但是該回收它的時(shí)候無法回收它,導(dǎo)致出現(xiàn)了內(nèi)存泄露,這里我們再往下看:
╰→ com.example.leakcaneraytestapplication.LeakActivity instance
? Leaking: YES (ObjectWatcher was watching this because com.example.leakcaneraytestapplication.LeakActivity received Activity#onDestroy() callback and Activity#mDestroyed is true)
這里就很明顯的指出了內(nèi)存泄露是發(fā)生在了那個(gè)activity里面,我們根據(jù)上面的提示,找到對(duì)應(yīng)的activity,然后發(fā)現(xiàn)了一段跟Toast有關(guān)的代碼:
這里再進(jìn)入ToastUtil這個(gè)自定義Toast類里面,看看下面的代碼,有沒有發(fā)現(xiàn)什么問題?這里定義了一個(gè)static的Toast對(duì)象類型,然后在showToast的時(shí)候創(chuàng)建了對(duì)象,之后就沒有然后了。我們要知道static的生命周期是存在于整個(gè)應(yīng)用期間的,而一般Toast對(duì)象只需要顯示那么幾秒鐘就可以了,因?yàn)檫@里創(chuàng)建一個(gè)靜態(tài)的Toast,用完之后又沒有銷毀掉,所以這里提示有內(nèi)存泄露了。因此我們這里要么不用static修飾,要么在用完之后把Toast置為null。
public class ToastUtil {
private static Toast mToast;
public static void showToast(Context context, int resId) {
String text = context.getString(resId);
showToast(context, text);
}
public static void showToast(Context context, String text){
showToast(context, text, Gravity.BOTTOM);
}
public static void showToastCenter(Context context, String text){
showToast(context, text, Gravity.CENTER);
}
public static void showToast(Context context, String text, int gravity){
cancelToast();
if (context != null){
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View layout = inflater.inflate(R.layout.toast_layout, null);
((TextView) layout.findViewById(R.id.tv_toast_text)).setText(text);
mToast = new Toast(context);
mToast.setView(layout);
mToast.setGravity(gravity, 0, 20);
mToast.setDuration(Toast.LENGTH_LONG);
mToast.show();
}
}
public static void cancelToast() {
if (mToast != null){
mToast.cancel();
}
}
}
講了這么多,其實(shí)內(nèi)存泄露的本質(zhì)是長周期對(duì)象持有了短周期對(duì)象的引用,導(dǎo)致短周期對(duì)象該被回收的時(shí)候無法被回收,從而導(dǎo)致內(nèi)存泄露。我們只要順著LeakCaneray的給出的引用鏈一個(gè)個(gè)的往下找,找到發(fā)生內(nèi)存泄露的地方,切斷引用鏈就可以釋放內(nèi)存了。
這里再補(bǔ)充一點(diǎn)上面的這個(gè)例子里面Leaking沒有UNKNOWN的狀態(tài),一般情況下除了YES、NO還會(huì)出現(xiàn)UNKNOWN的狀態(tài),UNKNOWN表示這里可能出現(xiàn)了內(nèi)存泄露,這些引用你需要花時(shí)間來調(diào)查一下,看看是哪里出了問題。一般推斷內(nèi)存泄露是從最后一個(gè)沒有泄漏的節(jié)點(diǎn)(Leaking: NO )到第一個(gè)泄漏的節(jié)點(diǎn)(Leaking: YES)之間的引用。