发现Message泄露
我是在使用AddressPicker(cn.qqtheme.framework.picker.AddressPicker)时候发现的这个问题,在关闭打开过AddressPicker的页面后,会有概率出现内存泄露。
LeakCanary报出的错误如下:
(上面的LeakAddressPicker继承了AddressPicker,可以理解为就是一个AddressPicker)
大致就是说:有一个HandlerThread持有了一个Message,这个Message.obj持有了AddressPicker,而AddressPicker持有了Activity,最终导致了我们的Activity无法回收。于是我尝试在AddressPicker中找出一个持有自己的Message,然后再从这个Message着手去分析最终原因,但是最后还是没能找到这个Message。
之后仔细查看LeakCanary消息,在Message.obj详情中看到了message.target=Dialog.ListenersHandler
同时发现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 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 allField = getAllField();
clearField(allField, "contentLayout");
clearField(allField, "dialog");
clearField(allField, "onAddressPickListener");
}
private List getAllField() {
Class clazz = getClass();
List fieldList = new ArrayList<>();
while (clazz != null) {
fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));
clazz = clazz.getSuperclass();
}
return fieldList;
}
private void clearField(List 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,使线程处于运行状态,所以这种方案不推荐。
最后感谢一个内存泄漏引发的血案,从这篇文中理清了整个流程。