Android內(nèi)存泄露檢測之LeakCanary的使用

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)了這樣的提示:

image.png
上面的意思是已經(jīng)發(fā)現(xiàn)了4個(gè)保留的對(duì)象,點(diǎn)擊通知可以觸發(fā)堆轉(zhuǎn)儲(chǔ)(dump heap)。在app可見的時(shí)候,會(huì)一直等到5個(gè)保留的對(duì)象才會(huì)觸發(fā)堆轉(zhuǎn)儲(chǔ)。這里要補(bǔ)充的一點(diǎn)是:當(dāng)應(yīng)用可見的時(shí)候默認(rèn)的閾值是5,應(yīng)用不可見的時(shí)候閾值是1。如果你看到了保留的對(duì)象的通知然后將應(yīng)用切換到后臺(tái)(例如點(diǎn)擊home鍵),那么閾值就會(huì)從5變到1,LeakCanary會(huì)立即進(jìn)行堆轉(zhuǎn)儲(chǔ)。那么堆轉(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è)提示。
image.png

LeakCanary是使用shark來轉(zhuǎn)換.hprof文件并定位Java堆中保留的對(duì)象。如果找不到保留的對(duì)象,那么它們很可能在堆轉(zhuǎn)儲(chǔ)的過程中回收了。

image.png
對(duì)于每個(gè)被保留的對(duì)象,LeakCanary會(huì)找出阻止該保留對(duì)象被回收的引用鏈:泄漏路徑。泄露路徑就是從GC ROOTS到保留對(duì)象的最短的強(qiáng)引用路徑的別名。確定泄漏路徑以后,LeakCanary使用它對(duì)Android框架的了解來找出在泄漏路徑上是誰泄漏了。

解決內(nèi)存泄露

打開生成的Leaks應(yīng)用,界面就類似下面這樣?jì)饍旱?。LeakCanary會(huì)計(jì)算一個(gè)泄漏路徑并在UI上展示出來。這就是LeakCanary很友好的地方,通過UI展示,可以很直接的看到內(nèi)存泄漏的過程。相對(duì)于mat和android studio 自帶的profiler分析工具,這個(gè)簡直太直觀清晰了!
image.png

同時(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)的代碼:
image.png

這里再進(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)之間的引用。

參考文章鏈接:http://m.itdecent.cn/p/bcaab8f0f280

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容