記一次Activity的內(nèi)存泄漏和分析過(guò)程

本篇文章已授權(quán)微信公眾號(hào)guolin_blog(郭霖)獨(dú)家發(fā)布

我的掘金


Android Profiler的使用可以看這里:https://blog.csdn.net/Double2hao/article/details/78784758

發(fā)現(xiàn)這個(gè)問題的原由是測(cè)試提出的一個(gè)bug,是某個(gè)地圖頁(yè)面多次操作以后會(huì)出現(xiàn)卡頓甚至?xí)嗀NR,很明顯肯定是內(nèi)存的問題,我就用Android Profiler查看了一下內(nèi)存,發(fā)現(xiàn)出現(xiàn)某個(gè)圖層操作的時(shí)候短時(shí)間內(nèi)會(huì)很頻繁的觸發(fā)GC操作,然后無(wú)意中發(fā)現(xiàn)退出這個(gè)地圖頁(yè)面的時(shí)候LeakCanary會(huì)說(shuō)此頁(yè)面泄露了10M+的內(nèi)存,雖然這個(gè)LeakCanary不是每次都很準(zhǔn)確,不過(guò)它報(bào)了就得去查看一下,然后在Android Profiler里發(fā)現(xiàn)此頁(yè)面(以后該頁(yè)面稱為MapActivity)在退出后仍然占用了大量?jī)?nèi)存,頻繁觸發(fā)GC先不管,先把這個(gè)問題解決掉,圖片如下:


image.png

這個(gè)MapActivity已經(jīng)退出了,看來(lái)是有實(shí)例在引用著它導(dǎo)致沒辦法釋放內(nèi)存,然后看一下都有誰(shuí)引用了此類


image.png

機(jī)智的我一眼就看到這個(gè)ConfigBean,這是個(gè)啥玩意?看來(lái)問題應(yīng)該出在了這里,然后我就搜索了一下這個(gè)類,發(fā)現(xiàn)是第三方Dialog庫(kù)com.hss01248.dialog里的,而context是此類里的一個(gè)屬性
/**
 * Created by Administrator on 2016/10/9 0009.
 */
public class ConfigBean extends MyDialogBuilder implements Styleable {

    public int type;

    public Context context;
    //省略其他代碼
}

好了,再查一下context是在哪里設(shè)置的,不過(guò)這個(gè)字段最好用弱引用WeakReference去包一下,而且是public的我覺得不太好吧。。。不過(guò)作者可能有他的考慮。。。如果是我的話我會(huì)用WeakReference去包一下,不然太容易內(nèi)存泄漏了,然后我找到了context設(shè)置的地方

    public ConfigBean setActivity(Activity activity) {
        this.context = activity;
        return this;
    }

在MapActivity類里我是這么調(diào)用的

    override fun showLoading() {
        StyledDialog.buildLoading()
                .setCancelable(false, false)
                .setActivity(this)
                .show()
    }

所以這個(gè)context是MapActivity,而內(nèi)存泄露的也是這個(gè)MapActivity,然后我們點(diǎn)擊前面的箭頭展開context,看誰(shuí)引用了ConfigBean


image.png

不知道為什么,其中ConfigBean$3這個(gè)類我并沒有找到,但是Tool的3個(gè)匿名類我在Tool的字節(jié)碼文件里找到了

  public static void setListener(android.app.Dialog, com.hss01248.dialog.config.ConfigBean);
    Code:
       0: aload_0
       1: ifnonnull     5
       4: return
       5: aload_0
       6: new           #27                 // class com/hss01248/dialog/Tool$2
       9: dup
      10: aload_1
      11: aload_0
      12: invokespecial #28                 // Method com/hss01248/dialog/Tool$2."<init>":(Lcom/hss01248/dialog/config/ConfigBean;Landroid/app/Dialog;)V
      15: invokevirtual #29                 // Method android/app/Dialog.setOnShowListener:(Landroid/content/DialogInterface$OnShowListener;)V
      18: aload_0
      19: new           #30                 // class com/hss01248/dialog/Tool$3
      22: dup
      23: aload_1
      24: invokespecial #31                 // Method com/hss01248/dialog/Tool$3."<init>":(Lcom/hss01248/dialog/config/ConfigBean;)V
      27: invokevirtual #32                 // Method android/app/Dialog.setOnCancelListener:(Landroid/content/DialogInterface$OnCancelListener;)V
      30: aload_0
      31: new           #33                 // class com/hss01248/dialog/Tool$4
      34: dup
      35: aload_1
      36: aload_0
      37: invokespecial #34                 // Method com/hss01248/dialog/Tool$4."<init>":(Lcom/hss01248/dialog/config/ConfigBean;Landroid/app/Dialog;)V
      40: invokevirtual #35                 // Method android/app/Dialog.setOnDismissListener:(Landroid/content/DialogInterface$OnDismissListener;)V
      43: return

