登录

去注册

登录

注册

去登录

注册

每日问答 Handler应该是大家再熟悉不过的类了,那么其中有个同步屏障机制,你了解多少呢?

xiaoyang   2019-07-23   收藏

1. 首先至少要知道同步屏障机制是什么?


2. 思考下,为什么要有这个机制?


本周1/3,家有喜事,最近熬夜已懵逼。

16

在wanandroid的群里当管理蛮久了,也一直在关注wanandroid的动态,当然少不了隔三差五的上来学习一波。现在,让我来回答一波以实际行动表示支持wanandroid。


恭喜鸿洋迎来一对贴心小棉袄,我的是个男孩儿,但是我最喜欢女儿了,鸿洋以后一定会很幸福的,再次祝福阖家幸福!


1 前言


想要了解清楚同步屏障机制,必须先完全理解Android消息机制和应用,不了解的同学可以先去搜索相关资料熟悉一下再继续看下面的内容。


2. 同步屏障是什么


我们知道消息机制中有一个重要的类是MessageQueue,望文生义就是消息队列的意思,在一般情况下,MessageQueue对于当前线程是同步的,那么什么是当前线程呢?就是实例化MessageQueue的线程,在消息机制这个完整的机制中,MessageQueue是在Looper的构造方法中被实例化。也就是说,MessageQueue正常情况是同步处理消息的,明白这一点就可以让同步屏障入场了。


同步屏障,看字面意思也能猜出个八九分,就是阻碍队列中同步消息的屏障,那么它是如何运行的呢?此时需要引进异步消息,在正常时候,我们发送的Message全都是同步消息,发送异步消息有两种方式,下面分别来看一下。


2.1 发送异步消息的方法


第一种发送异步消息的方式,使用Handler包含async参数的构造方法,例如下面这个。

public Handler(boolean async) {
this(null, async);
}

只要async参数为true,所有的消息都将是异步消息。

Handler mHandler = new Handler(true);
...

Message msg = mHandler.obtainMessage(...);
mHandler.sendMessageAtTime(msg, dueTime);


第二种方法,显示设置Message为异步消息

Message msg = mHandler.obtainMessage(...);
msg.setAsynchronous(true);
mHandler.sendToTarget();

其实第一种方式也是在我们发送Message时,Handler内部帮我们调用了Message#setAsynchronous(boolean),且参数传了true,因此这两使用方式了解即可。


我们知道,上面的Message的target都是非空的,而在MessageQueue#next()方法中,target非空的Message都会被正常处理(下面会有相关代码),因此在这个时候同步消息和异步消息并没有什么不同。如果同学们思路没有断的话,应该能想到,此时同步障碍就需要登场了。


2.2 开启同步障碍

现在我们要贴一点framework/base下的代码,都以oreo-release分支为准,因为不同版本SDK这些代码所在的为止略有不同。


如果相让异步消息起作用,就得开启同步障碍,同步障碍会阻碍同步消息,只允许通过异步消息,如果队列中没有异步消息,此时的loop()方法将被Linux epoll机制所阻塞。


开启同步障碍也很简单,调用MessageQueue#postSyncBarrier()方法即可,因为MessageQeueu绑定在Looper上,而Looper依附在Handler上,所以正常情况下,源码中是这样开启同步障碍的:

mHandler.getLooper().getQueue().postSyncBarrier();

它的代码也很简单:

public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}


private int postSyncBarrier(long when) {
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) {
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}

可以看出,在实例化Message对象的时候并没有设置它的target成员变量的值,然后随即就根据执行时间把它放到链表的某个位置了,实际上就是链表的开始位置。也就是说,当在消息队列中放入一个target为空的Message的时候,当前Handler的这一套消息机制就开启了同步阻断。


当开启同步障碍后,它是如何生效的呢?我们知道,Looper#loop()方法最终还是调用了MessageQueue#next()方法来获取队列中的消息,现在我们来看看该方法的代码:

for (;;) {
nativePollOnce(ptr, nextPollTimeoutMillis);

synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;

// 如果当前开启了同步障碍
if (msg != null && msg.target == null) {
// 处理同步障碍,获取队列中的下一个异步消息
do {
prevMsg = msg;
msg = msg.next;

// 如果这个消息是同步的,那么继续向下找异步的
} while (msg != null && !msg.isAsynchronous());
}

if (msg != null) {
if (now < msg.when) {
// 如果当前消息的执行时间没到,让它沉睡到下个消息的执行时间
nextPollTimeoutMillis = msg.when - now;
} else {
...
return msg; // 执行时间到了,执行它
}
} else {
// 当前没有任何消息需执行,一直沉睡
nextPollTimeoutMillis = -1;
}
}
}

我摘抄出了主要的代码,为了方便同学们阅读理解,上述代码是简化过的。

可以看出来,当开启了同步障碍时,Looper在获取下一个要执行的消息时,会在链表中寻找第一个要执行的异步消息,如果没有找到异步消息,就让当前线程沉睡。


