写在前面
两年前的时候有写过一点对 Handler 的总结(Android:关于 Handler 消息传递机制),现在重新回顾,并增加一些东西和理解。
内容
内存泄露
在 Activity 里我们使用 Handler 的时候,这样写的话 IDE 会提示可能存在内存泄露的问题。
Handler handler=new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
假设在一个这样的场景,我们通过 Handler 来发送一个延时的消息,在消息还没发送之前,我们就退出了 Activity,那么此时就会存在内存泄露的问题。
原因在于 Handler 在这里是一个匿名内部类,匿名内部类会持有外部类的引用,所以此时 Activity 退出,由于 Handler 里的内容还未处理完,就还持有 Activity 的引用。所以触发 GC 回收的时候,就无法回收这个 Activity。
那么 GC 不回收一些对象是因为GC Root 的可达性,这些对象在 GC Root 上还被引用着,所以就不会被回收。
在使用 Handler 的时候,我们需要Looper.prepare();
:
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
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));
}
这里用到了 ThreadLocal 类型的 sThreadLocal,是一个静态变量,静态变量就可以作为一个 GC Root 。
那么 Looper 里面会有个 MessageQueue,MessageQueue 里会有 Message:
public final class Looper {
...
final MessageQueue mQueue;
...
}
而 Message 的 target 是个 Handler 类型:
public final class Message implements Parcelable {
...
Handler target;
...
}
Handler 在发送消息的时候,会把自己设置到这个 target 里:
public class Handler {
...
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
...
return queue.enqueueMessage(msg, uptimeMillis);
}
...
}
于是这样一条 GC Root 的引用链就出现了:
ThreadLocal --> Looper --> MessageQueue --> Message --> Handler --> Activity
所以 Activity 退出后,因为消息未处理,就导致 Activity 无法被回收,从而导致了内存泄露。
解决方法
清空消息
在 Activity 退出的时候,把消息队列里的消息都清掉
// MainActivity.java
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
// Handler.java
/**
* Remove any pending posts of callbacks and sent messages whose
* <var>obj</var> is <var>token</var>. If <var>token</var> is null,
* all callbacks and messages will be removed.
*/
public final void removeCallbacksAndMessages(Object token) {
mQueue.removeCallbacksAndMessages(this, token);
}
静态内部类和弱引用
由于静态内部类不会持有外部类 的引用,所以将 Handler 以这种方式呈现,解决到 Handler 与 Activity 的引用关系。
但在一些场景下,我们可能需要在 Handler 里通过持有 Activity 来调用它的一些方法,此时如果直接传入的话,显然就造成了一种直接引用的情况,所以在这种情况下我们需要弱引用,被标记为弱引用的对象,在 GC 回收的时候,就会被回收。因此在被回收的情况下,我们就需要做多一些判空的处理。
所以当我们这个 Handler 里不需要使用到 Activity 的方法的时候,也没必要引入这个 Activity 了,也就不需要多这个弱引用的关系。
private static class MyHandler extends Handler {
WeakReference<MainActivity> mainActivityWeakReference;
public MyHandler(MainActivity activity) {
mainActivityWeakReference = new WeakReference<MainActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity activity = mainActivityWeakReference.get();
if(activity != null) {
activity.setTextViewText("hello");
}
}
}
创建一个 Message
我们在创建一个 Message 的时候,可能会使用
Message message = new Message();
但官方里还是推荐我们使用 Handler 的 obtainMessage()
方法,它会在一个全局的 Message 池里返回一个新的 Message 给你,从效率上会更快,并且也起到了复用的作用。
Handler handler = new Handler();
handler.obtainMessage();
这里面是一个享元的设计模式。
/**
* Returns a new {@link android.os.Message Message} from the global message pool. More efficient than
* creating and allocating new instances. The retrieved message has its handler set to this instance (Message.target == this).
* If you don't want that facility, just call Message.obtain() instead.
*/
@NonNull
public final Message obtainMessage()
{
return Message.obtain(this);
}
每个线程只能有一个 Looper
在使用 Handler 的时候,我们需要 Looper.prepare()
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
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));
}
ThreadLocal 里会在第一次的时候,创建 Looper,后续再调用的话就会抛出异常,提示每个线程里只能有一个 Looper 被创建。
因此我们要获取当前线程的 Looper 的话,就需要使用 Looper.myLooper()
:
/**
* Return the Looper object associated with the current thread. Returns
* null if the calling thread is not associated with a Looper.
*/
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
ThreadLocal
待续
Looper 死循环里为什么不会导致卡死
在 MessageQueue 里面会通过next()
方法来获取下一条信息:
// MessageQueue.java
private native void nativePollOnce(long ptr, int timeoutMillis);
Message next() {
...
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
...
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
}
...
}
...
nextPollTimeoutMillis = 0;
}
}
在这里面它会用 nextPollTimeoutMillis 来记录下一条消息要开始的时间点,如果消息列表里没有消息就设为 -1。
因此在循环的时候,会先判断这个变量,通过nativePollOnce(ptr, nextPollTimeoutMillis);
可以让其在下个时间点到来之前,进入休眠状态。这里涉及到 Linux 下的多路复用机制(epoll)相关。
假设现在有一条消息被加入到消息队列里,而线程又还处于休眠状态,时间点未到,那么就会通过nativeWake()
方法将其唤醒:
// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
...
synchronized (this) {
...
msg.markInUse();
msg.when = when;
Message p = mMessages;
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;
}
更多这方面的内容还可以查看:《卡顿、ANR、死锁,线上如何监控? 》