可以看到,是Tool類的setListener方法里的代碼,然后我們看源碼里的這個(gè)方法

    public static void setListener(final Dialog dialog, final ConfigBean bean) {
        if(dialog ==null){
            return;
        }

        dialog.setOnShowListener(new DialogInterface.OnShowListener() {
            @Override
            public void onShow(DialogInterface dialog0) {
                if (bean.alertDialog!= null){
                    setMdBtnStytle(bean);
                    setTitleMessageStyle(bean.alertDialog,bean);
                }
                bean.listener.onShow();
                DialogsMaintainer.addWhenShow(bean.context,dialog);
                if (bean.type == DefaultConfig.TYPE_IOS_LOADING || bean.type == DefaultConfig.TYPE_MD_LOADING) {
                    DialogsMaintainer.addLoadingDialog(bean.context,dialog);
                }

                 /*dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
                     WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
                Tool.showSoftKeyBoardDelayed(bean.needSoftKeyboard,bean.viewHolder);
                Tool.showSoftKeyBoardDelayed(bean.needSoftKeyboard,bean.customContentHolder);*/
            }
        });

        dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
            @Override
            public void onCancel(DialogInterface dialog0) {
                if(bean.type == DefaultConfig.TYPE_IOS_INPUT){
                    IosAlertDialogHolder iosAlertDialogHolder = (IosAlertDialogHolder) bean.viewHolder;
                    if(iosAlertDialogHolder!=null){
                        iosAlertDialogHolder.hideKeyBoard();
                    }
                }
                if(bean.listener!=null) {
                    bean.listener.onCancle();
                }
                /*DialogsMaintainer.removeWhenDismiss(dialog);
                if (bean.type == DefaultConfig.TYPE_IOS_LOADING || bean.type == DefaultConfig.TYPE_MD_LOADING) {
                    DialogsMaintainer.dismissLoading(dialog);

                }*/
            }
        });

        dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialog0) {
//                bean.context = null;
                if(bean.listener !=null){
                    bean.listener.onDismiss();
                }
                DialogsMaintainer.removeWhenDismiss(dialog);
                if (bean.type == DefaultConfig.TYPE_IOS_LOADING || bean.type == DefaultConfig.TYPE_MD_LOADING) {
                    DialogsMaintainer.dismissLoading(dialog);

                }
            }
        });
    }

可以看到,這3個(gè)類正是dialog設(shè)置的3個(gè)監(jiān)聽,是3個(gè)匿名類,而這3個(gè)匿名類都引用了外部的Dialog和ConfigBean,所以這3個(gè)匿名類持有參數(shù)傳遞過(guò)來(lái)的Dialog和ConfigBean兩個(gè)實(shí)例的強(qiáng)引用,我們先看其中的一個(gè)方法dialog.setOnShowListener的源碼

    /**
     * Sets a listener to be invoked when the dialog is shown.
     * @param listener The {@link DialogInterface.OnShowListener} to use.
     */
    public void setOnShowListener(@Nullable OnShowListener listener) {
        if (listener != null) {
            mShowMessage = mListenersHandler.obtainMessage(SHOW, listener);
        } else {
            mShowMessage = null;
        }
    }

而這個(gè)mShowMessage也對(duì)應(yīng)了我們上圖中的mShowMessage引用,我再發(fā)一次


image.png

其他兩個(gè)監(jiān)聽的設(shè)置也是一樣的,分別將3個(gè)匿名類作為Message的obj屬性存到了Message里,現(xiàn)在的情況是Message持有匿名類的實(shí)例,而匿名類持有Dialog和ConfigBean的實(shí)例

然后在我們隱藏Dialog的時(shí)候,調(diào)用了這個(gè)第三方庫(kù)的此方法

    override fun hideLoading() {
        StyledDialog.dismissLoading(this)
    }

然后我們看一下這個(gè)方法的源碼

    /**
     * 一鍵讓loading消失.
     */
    public static void dismissLoading(Activity activity) {
        DialogsMaintainer.dismissLoading(activity);
    }

