引言
Android的消息机制,就是Handler、Looper、Message、MessageQueue之间的运作机制。本文假设大家对 它们都已经有所了解,并不打算介绍它们之间千丝万缕的联系,不了解的同学可以参考网上其他博文。
这其中有个小细节,估计很多人没有注意到,那就是 Message 的 target, 当target == null
时有没有什么特殊含义?与 target 不为 null 的区别在哪里?这篇文章就是要揭开 Message 之 target 的面纱。
target 为何物
首先,看下 Message 类中 target 的定义:
Handler target;
复制代码
从这我们可以知道 target 即为 Handler对象。
让我们再看看 target 是哪里出现的?
我们知道,在发送消息的时候(如调用Handler#sendMessage()
),最终都是会走到 Handler#enqueueMessage()
,如下:
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
复制代码
看到没,当我们发送一个消息的时候,msg.target
就会被赋值为this
, 而 this 即为我们的 Handler对象。因此,通过这种方式传进来的消息的 target 肯定也就不为 null,并且 mAsynchronous 默认为 false,也即为同步消息。相对地,也应该有异步消息吧?的确还有一种很容易被忽略的异步消息,因为我们很少使用它。那么,如何发送异步消息呢?
简单来说有两方式种。一种是直接设置消息为异步的:
Message msg = mMyHandler.obtainMessage();
msg.setAsynchronous(true);
mMyHandler.sendMessage(msg);
复制代码
还有一个 需要用到 Handler 的一个构造方法,不过该方法已被标记为@Hide
了:
/**
*
* @hide
*/
public Handler(boolean async) {
this(null, async);
}
复制代码
使用如下:
Handler mMyHandler = new Handler(true);
Message msg = mHandler.obtainMessage();
mMyHandler.sendMessage(msg);
复制代码
参数 async
传 true
即为异步消息。
但是,通过上面两种方式来发送的消息还不是异步消息,因为它们最终还是会进入到 enqueueMessage()
给 target 赋值 ,导致 target 不为 null。这与前面所说的普通的同步消息无异。那么什么情况下会满足target == null
呢?
咱们今天的主角,同步屏障 (Sync Barrier) 就要登场了。
内存屏障是什么
没错,发送异步消息的关键就是要消息开启同步屏障。屏障的意思即为阻碍,同步屏障就是阻碍同步消息,只让异步消息通过。如何开启同步屏障呢?这样:
MessageQueue#postSyncBarrier()
复制代码
我们看源码里有什么黑科技:
/**
*
* @hide
*/
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();
//就是这里!!!初始化Message对象的时候,并没有给target赋值,因此target=null
msg.when = when;想
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
//如果开启同步屏障的时间(假设记为T)T不为0,且当前的同步消息里有时间小于T,则prev也不为null
prev = p;
p = p.next;
}
}
/根据prev是不是为null,将 msg 按照时间顺序插入到 消息队列(链表)的合适位置
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}
复制代码
可以看到,Message 对象初始化的时候没有给 target 赋值,因此, target == null
的 来源就找到了。上面消息的插入也做了相应的注释。这样,一条target == null
的消息就进入了消息队列。
那么,开启同步屏障后,所谓的异步消息又是如何获取到的呢?
如果对消息机制有所了解的话,应该知道消息的处理最终都是在MessageQueue#next()
中,将里面的代码拿出来:
Message next()
.....//省略
int pendingIdleHandlerCount = -1; // -1 only during first iteration
// 1.如果nextPollTimeoutMillis=-1,一直阻塞不会超时。
// 2.如果nextPollTimeoutMillis=0,不会阻塞,立即返回。
// 3.如果nextPollTimeoutMillis>0,最长阻塞nextPollTimeoutMillis毫秒(超时)
// 如果期间有程序唤醒会立即返回。
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
//获取系统开机到现在的时间
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages; //当前链表的头结点
//如果target==null,那么它就是屏障,需要循环遍历,一直往后找到第一个异步的消息
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) {
//如果有消息需要处理,先判断时间有没有到,如果没到的话设置一下阻塞时间,
//场景如常用的postDelay
if (now < msg.when) {
//计算出离执行时间还有多久赋值给nextPollTimeoutMillis,
//表示nativePollOnce方法要等待nextPollTimeoutMillis时长后返回
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// 获取到消息
mBlocked = false;
//链表操作,获取msg并且删除该节点
if (prevMsg != null)
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
msg.markInUse();
//返回拿到的消息
return msg;
}
} else {
//没有消息,nextPollTimeoutMillis复位
nextPollTimeoutMillis = -1;
}
.....//省略
}
复制代码
从上面可以看出,当消息队列开启同步屏障的时候(即标识为msg.target == null
),消息机制会通过循环遍历,优先处理异步消息。这样,同步屏障就起到了一种过滤和优先级的作用。
下面用示意图简单说明:
如上图所示,在消息队列中有同步消息和异步消息(黄色部分)以及一道墙----同步屏障(红色部分)。有了内存屏障的存在,msg_2这个异步消息可以被处理,而后面的 msg_3等同步消息不会被处理。那么什么时候这些同步消息可以被处理呢?那就需要移除这个内存屏障,调用removeSyncBarrier()
即可。
举个栗子。开演唱会的时候,观众们都在体育馆门口排队依次等候检票入场(相当于消息队列中的普通消息),这个时候有一大波工作人员来了(相当于异步消息,优先级高于观众),如果他们出示工作证(不出示工作证,就相当于普通观众入场,也还是需要排队,这种情形就是最前面所说的仅仅设置了msg.setAsynchronous(true)
),保安立马拦住(出示工作证就拦住就相当于开启了同步屏障)进场的观众,先让工作人员进去(只处理异步消息,而过滤掉同步消息)。等工作人员全部进去了,保安不再阻拦观众(即移除内存屏障),这样观众又可以进场了。只要保安不解除拦截,那么后面的观众就永远不可能进场(不移除内存屏障,同步消息就不会得到处理)。
内存屏障使用场景
似乎在应用开发中,很少用到内存屏障。那么,同步屏障有什么哪些使用场景呢?
比如,在View更新时,draw、requestLayout、invalidate等很多地方都调用了ViewRootImpl#scheduleTraversals()
,如下:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//开启内存屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//发送异步消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
复制代码
postCallback()
最终走到了ChoreographerpostCallbackDelayedInternal()
:
最后,当要移除内存屏障的时候需要调用ViewRootImpl#unscheduleTraversals()
。
void unscheduleTraversals() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除内存屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
mChoreographer.removeCallbacks(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
复制代码
总结
当我们调用mHandler.getLooper().getQueue().postSyncBarrier()
时,target 即为 null ,即开启了同步屏障。当消息队列 MessageQueue处理消息时,如果开启了内存屏障,会过滤同步消息而优先处理其中的异步消息。