AlertDialog内存泄露分析

1,673 阅读4分钟

说明:Dalvik虚拟机下才有可能发生内存泄露,Art不会有该问题。

原因分析

平时开发中AlertDialog经常使用到,一般代码写成这个样子:

AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setNegativeButton("确定", new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {

    }
});
builder.show();

这里先分析一下DialogInterface.OnClickListener对象是怎样响应点击事件的。

先看AlertDialog.Builder的setNegativeButton方法:

public Builder setNegativeButton(CharSequence text, final OnClickListener listener) {
    P.mNegativeButtonText = text;
    P.mNegativeButtonListener = listener;
    return this;
}

只是把引用保存起来,看看show方法:

public AlertDialog show() {
    final AlertDialog dialog = create();
    dialog.show();
    return dialog;
}

public AlertDialog create() {
    // Context has already been wrapped with the appropriate theme.
    final AlertDialog dialog = new AlertDialog(P.mContext, 0, false);
    P.apply(dialog.mAlert);
    dialog.setCancelable(P.mCancelable);
    if (P.mCancelable) {
        dialog.setCanceledOnTouchOutside(true);
    }
    dialog.setOnCancelListener(P.mOnCancelListener);
    dialog.setOnDismissListener(P.mOnDismissListener);
    if (P.mOnKeyListener != null) {
        dialog.setOnKeyListener(P.mOnKeyListener);
    }
    return dialog;
}

这里的P是一个AlertController.AlertParams对象,看它的apply方法:

public void apply(AlertController dialog) {
    ......
    if (mNegativeButtonText != null) {
        dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText,
                mNegativeButtonListener, null);
    }
    ......
}

mNegativeButtonListener就是刚才所说的保存的引用,看setButton方法:

public void setButton(int whichButton, CharSequence text,
        DialogInterface.OnClickListener listener, Message msg) {

    if (msg == null && listener != null) {
        msg = mHandler.obtainMessage(whichButton, listener);
    }

    switch (whichButton) {

        case DialogInterface.BUTTON_POSITIVE:
            mButtonPositiveText = text;
            mButtonPositiveMessage = msg;
            break;

        case DialogInterface.BUTTON_NEGATIVE:
            mButtonNegativeText = text;
            mButtonNegativeMessage = msg;
            break;

        case DialogInterface.BUTTON_NEUTRAL:
            mButtonNeutralText = text;
            mButtonNeutralMessage = msg;
            break;

        default:
            throw new IllegalArgumentException("Button does not exist");
    }
}

这个就比较重要了,这里会生成一个mButtonNegativeMessage,做为类的成员变量,它的obj引用着外部设置的DialogInterface.OnClickListener对象。接下来看AlertDialog触发点击事件后如何处理:

protected void setupButtons(ViewGroup buttonPanel) {
    int BIT_BUTTON_POSITIVE = 1;
    int BIT_BUTTON_NEGATIVE = 2;
    int BIT_BUTTON_NEUTRAL = 4;
    int whichButtons = 0;
    mButtonPositive = (Button) buttonPanel.findViewById(R.id.button1);
    mButtonPositive.setOnClickListener(mButtonHandler);
	......
}

private final View.OnClickListener mButtonHandler = new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        final Message m;
        if (v == mButtonPositive && mButtonPositiveMessage != null) {
            m = Message.obtain(mButtonPositiveMessage);
        } else if (v == mButtonNegative && mButtonNegativeMessage != null) {
            m = Message.obtain(mButtonNegativeMessage);
        } else if (v == mButtonNeutral && mButtonNeutralMessage != null) {
            m = Message.obtain(mButtonNeutralMessage);
        } else {
            m = null;
        }

        if (m != null) {
            m.sendToTarget();
        }

        // Post a message so we dismiss after the above handlers are executed
        mHandler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, mDialogInterface)
                .sendToTarget();
    }
};

点击后会响应mButtonHandler的onClick方法,然后复制一个Message对象,最后发送给mHandler处理。看一下mHandler是如何处理的:

private static final class ButtonHandler extends Handler {
    // Button clicks have Message.what as the BUTTON{1,2,3} constant
    private static final int MSG_DISMISS_DIALOG = 1;

    private WeakReference<DialogInterface> mDialog;

    public ButtonHandler(DialogInterface dialog) {
        mDialog = new WeakReference<>(dialog);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {

            case DialogInterface.BUTTON_POSITIVE:
            case DialogInterface.BUTTON_NEGATIVE:
            case DialogInterface.BUTTON_NEUTRAL:
                ((DialogInterface.OnClickListener) msg.obj).onClick(mDialog.get(), msg.what);
                break;

            case MSG_DISMISS_DIALOG:
                ((DialogInterface) msg.obj).dismiss();
        }
    }
}

拿到msg.obj后强转为DialogInterface.OnClickListener对象,然后调用其onClick方法。

以上就是setNegativeButton的流程,看起来都是比较正常的,但我们看到这里有个mHandler,就要猜测是不是经常说的Handler泄露问题,不过这个ButtonHandler被声明为static,然后用了WeakReference引用外部Dialog对象,看起来是不会有问题。还可能哪里会出现内存泄露问题呢?

按照刚才所说的,这里存在一条引用链:msg --> DialogInterface.OnClickListener对象 --> 外部类对象(比如Activity对象)

所以要是发生内存泄露,那么一定是msg被在某个地方被长期引用了。这里的某个地方,就是Looper中loop方法的本地栈帧的局部变量。看一下loop方法:

public static void loop() {
    ......
    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }

        ......

        msg.recycleUnchecked();
    }
}

假如有这一种情况:某个线程,queue.next()获取到了一个需要处理的msg,然后就开始分发事件处理,处理结束后调用recycleUnchecked,该msg就会被加入Message回收池里(注意这个池是多线程共用的),在Dalvik虚拟机上,loop方法里面的局部变量msg还是会继续引用着这个已经加入回收池的Message对象,直到queue.next()又获取到新的Message对象然后把msg覆盖掉。如果这时mButtonHandler的onClick里面obtain到的刚好是这个Message对象,那么这个Message对象的obj就会被设置为DialogInterface.OnClickListener对象,引用链:局部变量(任意Thread) --> Message对象 --> DialogInterface.OnClickListener对象 --> 外部类对象

这时退出Activity,就会出现内存泄露。

解决方法

1.首先在Dalvik上才会有这个问题,Art没有问题。

2.如果不需要用到外部类对象,就直接定义为单独的类,如果需要,就用WeakReference引用外部类对象,另外在对话框关闭时,清空相关的引用。

3.如果Dialog是在第三方SDK中,我们一般不能修改其代码,第2中说的方法也就用不了,这时可以给空闲线程发送一个空消息,让它的queue.next()返回,发送空消息的时机可以是Dialog消失,或者Activity关闭。

参考资料:

一个内存泄漏引发的血案-Square

另外,如果有兴趣,欢迎关注公众号:Android进阶驿站(纯个人用于平时技术经验总结,无广告等套路)