说明: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关闭。
参考资料:
另外,如果有兴趣,欢迎关注公众号:Android进阶驿站(纯个人用于平时技术经验总结,无广告等套路)