nativeWake()方法和nativePollOnce()方法采用了Linux的epoll机制,其中nativePollOnce()的第二个值,当它是-1时会一直沉睡,直到被主动唤醒为止,当它是0时不会沉睡,当它是大于0的值时会沉睡传入的值那么多的毫秒时间。epoll机制实质上是让CPU沉睡,来保障当前线程一直在运行而不中断或者卡死,这也是Looper#loop()死循环为什么不会导致住县城ANR的根本原因。

3. 同步障碍的应用

那么同步障碍有什么用呢?在我们日常的应用层开发中极少用到它,读了framework的代码后我们会发现,在ViewRootImpl.java中有使用它:

void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true; // 开启当前Handler的同步屏障 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); // 发送一条异步消息 mChoreographer.postCallback(..., mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
cheduleConsumeBatchedInput();
}

notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

实际上,这里的Handler使用的是主线程的Looper,因此这里会阻断主线程Looper的其他同步消息,在ViewRootImpl和Choreographer中多次使用到了异步消息,以完成View的整个绘制流程。


没错,也许有同学已经被启发了,当我们点击页面的某个控件时,希望瞬间得到它的回应,而不是卡在那里,最起码有个圈圈在转也行。当我们点击某个按钮,此时开启了一个Activity,如果队列中此时有很多消息在排队等候呢?那么这个Activity的测量、布局和绘制就得一直等到所有消息被处理完成才能执行,此时我们会看到页面一直黑着或者一直白着,反正不是我们想要的效果,因此如果这个消息队列有一个优先级的特点,那么不久可以解决这个问题了吗?


综上,所以在消息机制中也很巧妙的融入了优先级特点,这个同步障碍机制,实质上是一个对消息队列的优先级显示。关于Java/Android中的优先级任务队列的实践,大家可以多搜索相关资料来学习,这个在日常开发中很有用。


回复
yizems : @严振杰 

大佬出手,非同一般

2019-08-02 回复
白日依山尽_ : @白日依山尽_ 

api 9.0 postSyncBarrier() 也是被注释了 @hide 看来 google 不希望我们去用。

2019-07-22 回复
白日依山尽_ : @严振杰 

先赞一个,第一种发送异步消息的方法源码上 @hide 了,常规手段用不了。

2019-07-22 回复
hexu2019 : @严振杰 

这个应用层开发有什么用吗?好像都是hide方法,用不了啊

2019-07-22 回复
正阳 : @严振杰 

学到了

2019-07-22 回复
nanchen2251 : @严振杰 

借楼恭贺鸿神喜得千金~

2019-07-22 回复
星星y : @严振杰 

2019-07-22 回复
willwaywang6 : @严振杰 

点赞

2019-07-22 回复
陈小缘 : @严振杰 

棒棒棒~d(* ̄▽ ̄)===b 借楼恭贺鸿神喜得千金~

2019-07-22 回复
tustlitao : @严振杰 

赞(/≧▽≦/)

2019-07-21 回复
3

1 同步屏障机制就是插入一个同步屏障信息到Looper的队列头部,当Looper调用next获取信息时候,发现队列头部是一个同步屏障信息,就会跳过所有同步消息,寻找为异步的消息执行


这个过程可以简单理解为: 当一堆人在排队进周杰伦演唱会时候,周杰伦来了,就通知保安,周杰伦来了,于是优先给周杰伦进去,等周杰伦进去后,其他人再按顺序进去


2.为什么要这样呢?

用于实时

因为屏幕刷新是每16ms一次

假如队列信息是这样

a                       b                          刷新

200ms           120ms 

这样这个刷新信息要经过320ms才能执行,相等于丢了20帧


而用同步屏障,刷新消息会先执行,执行完成后,再执行a和b


刷新消息是由接收到系统的VSYNC信号才刷新的,VSYNC信号是需要手动注册的

常见当我们调用requestLayout或者postFrameCallBack都会请求监听下一次的VSYNC信号

所以app并不是因为每隔16ms都会刷新一次,而是我们主动监听下一次的VSYNC信号,得到系统通知才会刷新


所以在a和b的这段时间内,假如我们没有请求监听,那么a和b会顺序执行

否则会再次进行同步屏障,先执行刷新信息


但是同步屏障的时机是Queue的next方法,所以假如某个耗时方法正在执行的话

会需要等待这个耗时方法执行完成后,才能开启同步屏障




回复
1

看了好几次都看不懂,最后看了看源码。。。结合严sir还有csdn上面的文章才理解清楚。

1.handler发送的消息分为同步消息和异步消息,我们日常发送的是同步,发送异步消息的方式看严大佬的内容

(异步消息类似于着急赶火车的人,想插队又有保安在,只能老老实实排队)

2.MessageQueue#postSyncBarrier()开启同步屏障,实际上是在Message链表头部插入一个空target的Message用来标识让异步先走,同步稍后(保安让着急赶火车的人先走)

3.完事之后必须移除同步屏障MessageQueue#<premenlo';font-size:9.0pt;">removeSyncBarrier</premenlo';font-size:9.0pt;">(token),不然同步消息好像会一直无法处理(朱门酒肉臭,路有冻死骨

回复

删除留言

确认删除留言,会导致相关评论丢失?

取消 确定