DialogFragment引起的内存泄露

一、简介

DialogFragment是Android3.0之后引入的一种特殊的Fragment,官方建议使用DialogFragment代替Dialog或者AllertDialog来实现弹框的功能,因为它可以更好的管理Dialog的生命周期以及可以更好复用。

二、使用中遇到内存泄露

在使用过程中,由于业务需要对DialogFragment的dismiss事件进行了监听,在DialogFragment展示与消失的时候,经常会出现LeakCanary检测的内存泄露问题。查看LeakCanary的内存泄露引用链如下图所示:

这里只贴出了一张图,LeakCanary每次报出来的引用链并不完全相同,图上中显示的是RxJava的ThreadHandler,有的则显示的是高德地图的ThreadHandler(amapLocManagerThread),由此猜测这里并不是主线程的ThreadHandler引起的内存泄露,而是第三方库中的ThreadHandler引起的内存泄露。

但总的来说都是HandlerThread中处理的Message引用了NormalTitleBgDialog(DialogFragment)不能被释放。下面具体分析一下出现这个问题的原因。

三、原因分析

那么Message是怎么引用到DialogFragment的呢?在DialogFragment中搜索一下Message一无所获。DialogFragment实际是Dialog的封装,在Dialog中搜索Message试试,果然发现Dialog的Cancle和Dismiss都是通过Handler进行操作的,从这里入手分析一下内存泄露的原因:

  • DialogFragment中的onActivityCreated

Cancle和Dismiss的监听传入的是DialogFragment实现的两个接口:DialogInterface.OnCancelListener, DialogInterface.OnDismissListener

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    mDialog.setOnCancelListener(this);
    mDialog.setOnDismissListener(this);
}
  • setOnDismissListener

这里会通过mListenerHandler获取到一个mDismissMessage对象。

public void setOnDismissListener(@Nullable OnDismissListener listener) {
        if (mCancelAndDismissTaken != null) {
            throw new IllegalStateException(
                    "OnDismissListener is already taken by "
                    + mCancelAndDismissTaken + " and can not be replaced.");
        }
        if (listener != null) {
            mDismissMessage = mListenersHandler.obtainMessage(DISMISS, listener);
        } else {
            mDismissMessage = null;
        }
    }

   public void setOnCancelListener(@Nullable OnCancelListener listener) {
        if (mCancelAndDismissTaken != null) {
            throw new IllegalStateException(
                    "OnCancelListener is already taken by "
                    + mCancelAndDismissTaken + " and can not be replaced.");
        }
        if (listener != null) {
            mCancelMessage = mListenersHandler.obtainMessage(CANCEL, listener);
        } else {
            mCancelMessage = null;
        }
    }

其中obtainMessage内部是通过Message.obtain方法得到,而这个方法会从消息池中通过复用的方式拿到Message。

public static Message obtain(Handler h, int what, Object obj) {
        Message m = obtain();
        m.target = h;
        m.what = what;
        m.obj = obj;

        return m;
    }

 public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }

至此,mDismissMessage中的obj属性引用了DialogFragment。但是Message是怎么被ThreadHandler引用到并且不能被释放的呢?下面看一下消息循环的处理过程是怎么样的。

3.Looper.loop

Looper.loop()方法在HandlerThread中运行。Looper.loop()方法执行过程就是消息的处理过程,首先从MessageQueue中取出一条消息,然后调用msg.target.dispatchMessage(msg)分发处理消息,最后调用msg.recycleUnchecked()回收消息。当MessageQueue中没有消息时queue.next()方法会阻塞线程。

public static void loop() {
        final Looper me = myLooper();
        final MessageQueue queue = me.mQueue;
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
           msg.target.dispatchMessage(msg);//分发消息
            msg.recycleUnchecked();//回收消息
        }
    }

 void recycleUnchecked() {
        // Mark the message as in use while it remains in the recycled object pool.
        // Clear out all other details.
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = UID_NONE;
        workSourceUid = UID_NONE;
        when = 0;
        target = null;
        callback = null;
        data = null;

        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }

一些第三方库会创建自己的消息循环(HandlerThread),当这些消息循环(HandlerThread)中没有消息时,消息循环线程就会阻塞。从Java的内存模型我们知道线程开启时会创建自己独有的虚拟机栈空间,当消息循环发生阻塞时,方法中的局部变量不能被释放。

MessageQueue中取出的最后一条消息Message是Looper.loop()方法的局部变量,存储在栈帧的局部变量表中,由于当前线程被阻塞而不能被释放。以上我们知道了第三方库的HandlerThread会引用Message不能释放,但是第三方库的HandlerThread中的Message怎么会引用到DialogFragment呢?

由于内存泄露发生是在DialogFragment关闭时,我们看一下DialogFragment的dismiss是怎么处理的。

  1. dismiss()

Dialog关闭时也是通过发送消息来实现的,这里通过Message.obtain复制了一份mDismissMessage,同样是从消息池中复用的消息,因此这里是有可能取到已经回收的消息的。

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

