在Android Handler消息机制中,我在前两篇文章中已经介绍了消息机制中的消息Message和ThreadLocal原理,本篇文章会分析Handler消息机制中的另一个重要的工具,消息队列,在Handler整个消息机制中,每个具体的消息是如何被管理的,同步消息和异步消息是如何存储的?
该系列的其他文章
- Handler消息机制(一)Message复用原理
- Handler消息机制(二)ThreadLocal原理
- Handler消息机制(三)MessageQueue原理
- Handler消息机制(四)Looper原理
- Handler消息机制(五)Handler原理
本篇文章从以下四个部分来详细介绍MessageQueue。
- MessageQueue是什么?
- MessageQueue如何使用?
- MessageQueue原理?
- 什么是同步屏障?
- 同步屏障用法及原理?
1. MessageQueue是什么?
MessageQueue是一个包级私有类,Android消息的管理队列,包含消息的插入和读取功能,其内部是通过单链表的数据结构来维护消息列表,MessageQueue只是负责消息的插入和读取,而单链表在插入和删除上拥有更高的效率,所以采用单链表的结构对消息的维护和管理更加高效。
2. MessageQueue如何使用?
在Android handler消息中,因为MessageQueue是一个包级私有类,我们无法直接去创建MessagQueue对象,所以MessageQueue是在同包下的Looper内部进行创建的,这里只需要知道当我们使用Handler时,每个Handler都必须有一个Looper对象,而在Looper内部会持有MessageQueue消息队列。结合源码看下:
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
在Looper的构造方法中会创建MessageQueue队列,同时会记录当前线程。 MessageQueue内的插入和删除消息的方法,我们同样不能直接调用,而是通过Handler提供给我们的sendMessage系列方法向MessageQueue中插入消息,而删除消息是在Looper内部轮询消息时进行的读取删除操作。
3. MessageQueue原理?
MessageQueue的插入和读取分别对应的是enqueueMessage和next方法。下面我们分析下方法内部的原理。
3.1 插入消息的enqueueMessage方法
boolean enqueueMessage(Message msg, long when) {
// target是handler对象,检查消息是否有线程处理
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
// msg是否已经被使用
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
synchronized (this) {
// 处理消息的线程是否已经退出,如果线程退出,则将消息回收掉,无需进队列
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;
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 {
// 将新消息插入到队列中,同步消息根据when来确定队列位置,如果是同步屏障,同步屏障消息target==null,且新消息是异步的,则needWake为true,则计划唤醒队列。
// 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;
}
// 如果队头的一下个消息也是异步消息,且未到唤醒时间,则将needWake置为false,不唤醒队列
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;
}
首先是对消息进行检验,消息能否被处理和消息是否正在被使用等。msg.target是指Handler对象。当处理消息的线程已经退出,则消息会被回收。 针对同步消息:将时间小的消息放在靠近队头的位置,时间长的消息靠近队尾。 从队头开始,找到新消息在队列中合适的位置,然后将新消息插入到队列中,如果队列为null或新消息的等待时间小于队头消息的等待时间,则直接将新消息放入到队列的头部。如果队头消息的时间小于新消息,则新消息将从队头消息开始依次和队列中的消息进行比较,直到找到合适的位置。
这里特别说明下:同步消息和异步消息只有isAsynchronous()可以区分,即异步标志位区分,异步标志位是通过msg的setAsynchronous(boolean async)方法进行设置,异步和同步消息均是根据when来确定在队列中的位置。
3.2 读取消息的next方法
Message next() {
// 只有当线程退出时,mPtr为null
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
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 && 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) {
// 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;
}
// Process the quit message now that all pending messages have been handled.
if (mQuitting) {
dispose();
return null;
}
...
}
}
在next方法中,当发现队列中有同步屏障时,此时会优先返回队列中的异步消息,从队列中获取异步消息,并将该异步消息从队列中移除,如果同步屏障不被移除,即使异步消息被处理完毕,同步消息也不会被处理,队列会进入阻塞状态。 如果队列中没有同步屏障,则从队列中获取同步消息,并将该同步消息从队列中移除。
同步屏障下,异步消息的处理示意图:
4. 什么是同步屏障?
从名称上来看,就是将同步消息屏蔽的意思,当队列中包含同步屏障消息时,整个消息队列会进入优先处理异步消息的状态,同时同步消息会被屏蔽。 在说同步屏障时,需要区分清楚MessageQueue队列中的消息类型,方便理解同步屏障,MessageQueue队列中可以分为三种消息,屏障消息,同步消息和异步消息。 屏障消息: target为null的Message对象,即没有消费者的消息,该消息仅用来标记当前队列优先处理异步消息。 同步消息: target不为null的Message对象,即可以被消费的消息,且该消息的isAsynchronous方法返回false。 异步消息: target不为null的Message对象,和同步消息的区别是isAsynchronous方法返回true。 异步消息需要用户手动的将Message标记为异步,通过方法setAsynchronous(true)将消息标记为异步。 清楚了MessageQueue中的消息类型,我们再来看什么是同步屏障,就更容易理解了,同步屏障就是将同步消息屏蔽掉,只处理异步消息,这就是同步屏障的作用。
5. 同步屏障用法及原理?
5.1 同步屏障的作用我们理解了,如何使用同步屏障呢?
在MessageQueue类中有两个方法,用来添加和删除同步屏障,分别为postSyncBarrier()和removeSyncBarrier()。 通过Handler -> Looper -> MessageQueue,然后使用其方法即可,其实这两个方法的使用场景基本不可见,因为这两个方法被声明为hide方法,相当于系统自己留的后门,优先处理高优先级的任务,所以我们在调用时会出错,结合源码看,该方法只在Android的ViewRootImpl中被调用了,主要用来处理绘图操作,后续再去仔细分析ViewRootImpl的源码。
5.2 同步屏障的原理?
上面分析的插入消息我们知道,同步消息和异步消息的入队均是通过when的大小来确定消息在队列中的位置,在读取消息时,队列通过消息的msg.target==null来判断同步屏障。如果队列有同步屏障消息,则优先处理异步消息(即msg.isAsynchronous()返回true),直到异步屏障被清理,异步消息处理顺序一样是通过when大小来确定。如果不包含同步屏障,则处理同步消息,根据when大小来确定消息的处理顺序。 这里声明下:when大小是不会相同的,因为消息的入队是顺序的,所以根据入队时的时间计算出的when大小是不同的。
5.2.1 设置同步屏障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++;
// msg.target == null,由此判定该消息是同步屏障消息
// msg.arg1 == token,arg1参数记录同步屏障消息的token数据
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;
}
// token,用来删除同步屏障
return token;
}
}
5.1.2 删除同步屏障removeSyncBarrier方法
public void removeSyncBarrier(int token) {
// 通过token或targe == null来查找同步屏障消息
// Remove a sync barrier token from the queue.
// If the queue is no longer stalled by a barrier then wake it.
synchronized (this) {
// 查找同步屏障消息,即p.target == null 或 p.arg1 == token的消息
Message prev = null;
Message p = mMessages;
while (p != null && (p.target != null || p.arg1 != token)) {
prev = p;
p = p.next;
}
// 队列没有同步屏障消息,抛出异常
if (p == null) {
throw new IllegalStateException("The specified message queue synchronization "
+ " barrier token has not been posted or has already been removed.");
}
// 重新构建移除同步屏障的消息队列
final boolean needWake;
if (prev != null) {
prev.next = p.next;
needWake = false;
} else {
mMessages = p.next;
needWake = mMessages == null || mMessages.target != null;
}
// 同步屏障消息被回收到Message的Pool内
p.recycleUnchecked();
// If the loop is quitting then it is already awake.
// We can assume mPtr != 0 when mQuitting is false.
if (needWake && !mQuitting) {
nativeWake(mPtr);
}
}
}
删除同步屏障通过我们插入同步屏障返回的token或者target==null来识别的。
同步屏障在使用完毕后一定要清理,否则同步消息将无法被处理。
结余 MessageQuene是维护handler消息的队列,提供了插入和读取消息的功能,可以通过同步屏障的方式,来处理高优先级的消息。