发布网友
共1个回答
热心网友
The Artist
LeakCanary 通知我存在内存泄漏:
* GC ROOT thread com.squareup.picasso.Dispatcher.DispatcherThread.<Java Local>
* references android.os.Message.obj
* references com.example.MyActivity$MyDialogClickListener.this$0
* leaks com.example.MyActivity.MainActivity instance
简单来说就是:一个 Picasso 线程正在站内持有一个 Message 实例的本地变量,而 Message 持有 DialogInterface.OnClickListener 的引用,而 DialogInterface.OnClickListener 又持有一个被销毁 Activity 的引用。
本地变量通常由于他们仅存在于栈内存活时间较短,当线程调用某个方法,系统就会为其分配栈帧。当方法返回,栈帧也会随之被销毁,栈内所有本地变量都会被回收。如果本地变量导致了内存泄漏,一般意味着线程要么死循环,要么阻塞了,而且线程在这种状态时持有着 Message 实例的引用。
于是 Dimitris 和我都去 Picasso 源码中一探究竟:
Dispatcher.DispatcherThread 是一个简单的 HandlerThread:
static class DispatcherThread extends HandlerThread {
DispatcherThread() {
super(Utils.THREAD_PREFIX + DISPATCHER_THREAD_NAME, THREAD_PRIORITY_BACKGROUND);
}
}12345
这个线程用标准的方式通过 Handler 接收 Message:
private static class DispatcherHandler extends Handler {
private final Dispatcher dispatcher;
public DispatcherHandler(Looper looper, Dispatcher dispatcher) {
super(looper);
this.dispatcher = dispatcher;
}
@Override public void handleMessage(final Message msg) {
switch (msg.what) {
case REQUEST_SUBMIT: {
Action action = (Action) msg.obj;
dispatcher.performSubmit(action);
break;
}
// ... handles other types of messages
}
}
}123456710111213141516171819
显然 Dispatcher.DispatcherHandler.handleMessage() 里面没有明显会让本地变量持有 Message 引用的 Bug。
Queue Tips
Let’s look at how HandlerThread works:
后来越来越多内存泄漏的通知出现了,这些通知不仅仅来自 Picasso,各种各样线程中的本地变量都存在内存泄漏,而且这些内存泄漏往往和 Dialog 的 OnClickListener 有关。发生内存泄漏的线程有一个共同的特性:他们都是工作者线程,而且通过某种阻塞队列接收各自的工作。
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
return;
}
msg.target.dispatchMessage(msg);
msg.recycleUnchecked();
}12345678
通过源码可以发现,肯定存在本地变量持有 Message 的引用,然而它的生命周期本应很短,而且在循环结束时被清除。
我们尝试通过利用阻塞队列实现一个简单的工作者线程来重现这个 Bug,它只发送一个 Message:
static class MyMessage {
final String message;
MyMessage(String message) {
this.message = message;
}
}
static void startThread() {
final BlockingQueue<MyMessage> queue = new LinkedBlockingQueue<>();
MyMessage message = new MyMessage("Hello Leaking World");
queue.offer(message);
new Thread() {
@Override public void run() {
try {
loop(queue);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}.start();
}
static void loop(BlockingQueue<MyMessage> queue) throws InterruptedException {
while (true) {
MyMessage message = queue.take();
System.out.println("Received: " + message);
}
}
12345671011121314151617181920212223242526272829
一旦 Message 被打印到 Log 中,MyMessage 实例应该被回收,然而还是发生了内存泄漏:
* GC ROOT thread com.example.MyActivity$2.<Java Local> (named 'Thread-110')
* leaks com.example.MyActivity$MyMessage instance
我们发送新的 Message 到阻塞队列的瞬间,前一个 Message 就被回收,而新的 Message 就泄漏了。
在 VM 中,每一个栈帧都是本地变量的集合,而垃圾回收器是保守的:只要存在一个存活的引用,就不会回收它。
在循环结束后,本地变量不再可访问,然而本地变量仍持有对 Message 的引用,interpreter/JIT 理论上应该在本地变量不可访问时将其引用置为 null,然而它们并没有这样做,引用仍然存活,而且不会被置为 null,使得它不会被回收。
为了验证我们的结论,我们手动将引用设为 null,并打印它,使得 null 不会是最优化办法:
static void loop(BlockingQueue<MyMessage> queue) throws InterruptedException {
while (true) {
MyMessage message = queue.take();
System.out.println("Received: " + message);
message = null;
System.out.println("Now null: " + message);
}
}12345678
在测试上面的代码时,我们发现 MyMessage 实例在 Message 被设为 null 时立刻被回收。也就是说我们的结论似乎是正确的。
因为这样的内存泄漏会在各种各样的线程和阻塞队列的实现中发生,我们现在确定这是一个存在于 VM 中的 Bug。基于这个结论,我们只能在 Dalvik VM 中复现这个 Bug,在 ART VM 或 JVM 中则无法复现。
Message in a (recycled) bottle
我们发现了一个会导致内存泄漏的 Bug,但这会导致严重的内存泄漏吗?不妨看看我们最初的泄漏信息:
* GC ROOT thread com.squareup.picasso.Dispatcher.DispatcherThread.<Java Local>
* references android.os.Message.obj
* references com.example.MyActivity$MyDialogClickListener.this$0
* leaks com.example.MyActivity.MainActivity instance
我们发送给 Picasso Dispatcher 线程的 Message,我们从未将 Message.obj 设为 DialogInterface.OnClickListener,那它是怎么结束的?
此外,当 Message 被处理,它应该立刻被回收,而且 Message.obj 应该被设为 null。只有那样 HandlerThread 才会等待下一个 Message,并暂时泄漏前一个 Message:
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
return;
}
msg.target.dispatchMessage(msg);
msg.recycleUnchecked();
}12345678
因而我们知道泄漏的 Message 会被回收,因此不会持有之前的内容。