当刚Dialog关闭dismiss时,刚好取出的是已经回收的消息,并且这条消息被另一个线程所引用,此时的mDimissMessage重新引用了DialogFragment,因此不能被回收,造成内存泄露。

总结:下图更容易说明造成内存泄露的原因。左图是线程A中的消息循环,线程A持有被回收到消息池中的消息对象,右边是主线程消息循环,Dialog关闭时从消息池中复用的的mDismissMessage被线程A持有,而mDismissMessage又持有DialogFragment,因为造成了内存泄露。

四、解决方案

  • LeakCanary提供了一种解决方案:建议第三方库一直发送空的消息,保持第三方库的消息循环消息队列一直不为空。这种方式只能是提前知道哪个第三方库创建了自己的消息循环,才能向这个消息循环中发送空消息,这并不能覆盖到所有的第三方创建的消息循环。而且,不断的向一个阻塞线程中发消息,线程时刻处于运行状态,占用线程空间资源。因此,此方案对于客户端开发来说并不可行。

  • 不监听Dialog的dimiss和cancle:如果没有需求要监听这两个方法则可以直接继承Dialog,放弃使用DialogFragment。因为DialogFragment在onActivityCreate方法中会注册dismiss和cancle的监听。网络上有种方案是通过重写DialogFragment在onActivityCreate方法中设置dialog.setOnCancelListener(null)dialog.setOnDismissListener(null)。如下所示,这种方案仍然会出现内存泄露,原因是在super.onActivityCreate()方法中仍然有 mDialog.setOnCancelListener(this)mDialog.setOnDismissListener(this)。此时获取的mDismissMessage有可能是消息池中的消息,而这条消息刚好被一个消息循环所持有不能释放。

override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        dialog.setOnCancelListener(null)
        dialog.setOnDismissListener(null)
    }

  public void setOnDismissListener(@Nullable OnDismissListener listener) {
        if (mCancelAndDismissTaken != null) {
            throw new IllegalStateException(
                    "OnDismissListener is already taken by "
                    + mCancelAndDismissTaken + " and can not be replaced.");
        }
        if (listener != null) {
            mDismissMessage = mListenersHandler.obtainMessage(DISMISS, listener);
        } else {
            mDismissMessage = null;
        }
    }
  • 弱引用的方式:mDismissMessage实际上引用的是DialogInterface.OnDismissListener,如果把这个引用改成弱引用,当系统gc时就能够回收掉DialogFragment了。这里需要注意的是不能直接继承DialogFragment,因为如果继承的是DialogFragment,当重写onActivityCreate方法时加上 super.onActivityCreated(savedInstanceState)还会出现内存泄露,如果不加这句话则会报错。下面分步说明实现方法:

  • 重写DialogFragment

直接拷贝DialogFragment代码至LeakDialogFragment类中,放弃实现DialogInterface.OnCancelListener, DialogInterface.OnDismissListener两个接口

public class LeakDialogFragment extends Fragment {

}

   public void onCancelDialog(DialogInterface dialog) {

    }

   public void onDismissDialog(DialogInterface dialog) {
        if (!mViewDestroyed) {
            // Note: we need to use allowStateLoss, because the dialog
            // dispatches this asynchronously so we can receive the call
            // after the activity is paused.  Worst case, when the user comes
            // back to the activity they see the dialog again.
            dismissInternal(true);
        }
    }
@Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
    }

  public void onDestroyView() {
        super.onDestroyView();
    }
  • 自定义DialogInterface.OnDismissListener和DialogInterface.OnDismissListener
public static class DialogDismissListener implements DialogInterface.OnDismissListener {
        private WeakReference<LeakDialogFragment> leakDialogFragmentWeakReference;

        public DialogDismissListener(LeakDialogFragment leakDialogFragment) {
            this.leakDialogFragmentWeakReference = new WeakReference<>(leakDialogFragment);
        }

        @Override
        public void onDismiss(DialogInterface dialog) {
            LeakDialogFragment leakDialogFragment = leakDialogFragmentWeakReference.get();
            if(leakDialogFragment!=null){
                leakDialogFragment.onDismissDialog(dialog);
            }
        }
    }

 public static class DialogCancleListener implements DialogInterface.OnCancelListener {
        private WeakReference<LeakDialogFragment> leakDialogFragmentWeakReference;

        public DialogCancleListener(LeakDialogFragment leakDialogFragment) {
            this.leakDialogFragmentWeakReference = new WeakReference<>(leakDialogFragment);
        }

        @Override
        public void onCancel(DialogInterface dialog) {
            LeakDialogFragment leakDialogFragment = leakDialogFragmentWeakReference.get();
            if(leakDialogFragment!=null){
                leakDialogFragment.onCancelDialog(dialog);
            }
        }
    }
  • onActivityCreated

在onActivityCreated中设置自定义的onDismissListener和onCancleListener,并且在onDestroyView时设置为空。

@Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        mDialogDissmissLitener = new DialogDismissListener(this);
        mDialog.setOnDismissListener(mDialogDissmissLitener);
        mDialogCancleListener = new DialogCancleListener(this);
        mDialog.setOnCancelListener(mDialogCancleListener);
    }

  public void onDestroyView() {
        super.onDestroyView();
        if(mDialogDissmissLitener!=null){
            mDialogDissmissLitener = null;
        }
        if(mDialogCancleListener!=null){
            mDialogCancleListener = null;
        }
    }

五、总结

本文从一个DialogFragment内存泄露问题出发,通过分析Dialog的Dismiss的监听实现方法,找出了引起内存泄露的原因。然后重写DialogFragment,通过静态内部类以及弱引用的方式来解决内存泄露问题,希望对于DialogFragment的使用有帮助。

参考:https://medium.com/square-corner-blog/a-small-leak-will-sink-a-great-ship-efbae00f9a0f


https://mp.weixin.qq.com/s/1t2O-UUCKhru2SAcYoCuLA

谷歌确认将推出新功能 对标苹果AirDrop

北京时间7月1日早间消息,据外媒报道,此前几个月中一直有传言称谷歌将为Android手机添加一个类似苹果AirDrop的新功能。如今,谷歌终于证实,他们的确将推出这个功能,其正式名称为“Nearby Share”。一些Android手机用户已经测试了该功能的beta版本。

发布于:21小时以前  |  17次阅读  |  详细内容 »

老干妈回应:腾讯公司被骗了 并没有与腾讯有任何的合作

6月30日,中国裁判文书网显示,广东省深圳市南山区人民法院发布一则民事裁定书,同意原告腾讯请求查封、冻结被告老干妈公司公司名下价值人民币16240600元的财产。但6月30日下午,老干妈声明称,经核实,公司从未与腾讯或授权他人与腾讯就“老干妈”品牌签署《联合市场推广合作协议》,且从未与腾讯进行过任何商业合作。

发布于:21小时以前  |  18次阅读  |  详细内容 »

移动端常见崩溃指标

崩溃分析,是将 Android 和 iOS 平台常见的 APP 崩溃问题进行归类分析,帮助企业根据崩溃指标快速发现、定位问题。

发布于:1天以前  |  39次阅读  |  详细内容 »

58同城一季度净利润16.386亿元,同比增134.7%

6月26日讯,生活服务平台58同城(NYSE:WUBA)公布了截至2020年3月31日第一季度未经审计的财务报告。财报显示,58同城第一季度实现营收25.603亿元,同比下滑15.5%;净利润16.386亿元,同比增134.7%。

发布于:4天以前  |  28次阅读  |  详细内容 »

马斯克豪宅以2900万美元出手,买家为网易CEO丁磊

6月20日早间消息,据《华尔街日报》周五援引公开记录报道,特斯拉首席执行官埃隆·马斯克(Elon Musk)以2900万美元的价格出售了他在洛杉矶Bel-Air地区的一处房屋。报道称,买家是与中国亿万富翁丁磊有联系的公司。

发布于:10天以前  |  64次阅读  |  详细内容 »

甲骨文公司泄露数十亿条网络数据记录

据外媒报道,科技巨头「甲骨文」的数据管理平台BlueKai因为在服务器上不加密码从而泄露了全球数十亿条数据记录。甲骨文发言人黛博拉·海林格(Deborah Hellinger)对媒体表示,该消息属实。

发布于:10天以前  |  53次阅读  |  详细内容 »

罗永浩:筹备脱口秀节目,正在组建团队

6月21日晚间消息,极客公园与哔哩哔哩联合举办的Rebuild2020科技全明星峰会上,罗永浩在对话中透露,自己准备在一个比较大的平台上做一档综艺节目,是一档脱口秀节目,当下正在组建团队。“做这一档节目不是为了赚钱。”罗永浩说。

发布于:10天以前  |  40次阅读  |  详细内容 »

最多阅读

简化Android的UI开发 7月以前  |  192113次阅读
Google Enjarify:可代替dex2jar的dex反编译 1年以前  |  2288次阅读
Android设计与开发工作流 7月以前  |  2284次阅读
Android多渠道打包工具:apptools 1年以前  |  1922次阅读
Android权限 - 第一篇 1年以前  |  1893次阅读
Google Java编程风格规范(中文版) 1年以前  |  1869次阅读
Stetho 1年以前  |  1807次阅读
Android UI基本技术点 1年以前  |  1802次阅读
30分钟搭建一个android的私有Maven仓库 1年以前  |  1764次阅读
2015 Google IO带来的新 Android 开发工具 1年以前  |  1702次阅读
你应该知道的布局和属性 1年以前  |  1651次阅读
听FackBook工程师讲*Custom ViewGroups* 1年以前  |  1649次阅读
MVP在Android平台上的应用 1年以前  |  1631次阅读
Gradle小知识#3:任务的顺序 1年以前  |  1629次阅读