然后StyledDialog類的這個(gè)方法又調(diào)用了DialogsMaintainer.dismissLoading(activity);我們繼續(xù)查看DialogsMaintainer類的此方法

    ...
    private static HashMap<Activity, Set<Dialog>> dialogsOfActivity = new HashMap<>();
    private static HashMap<Activity, Set<Dialog>> loadingDialogs = new HashMap<>();
    ...
    public static void dismissLoading(Activity activity) {

        if (activity == null) {
            return;
        }
        if (!loadingDialogs.containsKey(activity)) {
            return;
        }
        Set<Dialog> dialogSet = loadingDialogs.get(activity);
        for (Dialog dialog : dialogSet) {
            dialog.dismiss();
            //在callback內(nèi)部自動(dòng)會(huì)去移除在dialogsOfActivity的引用
        }
        loadingDialogs.remove(activity);

    }

我覺得dialogsOfActivityloadingDialogs這兩個(gè)Map也是用弱引用比較好

這個(gè)方法我們會(huì)找到該Activity里所有的Dialog然后調(diào)用dialog的dismiss()方法
而Dialog的dismiss方法做了什么呢,看代碼

    /**
     * Dismiss this dialog, removing it from the screen. This method can be
     * invoked safely from any thread.  Note that you should not override this
     * method to do cleanup when the dialog is dismissed, instead implement
     * that in {@link #onStop}.
     */
    @Override
    public void dismiss() {
        if (Looper.myLooper() == mHandler.getLooper()) {
            dismissDialog();
        } else {
            mHandler.post(mDismissAction);
        }
    }

當(dāng)不在創(chuàng)建Dialog的線程的時(shí)候,會(huì)調(diào)用Dialog線程的mHandler發(fā)送mDismissAction這個(gè)Runnable,否則就直接在創(chuàng)建Dialog的線程執(zhí)行dismissDialog()方法,mDismissAction這個(gè)Runnable的run方法會(huì)執(zhí)行dismissDialog()方法(這個(gè)Runnable只是執(zhí)行run方法,它并沒有新起一個(gè)線程去start),然后看dismissDialog()方法

    void dismissDialog() {
        if (mDecor == null || !mShowing) {
            return;
        }

        if (mWindow.isDestroyed()) {
            Log.e(TAG, "Tried to dismissDialog() but the Dialog's window was already destroyed!");
            return;
        }

        try {
            mWindowManager.removeViewImmediate(mDecor);
        } finally {
            if (mActionMode != null) {
                mActionMode.finish();
            }
            mDecor = null;
            mWindow.closeAllPanels();
            onStop();
            mShowing = false;

            sendDismissMessage();
        }
    }

繼續(xù)看sendDismissMessage()方法

    private void sendDismissMessage() {
        if (mDismissMessage != null) {
            // Obtain a new message so this dialog can be re-used
            Message.obtain(mDismissMessage).sendToTarget();
        }
    }

可以看到之前將匿名類設(shè)置給自己obj屬性的Message將自己發(fā)送到了它的targerHandler所在Looper中的MessageQueue中
到現(xiàn)在了我們根據(jù)下圖總結(jié)一下


image.png

這里有個(gè)套路,每行所在的類被下一行in前面指針?biāo)茫韵聢D就是:MapActivity被ConfigBean的context屬性持有,ConfigBean作為參數(shù)bean被Tool$2、Tool$3、Tool$4這三個(gè)匿名類持有(val$bean代表方法參數(shù)bean),這三個(gè)匿名類又被Message的obj屬性所持有,下面以此類推,不過(guò)下面就看不太清楚邏輯了,這時(shí)候我們需要Eclipse Memory Analyzer,也就是平時(shí)所說(shuō)的MAT軟件,下載過(guò)程不贅述,假設(shè)現(xiàn)在讀者下載好了MAT,然后用Android Studio點(diǎn)擊下圖中紅框里的按鈕導(dǎo)出剛才我們分析的東東


image.png

導(dǎo)出文件假如命名為leak.hprof,然后打開終端用hprof-conv leak.hprof leak_mat.hprof生成可以給MAT分析的hprof文件,打開后我選擇第一項(xiàng)
image.png

我剛發(fā)現(xiàn)直接點(diǎn)Cancel也可以打開文件并分析。。。

打開后點(diǎn)擊如圖所示的按鈕


image.png

然后在向右的三個(gè)箭頭那里輸入我們泄露的MapActivity


image.png

然后右鍵選擇空白的那個(gè)圖標(biāo)


