美文网首页Android开发经验谈Android开发Android技术知识
记一次Activity的内存泄漏和分析过程

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

作者: o动感超人o | 来源:发表于2018-07-07 14:08 被阅读314次

    发现这个问题的原由是测试提出的一个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来用,这个故事告诉我们用第三方框架要谨慎啊!!!

    相关文章

      网友评论

      • leobert:用三方框架,不把代码梳理一遍就是埋雷。看一下DialogMaintainer就会发现这地方的隐患:smile: ,类似集中管理的功能上来就要看看是否使用了弱引用以及空指针问题的。:smile: :smile: :smile:
        o动感超人o:@leobert 是的,这些静态集合要小心使用,尤其是保存了Context的这种集合
      • 楊帥:不错,解决了我的问题

      本文标题:记一次Activity的内存泄漏和分析过程

      本文链接:https://www.haomeiwen.com/subject/wezvuftx.html