Android开发经验谈Android开发Android技术知识

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

2018-07-07  本文已影响314人  o动感超人o

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

上一篇下一篇

猜你喜欢

热点阅读