image.png

選擇Path to GC Roots和下面的沒什么區(qū)別,這里我們選擇Merge Shortest Paths to GC Roots,然后排除不需要關(guān)心的弱引用軟引用之類的東東
然后結(jié)果出來(lái)了
image.png

看到這里,其實(shí)我們應(yīng)該明白,Tool$4這個(gè)匿名類持有ConfigBean,而ConfigBean持有的context是我們的MapActivity,這個(gè)Tool$4匿名類是這樣的

dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialog0) {
                if(bean.listener !=null){
                    bean.listener.onDismiss();
                }
                DialogsMaintainer.removeWhenDismiss(dialog);
                if (bean.type == DefaultConfig.TYPE_IOS_LOADING || bean.type == DefaultConfig.TYPE_MD_LOADING) {
                    DialogsMaintainer.dismissLoading(dialog);

                }
            }
        });

方法參數(shù)new DialogInterface.OnDismissListener(){。。。}就是Tool$4,這個(gè)Tool$4被設(shè)置為了dialog的mDismissMessage類屬性的obj屬性,這個(gè)Message會(huì)在dialog執(zhí)行dismiss的時(shí)候發(fā)送到Looper所持有的MessageQueue中,在Looper的loop方法中取到這個(gè)Message消費(fèi)完以后會(huì)調(diào)用Message.recycleUnchecked方法去回收它所占用的內(nèi)存,而這時(shí)候肯定因?yàn)槟撤N原因無(wú)法釋放,然后可以思考一下,這個(gè)Message是Dialog中的一個(gè)類屬性,然后可以聯(lián)想到這個(gè)Dialog因?yàn)槟撤N原因被某類持有,然后查詢一下這個(gè)第三方庫(kù)會(huì)發(fā)現(xiàn)在DialogsMaintainer類中有2個(gè)靜態(tài)集合,而在調(diào)用DialogsMaintainer.dismissLoading之后的流程中并沒有把Dialog移除,好了這次內(nèi)存泄漏分析之旅到此結(jié)束。

提示:如果把ConfigBean中的context設(shè)置為弱引用,那么需要把
DialogsMaintainer中的兩個(gè)用到Activity的靜態(tài)map的key也變?yōu)槿跻茫驗(yàn)檫@兩個(gè)靜態(tài)map的key和ConfigBean中的context類屬性是同一個(gè)值,弱引用的特性是當(dāng)一個(gè)對(duì)象僅僅被weak reference指向,而沒有任何其他strong reference指向的時(shí)候,如果GC運(yùn)行,那么這個(gè)對(duì)象就會(huì)被回收。所以如果只把ConfigBean中的context類屬性改為弱引用,其他地方仍然有這個(gè)指針的強(qiáng)引用那么這樣的改動(dòng)沒有任何效果。而在這個(gè)框架里如果要修復(fù)這個(gè)bug,應(yīng)該像上面說(shuō)明的那樣改動(dòng)。

其實(shí)我們公司的項(xiàng)目只是用到它顯示了一個(gè)Dialog,后期我要去掉這個(gè)框架自己做一個(gè)Dialog來(lái)用,這個(gè)故事告訴我們用第三方框架要謹(jǐn)慎?。。?!

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

相關(guān)閱讀更多精彩內(nèi)容

  • Android 內(nèi)存管理的目的 內(nèi)存管理的目的就是讓我們?cè)陂_發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。簡(jiǎn)單粗...
    晨光光閱讀 1,374評(píng)論 1 4
  • Android 內(nèi)存泄漏總結(jié) 內(nèi)存管理的目的就是讓我們?cè)陂_發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏...
    _痞子閱讀 1,703評(píng)論 0 8
  • 內(nèi)存管理的目的就是讓我們?cè)陂_發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏大家都不陌生了,簡(jiǎn)單粗俗的講,...
    宇宙只有巴掌大閱讀 2,492評(píng)論 0 12
  • Android 內(nèi)存泄漏總結(jié) 內(nèi)存管理的目的就是讓我們?cè)陂_發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏...
    apkcore閱讀 1,310評(píng)論 2 7
  • 蕭瀟從來(lái)不知道廣州的臺(tái)風(fēng)會(huì)有如此大的力量,狂風(fēng)夾著暴雨狠狠拍打著這個(gè)城市的一切,吹得她幾乎快要飛起來(lái)。 蕭瀟彎著身...
    茶丁故事閱讀 782評(píng)論 0 3

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