今天无意间发现公司的App存在内存泄漏,商品详情页面无法正常回收。起初以为是WebView导致的,可是在上道具查看之后,发现其实并不是。
用Profiler将内存dump并export出来,扔到MAT中,结果如下图所示:
从图中可以看到,一共4个详情页,被别处用链表串了起来,整整齐齐,一个不落。不过这样有个好处,只用分析这一处就可以将此次泄漏问题一锅端。
从图中可以发现,引用详情页的地方是在ActivityThread
中的mNewActivities
字段中。该字段是ActivityClientRecord
类型的:
我们只需要关心两个字段:activity
和nextIdle
,正是这两个字段将详情页串起来的。
通过在ActivityThread中搜索mNewActivities
,发现一共有两处操作了该字段。第一处是在方法handleResumeActivity
中进行了赋值操作,另一处是在内部类Idler
中对它赋值为null。
简单说下ActivityClientRecord
。ActivityRecord
是ActivityManagerServer中用来标识Activity的类,包含了一个Activity的所有信息。而ActivityClientRecord
代表了应用进程中的一个Activity。
来看handleResumeActivity
方法的代码:
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
...
// 我们主要看操作mNewActivities字段的代码
if (!r.onlyLocalRequest) {
r.nextIdle = mNewActivities;
mNewActivities = r;
if (localLOGV) Slog.v(
TAG, "Scheduling idle handler for " + r);
Looper.myQueue().addIdleHandler(new Idler());
}
r.onlyLocalRequest = false;
...
}
从代码中可以看出来,新创建的ActivityClientRecord
都会插入到链表的头部,而mNewActivities
正好是该链表的头。而接下来的Looper.myQueue().addIdleHandler(new Idler());
正是前面说的第二处操作mNewActivities
的地方,这行代码的意思是将一个Idler
对象放入到消息队列中,也就是将Idler对象添加到MessageQueue
中的mIdleHandlers
字段中,如下所示:
public void addIdleHandler(@NonNull IdleHandler handler) {
if (handler == null) {
throw new NullPointerException("Can't add a null IdleHandler");
}
synchronized (this) {
mIdleHandlers.add(handler);
}
}
那么,它是什么时候执行的呢?从MessageQueue
的next
方法我们可以知道:当处于空闲状态时,会执行IdleHandler中的方法。
从这里猜测应该是发送到消息队列中的Idler
对象没有被执行,在商品详情页中添加如下代码进行测试。
Looper.getMainLooper().getQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
Log.e("Idler >>>", "queueIdle()");
return false;
}
});
经测试,发现确实没有打印出相关日志,说明消息队列中一直有消息存在。将消息队列中处理的消息打印出来。同样,在详情页添加如下代码:
Looper.getMainLooper().setMessageLogging(new Printer() {
@Override
public void println(String x) {
Log.e("message >>>", x);
}
});
发现大量的如下日志:
Dispatching to Handler (android.view.ViewRootImpl$ViewRootHandler) {a0266dd} null: 1
Finished to Handler (android.view.ViewRootImpl$ViewRootHandler) {a0266dd} null
......
从中可以知道,ViewRootHandler处理了大量ID为1的消息。这是什么消息呢?这其实是MSG_INVALIDATE
消息,用来刷新界面的。
局面已经很明了了:详情页一直在刷新界面,导致IdleHandler
无法执行,从而使mNewActivities
字段一直持有Activity的引用,最终造成了内存泄漏。
经过仔细排查,发现是详情页面引用了一个用来显示购物车数量的自定义View,在该自定义View的onDraw
方法中,将canvas
传递给外部的一个操作Helper
类,在Helper
类的绘画操作结束之后,调用了postInvalidate()
去刷新界面。这就导致了不停的onDraw死循环。
解决方法就很简单了,删掉相关的postInvalidate
调用。
经检验,引用到该控件的页面均能正常回收。
好了,结束。