前言
做Android开发一定绕不开Handler机制,关于Handler机制大家也是耳熟能详了,Looper+MessageQueue+Mesage+Handler组成,Handler将msg放入MessageQueue,looper循环从MessageQueue中取出msg交给handler执行。
本文就不详细讲解这些基本知识了,重点关注一下looper到底是怎么工作的?
我们知道一个普通线程如果想使用handler,官方给出了示例
class LooperThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
// process incoming messages here
}
};
Looper.loop();
}
那么就通过这几步,我们一点点来看looper到底是怎么回事。
创建looper
首先就是创建,执行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));
}
可以看到创建了一个looper对象,并通过ThreadLocal进行存储。如果该线程已经创建了looper直接抛出异常。
为什么要使用ThreadLocal存储?
ThreadLocal的作用之一就是每个线程独立保存信息,方便该线程后续方法直接使用,类似全局变量,避免了各种传参。
目前我们还看不到它的作用,不要着急,讲到后面就明白了。
继续看Looper的构造函数:
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
可以看到创建了一个MessageQueue,同时持有当前线程。
所以一个线程只有一个looper和一个MessageQueue。
这里注意:Android主线程,即UI线程执行的是另外一个函数prepareMainLooper()
,代码如下
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
可以看到有两处区别:
- 增加了一个sMainLooper变量,方便在其他线程获取主线程的looper,即
getMainLooper()
- 执行prepare时参数是false,也就是不允许退出,所以主线程的looper没法手动退出。
创建handler
下面就是创建handler,这里直接使用了new Handler(),可以看到没有任何参数,那么这个handler如何与looper关联?
看一下构造函数:
public Handler(@Nullable Callback callback, boolean async) {
...
mLooper = Looper.myLooper();
...
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
可以看到直接调用了Looper的myLooper函数,这是个static函数,它是的代码很简单
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
这里就体现了ThreadLocal的作用,自动根据当前线程获取对应的looper,这样避免的传参,也更加安全(防止传错)。
这样handler就得到了Looper和MessageQueue的对象,就与looper关联上了。
启动looper
最后就是启动looper,执行Looper.loop(),代码如下:
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
...
for (;;) {
Message msg = queue.next(); // 这里可能会阻塞
if (msg == null) {
// 注意,这里是线程结束。如果仅仅是MessageQueue为空,并不执行到这里,那种情况在queue.next()中,后面会讲。
return;
}
...
try {
//处理消息
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
...
} finally {
...
}
...
}
}
重点代码就是一个for循环,这里就是looper的关键。
可以看到一个for (;;)
会一直循环,每次执行queue.next()
获取消息,然后调用msg.target.dispatchMessage(msg);
处理消息。这里msg的target就是handler对象,所以就执行到handler的dispatchMessage
了。看一下这个代码:
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
可以看到如果msg有callback,先执行callback,这个callback就是Runnable。
当我们使用handler的post(Runnable)
等函数,handler会自动将runnable对象包装成Message,并将runnable对象赋值给这个msg的callback。
如果没有callback则继续执行,最后会给handleMessage(msg)
函数处理,这就是我们非常熟悉的了,我们创建handler会重写这个方法处理msg。
这么看looper好像很简单,但是这里要非常注意下面这段代码:
if (msg == null) {
// 注意,这里是线程结束。如果仅仅是MessageQueue为空,并不执行到这里,那种情况在queue.next()中,后面会讲。
return;
}
我也添加了注释,这里并不是如果队列中没有消息就退出,而且线程结束了(或手动关闭looper)时才会执行到这里跳出循环。
那么如果队列中没有消息是什么情况?这就涉及到looper的休眠问题了。
looper休眠问题 及 delay问题
上面看到loop()函数中是一个死循环,除非结束否则一直在循环,那样如果队列中没有消息,岂不是一直浪费资源么?
实际上looper是有休眠的,但是这个并不体现在looper的代码中,而是在MessageQueue的next()函数中。
先不着急看代码,因为这里还涉及另外一个问题:delay。
我们知道Handler可以send一个延时消息,那么这个延时是如何处理的呢?
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
可以看到会将当前时间+延长时间计算出消息的发送时间(为什么是发送时间不是执行,后面会讲解),我们知道handler其实还有postAtTime()
函数,剩下的逻辑就是一样的了。
下面调用sendMessageAtTime,代码:
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
...
return enqueueMessage(queue, msg, uptimeMillis);
}
没什么,直接调用enqueueMessage:
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
...
return queue.enqueueMessage(msg, uptimeMillis);
}
这里可以看到先将当前的handler赋值给msg的target,这就是上面提到的,所以每个message都持有handler对象,是一对一的关系。
然后调用了MessageQueue的enqueueMessage函数将msg入队。
boolean enqueueMessage(Message msg, long when) {
...
synchronized (this) {
...
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
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;
}
可以看到MessageQueue实际上是一个链表,每当加入一个msg时,会根据它的when插入到相应的位置,并不是每次都放到队尾。
最后执行了nativeWake(mPtr);
记住这个函数,很重要,后会详细解释。
上面仅仅是得到了MessageQueue会根据时间排序的结论,但是如果looper取出一个msg发现还没到when的时间怎么办?
其实根据第3小节,我们应该可以知道,并不是looper来处理(因为loop()函数没有相关代码),那么就是MessageQueue的next()函数处理的。因为它是取出消息。代码:
Message next() {
...
for (;;) {
...
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;
}
// 如果线程关闭(退出looper)了才返回null
if (mQuitting) {
dispose();
return null;
}
...
}
...
}
}
可以看到这里面也有一个for (;;) {}
,也是一个循环。
先看if (now < msg.when) { } else {}
这段代码,很明显将msg的when与当前时间进行比较,如果没到时间,则给nextPollTimeoutMillis
设置为剩余的时间;否则返回当前msg(队列头部)。
没到时间设置nextPollTimeoutMillis
之后呢?
因为是一个循环,所以开始下一轮,这样就执行到了nativePollOnce(ptr, nextPollTimeoutMillis);
这里就是delay和休眠的关键。这个函数会阻塞当前线程一段时间nextPollTimeoutMillis
,这样就释放了CPU资源,时间到后重新唤醒线程继续执行,所以讲解loop()函数时说next()会导致阻塞。
因为nativePollOnce(ptr, nextPollTimeoutMillis)
的存在,delay得以实现,因为nextPollTimeoutMillis是剩余时间,所以唤醒时正好是当前msg的when。
那么如果队列中没有msg呢?
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
根据代码可以看到如果没有任何msg,那么nextPollTimeoutMillis = -1;
这时再执行nativePollOnce(ptr, nextPollTimeoutMillis);
就会一直阻塞,没有超时时间,这样就不会一直占用CPU资源。
但是什么时候唤醒呢?
还记得上面讲解enqueueMessage提到的nativeWake(mPtr);
么,这个就是唤醒线程的,所以当有新msg加入时就会唤醒继续执行,这样就保证了没msg时休眠,有msg时立刻执行。
这里还有另外需要注意的情况。即delay休眠过程中加入新消息的情况。
当我们队列头部的消息是一个延时msg,这时进入休眠。然后我们加入了一个新消息,是即时msg,那么它就会排在延时msg前面,需要立刻执行,但是现在正处于延时的休眠状态,这时候nativeWake(mPtr)
就会立刻唤醒线程处理,这样就避免了因为delay导致即时消息无法立刻执行的问题。
MessageQueue就是通过这样的方式实现了delay,同时也实现了looper的休眠,防止持续占用CPU资源。
最后看下面的一段代码:
if (mQuitting) {
dispose();
return null;
}
只有当线程退出(或手动结束looper)时,才会返回null。根据loop()函数的代码可以知道,当next返回null,looper的循环就退出了,不会再处理任何消息了。
delay的到底是什么时间?
上面我们留了一个问题,delay通过计算当前时间+延长时间得到的是发送时间而不是执行时间,这又是什么意思?难道我们postDelay(runnable, 3000)并不是在3秒后执行?
实际上确实是这样,比如我们队列中有多个即时消息,这时我们发送一个即时消息,它的时间一定是now,但是因为前面有其他即时消息,所以轮到它执行的时候时间一定大于now了。
所以msg的when并不是执行时间,而是发送时间。
测试一下,有如下代码:
Handler handler = new Handler();
for(int i = 0; i < 10; i++){
handler.post(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
Log.e("post", "run now");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
Log.e("post", "send delay");
handler.postDelayed(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
Log.e("post", "run delay");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 3000);
循环向MessageQueue中放入10个耗时1秒的即时消息,然后放入一个延时3秒的消息,我们看一下执行的日志情况:
10-11 11:17:38.415 26225-26225/com.example.myapplication E/post: send delay
10-11 11:17:39.442 26225-26225/com.example.myapplication E/post: run now
10-11 11:17:40.450 26225-26225/com.example.myapplication E/post: run now
10-11 11:17:41.450 26225-26225/com.example.myapplication E/post: run now
10-11 11:17:42.450 26225-26225/com.example.myapplication E/post: run now
10-11 11:17:43.451 26225-26225/com.example.myapplication E/post: run now
10-11 11:17:44.452 26225-26225/com.example.myapplication E/post: run now
10-11 11:17:45.454 26225-26225/com.example.myapplication E/post: run now
10-11 11:17:46.456 26225-26225/com.example.myapplication E/post: run now
10-11 11:17:47.457 26225-26225/com.example.myapplication E/post: run now
10-11 11:17:48.457 26225-26225/com.example.myapplication E/post: run now
10-11 11:17:49.740 26225-26225/com.example.myapplication E/post: run delay
可以看到在执行postDelay后,并不是3秒后执行了延时消息,而是前面10个即时消息都执行完才执行的。
而且也不是在第10个即时消息执行完等3秒执行,而且立刻就执行了延时消息。
其实这部分在上面MessageQueue的enqueueMessage函数代码中已经体现了。只是与我们平时的直观印象不一致,所以单独拿出来再说一下。
delay的是发送时间这个如何理解?
虽然在执行postDelay后消息是立刻放入MessageQueue的,但是与3秒后执行post的效果是一样的。 从入队角度来看,虽然先放入MessageQueue,但是如果后续的消息when小于它的时间,一样会排到前面。
从执行角度来看,如果它前面没有其他消息,此时未到时间,也不会执行它,会休眠到时间后再执行。
所以这与3秒后再发送的效果是一样,所以说delay的是发送时间,而真正的执行时间则不固定,会依赖更早的消息执行时间。
同步异步消息(同步屏障机制)
Handler机制中的消息其实是有分类的:同步消息和异步消息。
可以通过Message的isAsynchronous
函数来获取是否是异步消息,如下:
public boolean isAsynchronous() {
return (flags & FLAG_ASYNCHRONOUS) != 0;
}
可以通过setAsynchronous
将消息设置为异步消息,如下:
public void setAsynchronous(boolean async) {
if (async) {
flags |= FLAG_ASYNCHRONOUS;
} else {
flags &= ~FLAG_ASYNCHRONOUS;
}
}
那么为什么有这两种消息?它们的区别是什么?
通常我们使用的都是同步消息,而异步消息是由ViewRootImp发出的(requestLayout
中),所有的UI绘制消息都是异步消息。也就说当要执行绘制(无论是否手动),都会发一个异步消息。
那么为什么UI绘制要使用异步消息?这是为了能优先处理这些消息,保证页面流畅。这部分逻辑也体现在MessageQueue的next()函数中,上面我们其实只解读这个函数的一部分代码,其实就是同步消息的处理过程,这次我们来看看被我们忽略的代码:
Message next() {
...
for (;;) {
...
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 && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
//这部分就是上面分析过的
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next; //注意这里,只是丢弃了一个消息(异步消息),但是mMessages没变
} 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;
}
...
}
...
}
}
可以看到,在我们上面分析的代码前面还有一部分逻辑,单独列出来看看:
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
首先如果当前msg的target是null,那么就进入这个流程。但是我们知道,无论通过sendMessage还是postDelay(最终还是调用sendMessageXXX函数)方式,target都是handler对象,不可能为null。那么这里何时为null?
因为异步消息不是给开发者使用的,所以我们很少接触。上面也说过了它们的使用一般是在ViewRootImp的requestLayout
中,最后会执行scheduleTraversals
方法,如下:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
}
首先看mHandler.getLooper().getQueue().postSyncBarrier()
,这是MessageQueue的postSyncBarrier
函数:
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
private int postSyncBarrier(long when) {
// Enqueue a new sync barrier token.
// We don't need to wake the queue because the purpose of a barrier is to stall it.
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}
可以看到这里向消息队列加入了一个空消息,重点是这个msg的target没有赋值,是空的!
继续看scheduleTraversals
函数的下一行,里面的细节就不一一细说了。实际上就是send了一个有关绘制的消息,但是这个消息通过setAsynchronous
设置为了异步消息。
所以异步消息其实要与SyncBarrier搭配使用。
这个SyncBarrier就是同步屏障!Handler中的同步屏障机制就是通过它实现的。
回到最初的next函数,我们知道了target为null的时机了,当它为null的时候,进入了一个while循环,很简单就是直接找到下一个异步消息。
然后继续执行就进入我们上面分析过的流程,处理这个消息。
那么SyncBarrier和异步消息之间的消息怎么办?
我们来看下那段我们分析过的代码,其中有这样一段:
if (prevMsg != null) {
prevMsg.next = msg.next; //注意这里,只是丢弃了一个消息(异步消息),但是mMessages没变
} else {
mMessages = msg.next;
}
这里prevMsg从上面可以看到,如果没有SyncBarrier和异步消息,它是null,如果有它就是异步消息的上一条消息。
所以,如果没有SyncBarrier和异步消息,mMessages正常指向下一条消息。而如果有的话,仅仅是将异步消息从链表上删除,mMessages并没变,所以后续还是从SyncBarrier后的消息开始执行。
所以消息并没有丢失,而是将异步消息优先执行,这样目的就是保证流畅性。
这里还有一个小问题,有异步消息的情况下mMessages并没变,其实还是指向SyncBarrier,这样下一次不是又进入查找异步消息的流程了么?
其实MessageQueue还有一个函数removeSyncBarrier,就是为了移除它的。至于何时调用,我们其实可以猜到,我也没有去源码里找,总之在执行下一次next前已经将这个SyncBarrier移除,mMessages指向了下一条消息,这样就可以正常执行消息了。
IdleHandler
Handler还有另外一个机制:IdleHandler。
IdleHandler的作用是只有在线程空闲的情况下才执行这些任务,即闲时机制。这个有点类似c#中协程的概念。
IdleHandler是一个回调接口,如下
public static interface IdleHandler {
boolean queueIdle();
}
可以通过MessageQueue的addIdleHandler
添加实现类。当MessageQueue中的任务暂时处理完了(没有新任务或者下一个任务延时在之后),这个时候会回调这个接口,返回false,那么就会移除它,返回true就会在下次message处理完了的时候继续回调。
public void addIdleHandler(@NonNull IdleHandler handler) {
if (handler == null) {
throw new NullPointerException("Can't add a null IdleHandler");
}
synchronized (this) {
mIdleHandlers.add(handler);
}
}
通过代码可以看到,addIdleHandler其实就是将IdleHandler添加到一个集合mIdleHandlers中。
那么Looper是如何处理IdleHandler的?如何实现闲时机制?
其实实现代码还是在MessageQueue的next函数中,在这个函数的最后还有一部分代码我们之前没有分析,如下:
Message next() {
...
for (;;) {
...
synchronized (this) {
...
if (msg != null) {
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
...
return msg;
}
} else {
nextPollTimeoutMillis = -1;
}
...
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}
...
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
...
nextPollTimeoutMillis = 0;
}
}
上面我们分析过,在next函数中,如果下一条消息当前可执行,则立刻return给looper执行;如果时间没到,或者没有消息了,则会通过nextPollTimeoutMillis
进行休眠。
其实这并不准确,因为如果没有return的情况下,后续还有一个流程:IdleHandler。
通过上面的代码可以看到,如果没有return,说明目前没有要立刻执行的任务,所以线程是空闲的。这时候就可以处理IdleHandler。
首先判断pendingIdleHandlerCount
,其实就是看是否有IdleHandler需要处理,如果没有直接continue,这样就进入了下次循环,进入了休眠,这就是我们之前分析的流程。
如果有IdleHandler需要处理,就向下执行,首先将mIdleHandlers转成mPendingIdleHandlers,然后遍历mPendingIdleHandlers依次执行idler.queueIdle()
。
这里注意queueIdle的返回值,如果是false则将这个IdleHandler从mIdleHandlers中移除,true则保留,这就是上面提到的。
最后很关键,将nextPollTimeoutMillis重新设置为0。因为这些IdleHandler执行不知道需要多少时间,很可能执行完已经过了下一个消息的时间,所以这里设为0使线程跳过休眠。在下个循环里,如果还没到下个消息的时候,那么可以重新计算时间休眠,如果到了时间直接执行。这样可以及时的执行消息,防止因为执行IdleHandler导致消息无法及时处理。
消息复用
Mesasge有复用机制,即obtain,代码如下:
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
public static Message obtain(Handler h) {
Message m = obtain();
m.target = h;
return m;
}
public static Message obtain(Handler h, Runnable callback) {
Message m = obtain();
m.target = h;
m.callback = callback;
return m;
}
...
可以看到,除了无参函数,还有一系列有参函数,这些有参函数都是先执行无参函数得到一个复用的msg,然后重新设置部分属性而已。所以关注无参函数即可。
在obtain中可以看到,其实是从sPool即消息池中获取一个消息,并重置一下复用,如果池中没有则新建。而sPool实际上是一个链表。
那么sPool中的消息怎么来的?在Message中的回收函数recycleUnchecked中:
void recycleUnchecked() {
...
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
可以看到有一个消息池的限制MAX_POOL_SIZE(默认是50),如果未达到限制,则将其加到尾部;如果达到了则什么也不做。
所以在消息回收的时候,通过判断MAX_POOL_SIZE将其入列,而且消息池只保留50个(所有线程共有,所以需要加同步锁),且一直保持直到应用退出。
所以可以看到消息复用并没有太复杂的逻辑(不像线程池那样),仅仅保留50个留着复用即可。