面试官:如何提高Message的优先级?
前言
在日常的开发中有的场景需要我们自己的Message尽快被执行,这时候就需要提高Message的处理优先级。想解决这个问题,就需要对MessageQueue的构造有一定的了解,MessageQueue通过一个链表的结构,根据Message的参数when的大小进行排序,将系统以及我们自己发出的所有Message串起来,when的值越小那么此Message在链表中的位置越靠前,当Looper需要处理一个Message的时候,就会从链表中找出一个符合时间要求的Message并分发处理。基于这个构造很容易想到的一种方法就是将Message插在列表的头部,那么这个Message肯定会被最优先处理。还有另外方法思路也可以间接的提高Message优先级:同步消息屏障和异步消息。
一、将Message插在列表的头部
MessageQueue通过一个链表的结构,根据Message的参数when的大小进行排序并连接起来,when越小那么在链表中的位置就越靠前,那首先弄明白的就是when代表的是什么?首先来看看我们用一个Handler发送一个Message的时候,when是什么
//Handler.java
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;
return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
Handler#sendMessage方法其实是调用了sendMessageDelayed方法,并且 delayMillis 传为 0. 当 delayMillis 小于 0 的时候,会默认赋值为0,所以当我们调用sendMessageDelayed方法时,如果传入的delayMillis为负数,其实最终会变成0. 然后 SystemClock.uptimeMillis() + delayMillis 作为参数uptimeMillis继续向下传递,最终调用queue.enqueueMessage(msg, uptimeMillis)。其实Message中的when就是 SystemClock.uptimeMillis() + delayMillis。 SystemClock.uptimeMillis()返回的是手机开机后的累积时间(单位是毫秒),delayMillis是希望延迟处理的时间。
所以我们正常的使用Handler发送Message时,when的数值一定是一个大于0的数。
回到我们的问题,想要Message的优先级变高,那就要想办法把Message插入到链表的头部,Handler还提供了以下的方法:
//Handler.java
public final boolean sendMessageAtFrontOfQueue(@NonNull Message msg) {
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, 0);
}
这个方法很明显了,最后调用enqueueMessage的时候,直接将uptimeMillis置为0,这样message就排到链表的头部了。不过此时又有问题了,如果我在插入的时候,头部已经有一个when=0的Message了,这个时候怎么办?其实MessageQueue已经想到了,只要我插入的Message的when为0,那么不管此时链表是什么,都直接插入到头部。
//MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
...
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
//只要when=0,直接插入到链表的头部
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
}
...
}
二、同步消息屏障和异步消息
另外一种思路就是可不可以在遍历链表的时候,跳过前边那些优先级不是那么高的Message,而定位到需要优先处理的Message,这样是不是也到达了提高Message优先级的效果?答案是肯定的。同步消息屏障和异步消息就是为了这个而存在的。
同步消息
:我们平时使用Handler发送的Message大都是同步消息,此处的 “同步” 和多线程同步不是一个概念,只是对消息的一种区分。同步Message的isAsync参数为false。
异步消息
:和同步消息唯一的区别就是isAsync参数为true。
同步消息屏障
:首先他也是一个Message消息对象,但是他的target参数为null(这是屏障消息和普通唯一的区别)。屏障的意思就是屏蔽掉,所以同步消息屏障顾名思义就是屏蔽掉同步消息,帮助我们获取异步消息。
解释完这三种概念,我们来看一张图,直观的讲一下同步消息屏障和异步消息是如何配合工作的。
如图,三个消息组成一个MessageQueue,三个消息按照when的的数值顺序排列。在正常情况下当我们遍历链表的时候,由于同步消息A排在异步消息B的前边,所以同步消息A的优先级肯定是要高于异步消息B的。此时,同步消息屏障就发挥它的作用了。
当我们遍历这个链表时,首先发现第一个消息是一个同步消息屏障,此时我们跳过这个屏障沿着链表继续向下遍历,然后找到同步消息A,由于前边已经发现有同步消息屏障了,所以同步消息A被屏蔽了,我们继续向下遍历。最终找到异步消息B,并将异步消息B从链表总分离出来,进行后续的消息分发处理。这就是他的整个过程了!!!我们没有将消息B移动到链表的头部,但是消息B的优先级却提高了(比消息A先执行)。
下边来看看具体的实现代码吧:
#MessageQueue.java
@UnsupportedAppUsage
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
private int postSyncBarrier(long when) {
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;
}
}
发送同步消息屏障,需要使用MessageQueue#postSyncBarrier方法,但是这个是对我们隐藏的,我们是不能直接调用的(想调用的话只能通过反射),看代码可以知道屏障消息就是一个普通的Message对象,只不过这个Message的target没有初始化所以为null。并且这个Message对象的when就是SystemClock.uptimeMillis(),也就是说屏障消息在插到消息队列的时候也需要遵循按照when的大小排序。
最后看一下MessageQueue#next方法,看一下从消息队列中取消息时是如何处理屏障消息和异步消息的:
//MessageQueue.java
Message next() {
...
Message prevMsg = null;
Message msg = mMessages;
//如果msg的target==null,则认为这个msg是同步消息屏障
if (msg != null && msg.target == null) {
// 由于msg是同步消息屏障,所以继续向后遍历,寻找异步消息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
...
return msg;
}
总结
这就是两种提高Message优先级的思路了。但是第二种同步消息屏障的方式,源码我们是不能直接调用的,说明系统不想让我们用。那既然不想让我们用这个api又为什么存在呢?其实同步消息屏障api被应用在屏幕刷新机制中了。因为屏幕刷新是最重要的消息,如果他的消息如果不能及时处理就会出现屏幕卡顿。所以安卓系统通过这个机制来提高刷新屏幕消息的优先级。那他为什么不开放给我们开发者使用呢?交给你自己思考了哈哈哈哈。