Android 消息屏障与异步消息

  1. 如何插入一个消息屏障?
  2. 如何删除一个消息屏障?
  3. 如何插入一个异步消息?
  4. 消息屏障对插入消息有什么影响?
  5. 消息屏障是如何优先处理异步消息的?
  6. Framework 中哪里使用了消息屏障?

Android 消息机制中的 MessageQueue 可以存放三种类型的消息,普通消息、消息屏障和异步消息。其中消息屏障和异步消息搭配使用,可以达到屏蔽普通消息、优先处理异步消息的目的。

1. 如何插入一个消息屏障?

见 MessageQueue 的 postSyncBarrier 方法:

1
2
3
4
5
6
7
8
9
10
11
private int postSyncBarrier(long when) {
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
//按时间排序插入到队列中...
return token;
}
}
  1. 消息屏障不需分发处理,没有 target Handler,后续也会根据有无 target 来判断是否为消息屏障
  2. 消息屏障也是有时间戳的,并且只会对后面的消息起到屏障作用,不会影响前面的消息
  3. 消息屏障插入后无需唤醒线程,因为消息屏障原本的目的就是打算屏蔽消息处理的
  4. 插入后会返回一个 token,用于后续移除指定 token 的消息屏障
  5. 方法为 private,外部调用需反射

2. 如何删除一个消息屏障?

见 MessageQueue 的 removeSyncBarrier 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void removeSyncBarrier(int token) {
synchronized (this) {
Message prev = null;
Message p = mMessages;
while (p != null && (p.target != null || p.arg1 != token)) {
prev = p;
p = p.next;
}
//从队列中删除这个消息屏障...
if (needWake && !mQuitting) {
nativeWake(mPtr);
}
}
}
  1. 根据无 target 及 token 匹配找到对应的消息屏障
  2. 删除屏障后可能需要唤醒线程,是否唤醒取决于当前是否是因为消息屏障而阻塞的

3. 如何插入一个异步消息?

Message 的 setAsynchronous 为开放 API,直接调用设置即可,比如在 ViewRootImpl 中对输入事件的处理:

1
2
3
4
5
6
7
8
public void dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
SomeArgs args = SomeArgs.obtain();
args.arg1 = event;
args.arg2 = receiver;
Message msg = mHandler.obtainMessage(MSG_DISPATCH_INPUT_EVENT, args);
msg.setAsynchronous(true); //异步消息
mHandler.sendMessage(msg);
}

由于输入事件需要快速响应,优先级比较高,所以设置为异步消息,避免被消息屏障屏蔽掉

4. 消息屏障对插入消息有什么影响?

见 MessageQueue 的 enqueueMessage 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//省略部分代码...
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;
prev.next = msg;
}
if (needWake) { //唤醒
nativeWake(mPtr);
}
  1. 如果插入到队列头部,那么只要当前线程是休眠的,就要唤醒,不管有没有消息屏障,因为消息屏障不会影响在它之前的消息
  2. 如果插入到队列中间且队列头消息为消息屏障,那还要判断插入的消息是不是最早的异步消息,如果是才唤醒线程。因为如果之前已经有异步消息,那说明已经对之前的异步消息做过唤醒或休眠指定时间的处理了,不用再此唤醒

5. 消息屏障是如何优先处理异步消息的?

见 MessageQueue 的 next 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Message next() {
//省略部分代码...
for (;;) {
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
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());
}
//省略部分代码...
}
  1. 如果当前消息不是消息屏障,那异步消息和普通消息无异,都会按照时间排序依次执行
  2. 如果当前消息为消息屏障,就会去找队列中的异步消息,如果没有异步消息,就无限休眠;如果有,就根据这个异步消息的处理时间去分发处理或休眠

6. Framework 中哪里使用了消息屏障?

ViewRootImpl 中界面绘制时使用了消息屏障:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//插入一个消息屏障,屏蔽普通消息的处理
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}

void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除消息屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
}
}

与输入事件一样,界面绘制也是优先级高的消息,需要优先处理,所以这里插入消息屏障 block 其他普通消息,以达到优先处理界面绘制的目的。