记一次Activity的内存泄漏和分析过程

本篇文章已授权微信公众号guolin_blog(郭霖)独家发布

我的掘金


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

发现这个问题的原由是测试提出的一个bug,是某个地图页面多次操作以后会出现卡顿甚至会ANR,很明显肯定是内存的问题,我就用Android Profiler查看了一下内存,发现出现某个图层操作的时候短时间内会很频繁的触发GC操作,然后无意中发现退出这个地图页面的时候LeakCanary会说此页面泄露了10M+的内存,虽然这个LeakCanary不是每次都很准确,不过它报了就得去查看一下,然后在Android Profiler里发现此页面(以后该页面称为MapActivity)在退出后仍然占用了大量内存,频繁触发GC先不管,先把这个问题解决掉,图片如下:


image.png

这个MapActivity已经退出了,看来是有实例在引用着它导致没办法释放内存,然后看一下都有谁引用了此类


image.png

机智的我一眼就看到这个ConfigBean,这是个啥玩意?看来问题应该出在了这里,然后我就搜索了一下这个类,发现是第三方Dialog库com.hss01248.dialog里的,而context是此类里的一个属性
/**
 * Created by Administrator on 2016/10/9 0009.
 */
public class ConfigBean extends MyDialogBuilder implements Styleable {

    public int type;

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

好了,再查一下context是在哪里设置的,不过这个字段最好用弱引用WeakReference去包一下,而且是public的我觉得不太好吧。。。不过作者可能有他的考虑。。。如果是我的话我会用WeakReference去包一下,不然太容易内存泄漏了,然后我找到了context设置的地方

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

在MapActivity类里我是这么调用的

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

所以这个context是MapActivity,而内存泄露的也是这个MapActivity,然后我们点击前面的箭头展开context,看谁引用了ConfigBean


image.png

不知道为什么,其中ConfigBean$3这个类我并没有找到,但是Tool的3个匿名类我在Tool的字节码文件里找到了

  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方法里的代码,然后我们看源码里的这个方法

    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个类正是dialog设置的3个监听,是3个匿名类,而这3个匿名类都引用了外部的Dialog和ConfigBean,所以这3个匿名类持有参数传递过来的Dialog和ConfigBean两个实例的强引用,我们先看其中的一个方法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;
        }
    }

而这个mShowMessage也对应了我们上图中的mShowMessage引用,我再发一次


image.png

其他两个监听的设置也是一样的,分别将3个匿名类作为Message的obj属性存到了Message里,现在的情况是Message持有匿名类的实例,而匿名类持有Dialog和ConfigBean的实例

然后在我们隐藏Dialog的时候,调用了这个第三方库的此方法

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

然后我们看一下这个方法的源码

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

然后StyledDialog类的这个方法又调用了DialogsMaintainer.dismissLoading(activity);我们继续查看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内部自动会去移除在dialogsOfActivity的引用
        }
        loadingDialogs.remove(activity);

    }

我觉得dialogsOfActivityloadingDialogs这两个Map也是用弱引用比较好

这个方法我们会找到该Activity里所有的Dialog然后调用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);
        }
    }

当不在创建Dialog的线程的时候,会调用Dialog线程的mHandler发送mDismissAction这个Runnable,否则就直接在创建Dialog的线程执行dismissDialog()方法,mDismissAction这个Runnable的run方法会执行dismissDialog()方法(这个Runnable只是执行run方法,它并没有新起一个线程去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();
        }
    }

继续看sendDismissMessage()方法

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

可以看到之前将匿名类设置给自己obj属性的Message将自己发送到了它的targerHandler所在Looper中的MessageQueue中
到现在了我们根据下图总结一下


image.png

这里有个套路,每行所在的类被下一行in前面指针所引用,所以下图就是:MapActivity被ConfigBean的context属性持有,ConfigBean作为参数bean被Tool$2、Tool$3、Tool$4这三个匿名类持有(val$bean代表方法参数bean),这三个匿名类又被Message的obj属性所持有,下面以此类推,不过下面就看不太清楚逻辑了,这时候我们需要Eclipse Memory Analyzer,也就是平时所说的MAT软件,下载过程不赘述,假设现在读者下载好了MAT,然后用Android Studio点击下图中红框里的按钮导出刚才我们分析的东东


image.png

导出文件假如命名为leak.hprof,然后打开终端用hprof-conv leak.hprof leak_mat.hprof生成可以给MAT分析的hprof文件,打开后我选择第一项
image.png

我刚发现直接点Cancel也可以打开文件并分析。。。

打开后点击如图所示的按钮


image.png

然后在向右的三个箭头那里输入我们泄露的MapActivity


image.png

然后右键选择空白的那个图标


image.png

选择Path to GC Roots和下面的没什么区别,这里我们选择Merge Shortest Paths to GC Roots,然后排除不需要关心的弱引用软引用之类的东东
然后结果出来了
image.png

看到这里,其实我们应该明白,Tool$4这个匿名类持有ConfigBean,而ConfigBean持有的context是我们的MapActivity,这个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);

                }
            }
        });

方法参数new DialogInterface.OnDismissListener(){。。。}就是Tool$4,这个Tool$4被设置为了dialog的mDismissMessage类属性的obj属性,这个Message会在dialog执行dismiss的时候发送到Looper所持有的MessageQueue中,在Looper的loop方法中取到这个Message消费完以后会调用Message.recycleUnchecked方法去回收它所占用的内存,而这时候肯定因为某种原因无法释放,然后可以思考一下,这个Message是Dialog中的一个类属性,然后可以联想到这个Dialog因为某种原因被某类持有,然后查询一下这个第三方库会发现在DialogsMaintainer类中有2个静态集合,而在调用DialogsMaintainer.dismissLoading之后的流程中并没有把Dialog移除,好了这次内存泄漏分析之旅到此结束。

提示:如果把ConfigBean中的context设置为弱引用,那么需要把
DialogsMaintainer中的两个用到Activity的静态map的key也变为弱引用,因为这两个静态map的key和ConfigBean中的context类属性是同一个值,弱引用的特性是当一个对象仅仅被weak reference指向,而没有任何其他strong reference指向的时候,如果GC运行,那么这个对象就会被回收。所以如果只把ConfigBean中的context类属性改为弱引用,其他地方仍然有这个指针的强引用那么这样的改动没有任何效果。而在这个框架里如果要修复这个bug,应该像上面说明的那样改动。

其实我们公司的项目只是用到它显示了一个Dialog,后期我要去掉这个框架自己做一个Dialog来用,这个故事告诉我们用第三方框架要谨慎啊!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容

  • Android 内存管理的目的 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。简单粗...
    晨光光阅读 1,290评论 1 4
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    _痞子阅读 1,626评论 0 8
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    宇宙只有巴掌大阅读 2,361评论 0 12
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    apkcore阅读 1,219评论 2 7
  • 萧潇从来不知道广州的台风会有如此大的力量,狂风夹着暴雨狠狠拍打着这个城市的一切,吹得她几乎快要飞起来。 萧潇弯着身...
    茶丁故事阅读 557评论 0 3