美文网首页
记一次Dialog导致的内存泄露

记一次Dialog导致的内存泄露

作者: 在追风筝丶 | 来源:发表于2020-05-22 18:02 被阅读0次

    发现Message泄露

    我是在使用AddressPicker(cn.qqtheme.framework.picker.AddressPicker)时候发现的这个问题,在关闭打开过AddressPicker的页面后,会有概率出现内存泄露。

    LeakCanary报出的错误如下:


    leakCanary_message.jpg

    (上面的LeakAddressPicker继承了AddressPicker,可以理解为就是一个AddressPicker)

    大致就是说:有一个HandlerThread持有了一个Message,这个Message.obj持有了AddressPicker,而AddressPicker持有了Activity,最终导致了我们的Activity无法回收。于是我尝试在AddressPicker中找出一个持有自己的Message,然后再从这个Message着手去分析最终原因,但是最后还是没能找到这个Message。
    之后仔细查看LeakCanary消息,在Message.obj详情中看到了message.target=Dialog.ListenersHandler


    message_detail.jpg

    同时发现AddressPicker的弹窗就是个Dialog,于是猜测是这个Dialog导致了这次的内存泄露(本质上泄露的是Message,后面会说到)。

    Dialog中的Message

    我们可以在Dialog中找到这样3个Message

    public class Dialog implements DialogInterface, Window.Callback,
            KeyEvent.Callback, OnCreateContextMenuListener, Window.OnWindowDismissedCallback{
        private Message mCancelMessage;
        private Message mDismissMessage;
        private Message mShowMessage;
    }
    

    找到它们被赋值的地方:

    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;
            }
        }
    
      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 setOnShowListener(@Nullable OnShowListener listener) {
            if (listener != null) {
                mShowMessage = mListenersHandler.obtainMessage(SHOW, listener);
            } else {
                mShowMessage = null;
            }
        }
    

    可以看到它们都是在设置Dialog对应的事件时,从Message的缓冲池中获取到的一个Message。并将对应的message.obj指向了listener,而这个listener持有外部对象。我这里的OnDismissListener 持有了AddressPicker,所以导致mDismissMessage持有了AddressPicker。

    接下来我们来看这些Message是在什么地方被发送的

        public void cancel() {
            if (!mCanceled && mCancelMessage != null) {
                mCanceled = true;
                // Obtain a new message so this dialog can be re-used
                Message.obtain(mCancelMessage).sendToTarget();
            }
            dismiss();
        }
    
        private void sendDismissMessage() {
            if (mDismissMessage != null) {
                // Obtain a new message so this dialog can be re-used
                Message.obtain(mDismissMessage).sendToTarget();
            }
        }
    
        private void sendShowMessage() {
            if (mShowMessage != null) {
                // Obtain a new message so this dialog can be re-used
                Message.obtain(mShowMessage).sendToTarget();
            }
        }
    

    可以看到,在Dialog的对应的cancle、dissmiss、show方法中都是通过发送对应的Message到Handler(Dialog.mListenersHandler),再在Handler中接收到Message后根据对应的Message的obj做出对应回调。但是这里发出的Message并不是Dialog中的mCancelMessage、mDismissMessage、mShowMessage这三个Message,都是将它们复制出一个Message进行发送,而这三个Message一直都不会被发出。也就不会被销毁,直到垃圾回收器回收AddressPicker。(这里是这次泄露的一个关键点,如果对应的Message被发出,当Message被处理完后,会调用message.recycleUnchecked()来清空message的内容,也就会清空message.obj对AddressPicker的引用了。当然如果一直不发送该Message的话,它也就会一直持有AddressPicker)

    到这里,我们找到了持有AddressPicker的Message了,但是该Message在这里只是作为AddressPicker的一个成员,本不会有泄露问题,除非它在被Dialog使用前就已经发生了泄露。

    Looper.loop()--Message泄露的根源

    另外我们可以从LeakCanary报出的信息看出这个Message是被一个HandlerThread一直持有的。

    在HandlerThread对应的线程中会有一个Looper对象,在Looper.loop()中会有一个死循环一直从对应的MessageQueue中取出Message,并处理Message,在Message处理完后会清空他的内容(massage.recycleUnchecked()),并添加到Message的缓冲池中。

    下面是Looper.loop()与Message.recycleUnchecked()方法

    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;
                }
               //处理message
               msg.target.dispatchMessage(msg);
                //清空message内容,并添加到缓冲池
               msg.recycleUnchecked();
            }
        }
    
     void recycleUnchecked() {
            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++;
                }
            }
     }
    

    Java的内存模型告诉我们线程开启时会创建自己独有的虚拟机栈空间,当消息循环发生阻塞时,方法中的局部变量不能被释放。
    而Looper.loop()方法中就有这样一个死循环,当Looper对应的MessageQueue中不能取出Message时便发生了阻塞,所以这时循环中最后一条msg不能被正常释放,发生了泄露。这就是Looper泄露Message的根源。
    可以这样理解,在我们的项目中如果创建了一个HandlerThread,处理了一些Message之后很长一段时间没有新的Message加到MessageQueue中,或者说再也没有Message加入,此时HandlerThread便一直持有着最后一条Message,由于持有的Message最后调用了msg.recycleUnchecked()方法,所以这时候持有的是一个没有内容的message。但是msg.recycleUnchecked()最后可能会将msg添加到Message的缓冲池中,从而这条Message可能会在其它地方被使用到。

    到这里,应该就能理清整个泄露的原因了。有这样一种情况,上面HandlerThread持有的Message恰巧被Dialog给获取到了。所以最中导致了HandlerThread->Message->OnCancelListener->AddressPicker->Activity这样的引用链,导致activity不能正常回收。

    解决方案

    1、既然找到了对应的引用链,就可以通过具体的将对应对象置空释放的方式切断引用链。
    在这次AddressPicker泄露中,用于Dialog在AddressPicker内部,且OnDissmissListener在初始化时就已经设置,不方便通过Dialog切断引用,所以这里选择将AddressPicker中所有对activity的引用(直接或间接)置空。

    public class LeakAddressPicker extends AddressPicker {
    
        LeakAddressPicker(Activity activity, ArrayList<Province> provinces) {
            super(activity, provinces);
        }
    
        @Override
        public void onDismiss(DialogInterface dialog) {
            super.onDismiss(dialog);
            refreshLeak();
        }
    
        private void refreshLeak() {
            activity = null;
            cancelButton = null;
            submitButton = null;
            titleView = null;
            headerView = null;
            centerView = null;
            footerView = null;
            List<Field> allField = getAllField();
    
            clearField(allField, "contentLayout");
            clearField(allField, "dialog");
            clearField(allField, "onAddressPickListener");
        }
    
        private List<Field> getAllField() {
            Class clazz = getClass();
            List<Field> fieldList = new ArrayList<>();
            while (clazz != null) {
                fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));
                clazz = clazz.getSuperclass();
            }
            return fieldList;
        }
    
        private void clearField(List<Field> allField, String fieldName) {
            Field contentLayoutField = null;
            for (Field targetField : allField) {
                if (targetField.getName().equals(fieldName)) {
                    contentLayoutField = targetField;
                    break;
                }
            }
    
            if (contentLayoutField != null) {
                contentLayoutField.setAccessible(true);
                try {
                    contentLayoutField.set(this, null);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
                contentLayoutField.setAccessible(false);
            }
        }
    }
    

    2、如果是自己使用Dialog,则可以通过对对应的Listener进行包装,内部使用弱引用持有外部对象,使得外部对象能够正常回收。类似Handler内存泄露处理。

    3、LeakCanary作者提供了如下的一种解决方案

    static void flushStackLocalLeaks(Looper looper) {
      final Handler handler = new Handler(looper);
      handler.post(new Runnable() {
        @Override public void run() {
          Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
            @Override public boolean queueIdle() {
              handler.sendMessageDelayed(handler.obtainMessage(), 1000);
              return true;
            }
          });
        }
      });
    }
    

    找到所有可能导致Message泄漏的HandlerThread对应的Looper,并在它空闲时往它里面添加空内容的Message,使Looper.loop()中不会出现阻塞,或者只是短暂阻塞,从而避免msg的泄露。但是这种方案前提是需要我们找到所有可能发生Message泄露的HandlerThread。同时不断往他们的MessageQueue中添加message,使线程处于运行状态,所以这种方案不推荐。

    最后感谢一个内存泄漏引发的血案,从这篇文中理清了整个流程。

    相关文章

      网友评论

          本文标题:记一次Dialog导致的内存泄露

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