Android5.0以下系统,由于Message复用引起的内存泄漏问题
相关链接:
A small leak will sink a great ship
源码
/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the 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.target.dispatchMessage(msg);
......
msg.recycle();
}
}
-
在5.0以下系统,如果 queue.next() 处于阻塞挂起状态,则 Message msg 这个__局部变量__ 依然会存在于__方法栈__ 中(虚拟机相关),而此时该 msg 已经执行 msg.recycle() 方法了,因此该 msg 可能会被 复用,这样在某些场景下,就会导致该 msg(复用之后) 所持有的引用无法被及时释放;
-
当然,如果这个线程一直在运行而不阻塞挂起,那么这个问题也就不复存在了;然而,对于 HandlerThread 来说,总会有挂起的时候- 。-
public class Dialog implements DialogInterface, Window.Callback,
KeyEvent.Callback, OnCreateContextMenuListener {
......
private Message mCancelMessage;
private Message mDismissMessage;
private Message mShowMessage;
......
/**
* Sets a listener to be invoked when the dialog is shown.
* @param listener The {@link DialogInterface.OnShowListener} to use.
*/
public void setOnShowListener(OnShowListener listener) {
if (listener != null) {
mShowMessage = mListenersHandler.obtainMessage(SHOW, listener);
} else {
mShowMessage = null;
}
}
......
private void sendShowMessage() {
if (mShowMessage != null) {
// Obtain a new message so this dialog can be re-used
Message.obtain(mShowMessage).sendToTarget();
}
}
......
}
-
实际上,如果这个 msg 不断的被回收并复用,那么这个内存泄漏也是很短暂的一瞬间,可以忽略;但是如果这个msg不会被回收复用呢?比如 Dialog
-
从 Dialog 的源代码可以发现,mCancelMessage/mDismissMessage/mShowMessage 都是通过 Message.obtain() 方法创建的,也就是说有很大的可能是复用已回收的 Message,但是这几个 Message 都__不会再次被回收复用__
-
如果这几个 Message 恰好又是上文中提到的 Message ,那么这个 Message 就无法被及时回收,另外因为这几个 Message 都持有匿名内部类,也就是间接持有 Dialog 引用,这样就会引起 Dialog 内存泄漏。
实例
上图为 Dialog 泄漏的实例,GalaCompatDialog 是兼容处理类,因此图上这个 Dialog 没有出现内存泄漏;但是从这个图上,可以清楚的看到整个泄漏的过程;可以想象如果这个 Message 持有的不是 WeakOnCancelListener,那么这个 Dialog 就无法被及时回收。
总结
前置条件
-
Android 5.0 以下系统(Dalvik虚拟机)
-
HandlerThread 阻塞挂起,最后一个 Message 已经被回收待复用
-
该 Message 被复用,且未再次被回收
分析特征
-
泄漏对象因为 Message 而被链接到一个(无关的) HandlerThread
-
一般为方法栈引起的泄漏(Java Local)
解决
-
Android 5.0 及以上系统(ART虚拟机),无需处理
-
使用 Dialog 时,尽量确保 On[Cancel|Dismiss|Show]Listener 不以强引用的方式持有外部对象 或 退出时调用 setOn[Cancel|Dismiss|Show]Listener(null)
-
涉及 Message 回收复用的逻辑,尽量确保 Message 会再次被回收
附注
- Android 5.0 及以上系统(ART虚拟机),该问题已经修复