Android 消息机制Handler完全解析(一)
Android 消息机制Handler完全解析(二)
前面两篇我们主要讲了Handler消息机制的一些基础,今天来看下消息屏障,通过本篇文章你将学到如下知识点
(1)什么是同步屏障
(2)为什么要有同步屏障
(3)同步屏障的原理
(4)Android FrameWork中哪里用到了同步屏障
(5)使用反射机制调用FrameWork层的发送屏障消息的方法手写一个demo
一、什么是同步屏障,为什么会有同步屏障?
想搞明白这个问题,我们需要对Android的UI刷新有点了解,以电影为例,动画至少要达到24FPS,才能保证动画的流畅性,低于这个值肉眼会感觉到卡顿,在手机上这个值被调整到60FPS,60FPS意思就是每秒播放60帧画面,那么每帧画面的时间间隔为1000 /60=16ms,这也是为什么有个16ms的指标,也就是说一般android中UI刷新的间隔是16ms。UI的刷新也是通过Handler发送消息进行的,并且UI刷新的消息是优先级最高的,否则当消息比较多时手机上的画面就会卡顿,这一点大家应该很容易想明白。通过前面两篇文章我们知道Handler的消息会被发送到MessageQueue中并且会按照时间进行排序,那么问题来了,当有比较紧急的消息到来时怎么能让它优先执行呢?比如MessageQueue中已经有100个Message排好了队,这时UI刷新消息到来怎么能让这个UI刷新的消息优先执行呢?这里就用到了我们的消息屏障。
对比现实生活中的例子就是120可以闯红灯,这是一个紧急信号,当救护车的声音响起时它就有特权插队,它的优先级是最高的因为生命是最重要的。
所以同步屏障是一种同步机制,用于确保某些操作按照预定的顺序执行,在Handler中是一种优先级策略它会让异步消息的优先级高于同步消息。为什么会有同步屏障呢?是因为在Android中有些的消息(UI刷新的消息)的优先级是最高的,当此类消息到来时我们要确保此类消息能够立即执行
二、三种消息类型是如何产生的
刚才我们提到同步屏障会让异步消息的优先级高于同步消息,我们首先来看下在Handler消息机制中有几种消息,共有三种消息分别为:
- 同步消息
- 异步消息
- 屏障消息
在MessageQueue这种数据结构中存储方式如下图
这三种消息都是怎么来的呢?接下来我们就通过源码来具体分析下,首先同步消息在前面两篇文章中我们讲的都是同步消息,平时我们开发中使用Handler发送的消息大部分也都是同步消息。因此我们主要看下屏障消息和异步消息。
屏障消息
屏障消息怎么来的呢?在MessageQueue中提供了此方法,通过调用系统的postSyncBarrier方法会发送一条屏障消息。
这里有一点需要注意:系统的postSyncBarrier方法是添加了@hide的public方法,谷歌不建议开发者使用此方法,因为此方法使用不当很容易出问题,在后面我们会看在FrameWork中哪里用到了此方法。然后通过一个demo反射调用此方法大家就会对此有比较深入的理解。
public final class MessageQueue {
/**
* @hide
*/
@UnsupportedAppUsage
@TestApi
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) {
prev = p;
p = p.next;
}
}
// 将message插入到MessgeQueue中
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}
}
这里的关键就是在初始化Message对象的时候没有给Message的target赋值,即target=null,这一点非常重要,即屏障消息的target为null
同时重置msg的when和arg1对象,将msg.arg1的值设置为token。
还记得我们自己发送的Handler消息的target是什么吗?它的源码如下
public class Handler {
...
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);
}
...
}
在这里msg.target=this就是将handler对象赋值给msg.target,这种就是同步消息,到这里大家应该能区分屏障消息和非屏障消息了,主要就是看msg.target是否为null。
异步消息
什么样的消息是异步消息呢?异步消息可以通过Message的setAsynchronous(true)进行设置,也可以通过Handler的构造方法进行设置
public class Handler {
/**
* @hide
*/
public Handler(@Nullable Callback callback, boolean async) {
...
mAsynchronous = async;
...
}
...
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的构造方法中传递async为true时,在调用Handler的enqueueMessage时会调用msg.setAsynchronous(true)将此消息设置为异步消息,本质还是调用Message的setAsynchronous方法。
到这里同步消息、异步消息、屏障消息这三种消息的区别大家应该都清楚了吧
同步消息: Message的asynchronous为false且msg.target != null
异步消息: Message的asynchronous为true且msg.target != null
屏障消息: msg.target == null
三、屏障消息是如何实现插队的
在了解了三种消息的区别之后,我们来看下同步屏障是怎么实现优先级高的消息先执行的,这里要从取消息的方法看起,我们知道MessageQueue有一个enqueueMessage用来将消息按照时间顺序插入到MessageQueue中,还有一个next()方法用来取消息,next方法的源码如下(为了阅读方便省略了部分代码)
@UnsupportedAppUsage
Message next() {
...
for (;;) {
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) { // 注释1
// Stalled by a barrier. Find the next asynchronous message in the queue.
// 先执行do,再执行while,遇到同步消息会跳过,遇到异步消息退出循环
// 即取出的msg为该屏障消息后的第一条异步消息,屏障消息不会被取出
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) { // 注释2
// 移出异步消息
prevMsg.next = msg.next;
} else {
// 移出同步消息
mMessages = msg.next;
}
msg.next = null;
msg.markInUse();
return msg;
}
}
...
}
这里重点来了,可以看到第7行如果msg != null && msg.target == null则会进入到一个do…while循环,这个while循环结束的条件就是msg==null 或者msg.isAsynchronous()为true即异步消息,如果是同步消息的话就会继续往后找,因此这里就会跳过同步消息直到找到链表中的第一个异步消息然后跳出while循环进入到下面的if条件中,对于prevMsg需要特别注意它是指向异步消息的前一个节点的Message对象,最开始的时候prevMsg的值为null,只有进入到“注释1”即第7行的if语句中时才会被赋值并且不为null,所以当找到异步消息退出while循环时会进入到"注释2"即第23行的if语句中将此异步消息移除然后将其返回。如果你不是特别清楚,我画个图你可能会理解的更加深入
假如当前的MessageQueue是如下情形
即链表的表头是mMessages指向同步消息msg1,因为msg1不是屏障消息所以不会进入到第7行的if语句中而是直接进入到第16行的if语句中,假如到了此消息的执行时间则会进入到第26行的分支将msg1移除,此时MessageQueue的情形如下
可以看到执行了mMessages=msg.next之后将同步消息msg1移除了,接着继续取消息,由于msg2是屏障消息因此会进入到第7行的if语句并进入到do…while循环首先会把链表的第一个Message赋值给prevMsg,然后msg=msg.next遍历链表直到找到异步消息退出while循环,这里也就是找到msg5。msg3和msg4这两条同步消息会被直接跳过。此时的情形如下
然后执行第25行移除异步消息的语句这也是单链表移除某个节点的常规操作即prevMsg.next = msg.next执行之后的情形如下
这样就把异步消息移除并且返回了,到这里不知道大家有没有发现一个问题,就是上述这种情形,每次进入到第4行的for(;;)循环之后都会进入到第7行的if语句中,因为链表的第一个元素始终都是屏障消息,这样是不是会导致同步消息永远执行不到?是的会有这样的问题,因此当我们插入一条消息屏障之后一定要记得将其移除,也就是说我们的屏障消息让优先级高的消息执行完之后要将其移除掉,然后去执行优先级低的消息。就好比120救护车来了时其它车辆收到一条消息(当前有紧急任务要执行)要给其让道,当120救护车行驶过之后(当前有紧急任务要执行)这条消息被移除,其它车辆就可以正常行驶了。问题来了消息屏障是怎么移除的呢?插入消息屏障我们会调用postSyncBarrier同理移除消息屏障系统也为我们提供了方法removeSyncBarrier源码如下
/**
* @hide
*/
@UnsupportedAppUsage
@TestApi
public void removeSyncBarrier(int token) {
// Remove a sync barrier token from the queue.
// If the queue is no longer stalled by a barrier then wake it.
synchronized (this) {
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) {
// 删除屏障消息p
prev.next = p.next;
needWake = false;
} else {
mMessages = p.next;
needWake = mMessages == null || mMessages.target != null;
}
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);
}
}
}
删除屏障消息的方法很简单,就是不断遍历消息链表,直到找到屏障消息,退出第13行while循环的条件有两个p.target == null(说明是屏障消息)和p.arg1 == token(说明p是屏障消息,在屏障消息入队的时候,设置过msg.arg1 = token)。找到屏障消息后,把它从消息队列中删除并回收。举个实例看下你就会清楚整个过程,假设当前的是如下情形
此时调用了removeSyncBarrier方法首先会循环遍历找到屏障消息,由于msg2满足p.target==null且p.arg1=token所以会退出第13行while循环,此时情形如下
然后进入到第22行的if语句执行
prev.next = p.next;
这种典型的单链表删除某个节点的代码我们是不是见到很多次了,执行之后的
此时就完成屏障消息的删除操作,如果链表的第一个节点就是屏障消息则更简单,因为第13行的while循环不会进入,会直接进入第26行的分支执行mMessages = p.next;从而将屏障消息删除。到这里相信大家对同步屏障有了了解,接下来通过查看FrameWork使用同步屏障的源码以及自己写的一个demo来让大家对同步屏障的了解更近一步。
四、FrameWork层哪里用到了同步屏障
接下来看下在Android中哪里用到了同步屏障,我们开头就提到了UI刷新的时候是通过Handler发送的消息且它的优先级是最高的,关于UI刷新也包含很多内容后续也会写几篇文章,在这里大家只需要知道UI更新都会调用ViewRootImpl.scheduleTraversals()
方法,它的源码如下
public final class ViewRootImpl {
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 获取到handler的MessgeQueue对象并调用postSyncBarrier方法往队列中插入一个屏障消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 往队列中插入一个异步消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
}
因为UI刷新的消息优先级最高所以在这里插入了一个同步屏障,第7行的postCallback最终会调用Choreographer的postCallbackDelayedInternal这个方法
public final class Choreographer {
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
// 将消息设置为异步消息
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
}
这就往MessageQueue中插入了一条异步消息,因为在前面插入了一个屏障消息因此此异步消息的会优先执行。前面我们说过屏障消息不会自己移除,需要调用相关的方法才能移除,从而让同步消息能够正常执行。Android源码中在执行UI绘制流程之前执行了移除同步屏障的代码
public final class ViewRootImpl {
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 移除屏障消息
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
...
// 这个方法很重要,后面讲UI刷新机制的时候会再次提到,这里执行了UI绘制的三大流程
performTraversals();
...
}
}
}
所以在FrameWork UI刷新时首先会发送一个屏障消息,确保后续的异步消息要优先执行,然后再发送一个刷新UI的异步消息,此消息会优先执行,在UI绘制之前将屏障消息移除,从而确保同步消息的正常运行。
五、使用反射调用FrameWork层的发送屏障消息的方法
接下来我们自己手写一个实例,相信通过这个实例你会对同步屏障有更进一步的认识,前面我们提到屏障消息的发送和移除方法都是@hide的因此要通过反射才能调用,首先我们先看下如何反射调用
private Deque<Integer> mBarrierTokens = new LinkedList<>();
private void addBarrier() {
try {
// 获得发送同步屏障的方法
Method method = MessageQueue.class.getMethod("postSyncBarrier");
method.setAccessible(true);
// postSyncBarrier返回的token
int barrierToken = (int) method.invoke(mHandler.getLooper().getQueue());
mBarrierTokens.add(barrierToken);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
private void removeBarrier() {
if (mBarrierTokens.isEmpty()) {
return;
}
try {
// 获取移除同步屏障的方法
Method method = MessageQueue.class.getMethod("removeSyncBarrier", int.class);
method.setAccessible(true);
int barrierToken = mBarrierTokens.pollLast();
// 取出最新的token做为参数调用removeSyncBarrier方法
method.invoke(mHandler.getLooper().getQueue(), barrierToken);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
我们先在Activity中定义几个Message,然后点击按钮发送消息,代码如下
Message msg1 = Message.obtain(mHandler, new Runnable() {
@Override
public void run() {
Log.d(TAG, "1000");
// 添加屏障
addBarrier();
}
});
Message msg2 = Message.obtain(mHandler, new Runnable() {
@Override
public void run() {
Log.d(TAG, "2000");
}
});
Message msg3 = Message.obtain(mHandler, new Runnable() {
@Override
public void run() {
Log.d(TAG, "3000");
}
});
Message msg4 = Message.obtain(mHandler, new Runnable() {
@Override
public void run() {
Log.d(TAG, "4000");
}
});
Message msg5 = Message.obtain(mHandler, new Runnable() {
@Override
public void run() {
Log.d(TAG, "5000");
// 解除屏障
removeBarrier();
}
});
// 将msg3设置为异步消息
msg3.setAsynchronous(true);
// 将msg5设置为异步消息
msg5.setAsynchronous(true);
btnSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 发送延时消息
mHandler.sendMessageDelayed(msg1, 1000);
mHandler.sendMessageDelayed(msg2, 2000);
mHandler.sendMessageDelayed(msg3, 3000);
mHandler.sendMessageDelayed(msg4, 4000);
mHandler.sendMessageDelayed(msg5, 5000);
}
});
大家先想想会如何打印
2024-10-07 22:45:43.013 MainActivity D 1000
2024-10-07 22:45:45.014 MainActivity D 3000
2024-10-07 22:45:47.013 MainActivity D 5000
2024-10-07 22:45:47.018 MainActivity D 2000
2024-10-07 22:45:47.018 MainActivity D 4000
我来解释下为什么会这样打印,点击按钮发送了5条消息分别延时1s到5s,首先会执行msg1打印"1000",在msg1里我们添加了一个屏障根据前面所讲此时异步消息会优先执行,在上述五个消息中msg3和msg5是异步消息因此会先执行打印"3000"和"5000",在msg5执行之后会解除屏障此时同步消息正常执行,此时会打印"2000"和"4000"
总结
到这里关于消息屏障就讲的差不多了,做个总结吧
(1)Handler消息机制中的消息分为三类分别为:屏障消息、同步消息、异步消息。
(2)屏障消息的发送和移除必须成对出现,否则会一直循环查找并执行异步消息
(3)屏障消息发送后异步消息会优先执行
大家有任何疑问或者发现文章中的错误欢迎批评指正,如果觉得不错的欢迎点赞评论666