从源码角度看Handler为何发生内存泄漏及其解决方法
文章目录
前言
在一次开发工作中,由于平时使用的测试机坏了,我拿了一个很老版本的手机来测试app,结果发现在某个复杂页面的性能表现有很大的问题。于是打开相关的性能统计平台,果不其然,发现了内存泄漏问题。于是我打开了小伙伴写的代码,发现了这么一串类似代码。
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg){
super.handleMessage(msg);
mPriceEditText.setText("1000");
}
};
于是我立马联系小伙伴说不能这么写,这样子会导致内存泄漏,小伙伴听到之后问了我一句,为什么会导致内存泄漏,我一下子不知道该如何回答,于是直接把写这段代码时,官方的提示给他看。
小伙伴哦的一句,就默默改代码去了。问题挺好解决,小伙伴很好糊弄,自己却糊弄不了自己,不只要知道怎么解决,还得静下心来自己摸索摸索,为什么handler会导致能存泄漏呢?是因为内部类持有了对外部类的引用引起的么?但为何其他内部类不会有这个问题?怀着这个问题,我觉得看看handler的相关源码。
下面我们从源码来了解Handler发生内存泄漏的原因,以及解决方法
一、内存泄漏的本质是什么?
在了解handler为什么会导致内存泄漏,是否是因为是内部类,其他的内部类是否会导致内存泄漏之前,我觉得我们的先了解内存泄漏的本质是什么。
内存泄漏的本质和原因是:长生命周期的对象持有短生命周期对象的引用.
在android里面,很多情况下的短生命周期最有可能就是我们的Activity。假如用户退出了activity,但是有一个长生命周期的对象引用了该activity,就会导致activity无法回收。
而我们App里面最长的生命周期是主线程。任何被主线程持有的对象都是无法被回收的。
二、Handler内存泄漏的原因
1.什么是Handler
首先我们了解下handler是什么:
Handler是android子线程和主线程之间通信的一种机制。
主要是通过looper轮询检测发送消息到MessagerQuerue,如果MessageQueue对Message入列,就会取出消息,交给Handler去处理。
也就是说,在Handler通信过程中,一共涉及到了Hanler、Looper、MessageQueue和Message这四个类。所以我们就需要对这4个类进行具体分析,看他们之间的持有链是什么样的。
2.Looper的创建与持有
如果我们打开Handler的源码,我们可以看到他的构造方法有这么一串。
public Handler(@Nullable Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class<? extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}
mLooper = Looper.myLooper();// 获取该线程的Looper对象
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
在这个构造方法中,我们可以看到如果我们不传入Looper对象,那么将会使用本线程的Looper。比如我们在非UI线程之后,会首先调用Looper.prepare()来制定Looper对象。
那如果在UI线程中初始化的Handler,它的Looper是在哪里赋值的?
这时需要我们打开UI线程源码,也就是主线程ActivityThread,因为ActivityThread是系统文件,并没有被打包到jar包中,所以反编译是看不到的。但我们有其他办法可以看到,就是在SDK目录下,打开任一个SDK目录的app目录中,可以找到ActivityThread文件,具体目录如下:
分析源码,我们首要看其main函数,在main函数中有一段
public static void main(String[] args) {
//省略与本文无关的代码
...
Process.setArgV0("<pre-initialized>");
Looper.prepareMainLooper();
long startSeq = 0;
if (args != null) {
for (int i = args.length - 1; i >= 0; --i) {
if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
startSeq = Long.parseLong(
args[i].substring(PROC_START_SEQ_IDENT.length()));
}
}
}
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
// End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
可以看到在ActivityThread的main函数中,调用了Looper的prepareMainLooper()方法,于是我们进入looper的源码查看相关方法
public static void prepareMainLooper() {
prepare(false);//执行了looper.prepare()
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
这里可以看到在主线程中ActivityThread的main方法中会调用looper.prepareMainLopper方法,进而构造一个looper。然后把looper放进ThreadLocal,并放到主线程的ThreadLocalMap中。也就是说这个looper是主线程持有的,是无法被释放掉的。
3.MessageQueue的持有链
在前面我们知道Handler的原理是Looper调用loop()方法不断的轮询MessageQueue,那按照道理,Looper应该是持有对MessageQueue的引用的,于是我们带着问题来查找下源码,能看到以下的片段:
public static void loop() {
//与本文无关代码...
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;
}
//与本文无关代码...
}
}
在looper.loop()方法中,我们看到了死循环中对MessageQueue的轮询操作,也就是looper是实现了对MessageQueue的引用,实现了引用链ActivityThread->looper->MessageQueue。
4.Message和Handler的持有链
我们知道MessageQueue是消息队列,Message是消息,那Message是肯定入列的,也就是MessageQueue是肯定持有Message的,但是这也就引出一个问题,Message是什么时候入列的。
这也就引出了Handler造成内存泄漏有一个前提,必须曾经发送过消息,即使用过sendMessage()方法,为什么使用了sendMessage()方法才会造成内存泄漏,我们看看sendMessage方法的源码。
public final boolean sendMessage(@NonNull Message msg) {
return sendMessageDelayed(msg, 0);
}
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this; //把handler赋值给message的target
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
在我们调用Handler的sendMessage()方法之后,其实会一步步最终执行Handler的enqueueMessage()方法,这里可以看到把handler自身赋值给了Message的target。这是一个很重要的操作。保证了一个线程中多个handler发消息处理消息时,能准确的找到对应的Handler来处理。但是这里也就意味着Message持有了Handler。那意味着Message -> handler。此时只要MessageQueue持有了Message,那持有链就连接成了,下面我们看看Message的入列。
在上面的queue.enqueueMessage方法中引导我们来到了MessageQueue的源码中。
enqueueMessage()是MessageQueue的入列方法。虽然 MessageQueue 的名字包含队列(Queue),但是其底层实现采用的是单链表,这是因为链表在插入和删除方面的性能好,下面是enqueueMessage()源码:
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
synchronized (this) {
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
msg.recycle();
return false;
}
msg.markInUse();
msg.when = when;
Message p = mMessages;//p 代表下一个消息
boolean needWake;
if (p == null || when == 0 || when < p.when) {//插在消息列表的最前面
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {//按照时间顺序从小到大进行的
// Inserted within the middle of the queue. Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
在上面代码中,我们可以看到如果Handler调用了sendMessage的话,就会推动把Message加入到MessageQueue队列之中,至于添加到哪个位置,主要根据时间而定。并且会把handler自身赋值给Message的target。此时完整的持有链ActivityThread -> Looper ->MessageQueue -> Message -> Handler就成立了。也就是说Handler此时被主线程间接持有,是无法被回收的。
在我们的Actvity中,Handler也包括了对Activity外部类的引用,如果在我们关闭界面时,有消息还未处理完,就会导致Activity无法被回收,导致内存泄漏。
三、Handler内存泄漏的解决方案
刚刚说到内存泄漏时MessageQueue里面还有消息未处理导致的,那是否只要保证MessageQueue里面没有队列就可以了?
确实当Message被回收时,会自动调用recycle方法把message置为空,即target不引用Handler。也就是说能把Message -> Handler的链条断开。即message都被消费后,messageQueue为空时,就不会指向handler,就不会导致activity内存泄漏。
但是我们在开发中,是无法保证message全部被消费的,特别是发送延时消息,当然我们可以通过监听生命周期,一旦Activity被销毁,则清空MessageQueue里面所有的Message,但是如果忘记重写onDestroy()方法,就会导致内存泄漏,所以这个方法不太合理。
那该如何更合理的解决呢?
其实在前言那里加油提到,在我们写下Handler内部类时,AndroidStudio就会提示我们需要使用static修饰符,否则会内存泄漏,所以最简单的解决方法就是乖乖听劝,设置为静态内部类。
我们也可以使用弱引用,可以防止Handler持有Activity,断开Handler -> Activity这条链。
下面是静态内部类和弱引用的写法,仅供参考:
private static class FragmentHandler extends Handler{
private WeakReference<TradeDetailFragment> mWeakReference;
public FragmentHandler(TradeDetailFragment fragment) {
mWeakReference = new WeakReference<>(fragment);
}
@Override
public void handleMessage(Message msg) {
TradeDetailFragment host = mWeakReference.get(); // 判断所在的 Activity 的引用是否被回收了
if (host != null) {
switch (msg.what){
case 99:
// do something
sendEmptyMessageDelayed(100, 2000);
break;
case 100:
// do something
break;
default:break;
}
}
}
}
这种方式在Activity和Fragment都是可以实现的。
总结一下今天的内容:
主要从源码分析了Handler为何会导致内存泄漏,其实不是因为是内部类导致,主要是因为Handler被长生命周期对象所持有,形成了 ActivityThread -> Looper ->MessageQueue -> Message -> Handler的持有链。而Handler又持有了外部类的引用,所以导致了Activity无法被回收。
然后提供了解决Handler内存泄漏的方法:
1、保证Activity退出时,清空MessageQueue的所有Message。
2、使用static修饰内部类,内部类写成静态内部类或者外部类。
3、在Handler中,将对象改成弱引用,就能保证在垃圾回收时被正常回收。