编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 1.1App启动流程梳理
  • 1.2ActivityThread分析
  • 1.3Context设计思想
  • 2.1Activity基础介绍
  • 2.2Activity启动流程
  • 2.3Activity布局创建
  • 2.4Activity布局绘制
  • 2.5Service基础介绍
  • 2.6Service启动流程
  • 2.7Receiver广播基础
  • 2.8Receiver深入原理
  • 2.9ContentProvider分析
  • 2.10Fragment实践
  • 2.11Intent深入思考
  • 3.1Paint和Canvas
  • 3.2View的绘制基础
  • 3.3onMeasure流程设计
  • 3.4onLayout流程设计
  • 3.5onDraw流程设计
  • 3.6View工作原理
  • 3.7View刷新设计流程
  • 3.8自定义View控件
  • 3.9自定义ViewGroup控件
  • 4.1Handler基础使用
  • 4.2消息机制流程分析
  • 4.3Handler深度解析
  • 4.4Message深度理解
  • 4.5MessageQueue解析
  • 4.6Looper深度解析
  • 4.7理解Handler同步屏障
  • 4.8ThreadLocal分析
  • 4.9ThreadLocal场景
  • 5.1View事件设计思考
  • 5.2View滑动冲突处理
  • 5.3View事件源码分析
  • 5.4View事件总结案例
  • 6.1DecorView设计思想
  • 6.2视图的载体View
  • 6.3视图管理者Window
  • 6.4窗口管理服务WMS
  • 6.5布局解析者Inflater
  • 7.1AsyncTask深入介绍
  • 7.2HandlerThread设计
  • 7.3IntentService设计
  • 8.1IPC通信方式介绍
  • 8.2AIDL进程间通信
  • 8.7Binder通信机制设计
  • /zh/android/basic/9.1注解设计思想和原理.html
  • 9.2APT技术设计详解
  • 9.3APT多种案例实践

4.7理解Handler同步屏障

目录介绍

  • 01.先了解一个场景
    • 1.1 handler消息类型
    • 1.2 什么叫同步屏障
    • 1.3 为何要同步屏障
  • 02.Android中异步消息
    • 2.1 如何发异步消息
    • 2.2 如何设置同步屏障
    • 2.3 同步屏障应用场景
    • 2.4 同步屏障的代价
    • 2.5 同步屏障使用流程
  • 03.同步屏障源码解读
    • 3.1 postSyncBarrier开启同步屏障
    • 3.2 如何判断同步屏障
    • 3.3 屏障消息工作原理
    • 3.4 如何处理有同步和异步混合消息

01.先了解一个场景

1.1 handler消息类型

  • Handler的Message种类分为三种:
    • 普通消息
    • 异步消息
    • 屏障消息
  • 普通消息
    • 普通消息又称为同步消息,我们平时发的消息基本都是同步消息。
  • 异步消息
    • Handler的构造方法有个async参数,默认的构造方法此参数是false,只要我们在构造handler对象的时候,把该参数设置为true就可以。
    • 在创建Message对象时,直接调用Message的setAsynchronous()方法。
  • 普通消息和异步消息区别
    • 在一般情况下,异步消息和同步消息没有什么区别,但是一旦开启了同步屏障以后就有区别了。

1.2 什么叫同步屏障

  • 什么场景用到同步屏障
    • 在系统源码中就有使用。Android系统中的UI更新相关的消息即为异步消息,需要优先处理。那么handler机制中如何优先消息了,这个就需要了解同步屏障。
  • 屏幕刷新机制
    • 16ms左右刷新UI,而是60hz的屏幕,即1s刷新60次。屏幕会在每次刷新的时候发出一个 VSYNC 信号,通知CPU进行绘制计算。具体到我们的代码中,可以认为就是执行onMeasure()、onLayout()、onDraw()这些方法。
  • 消息机制的同步屏障
    • 同步屏障消息就是在消息队列中插入一个屏障,在屏障之后的所有普通消息都会被挡着,不能被处理。
    • 不过异步消息却例外,屏障不会挡住异步消息,因此可以认为,屏障消息就是为了确保异步消息的优先级,设置了屏障后,只能处理其后的异步消息,同步消息会被挡住,除非撤销屏障。
  • 一句话概括
    • 其实就是阻碍同步消息,只让异步消息通过。

1.3 为何要同步屏障

  • 如果设置了异步消息,则首先让先执行。可以理解为先执行异步消息,然后在执行同步消息(开发中一般使用handler发送消息都是同步消息)。

02.Android中异步消息

2.1 如何发异步消息

  • 在Android中什么是异步消息?即给:
    Message.setAsynchronous(true);

2.2 如何设置同步屏障

开启同步屏障的方法就是调用下面的方法:MessageQueue#postSyncBarrier()

Android 是禁止App往MessageQueue插入同步屏障消息的,代码会报错。

Looper.myLooper().getQueue().postSyncBarrier();

2.3 同步屏障应用场景

  • view 绘制原理
    • view绘制的起点是在 viewRootImpl.requestLayout() 方法开始,这个方法会去执行上面的三大绘制任务,就是测量布局绘制。
  • 同步屏障应用场景
    • 调用requestLayout()方法之后,并不会马上开始进行绘制任务,而是会给主线程设置一个同步屏障,并设置 ASYNC 信号监听。
    • 当 ASYNC 信号的到来,会发送一个异步消息到主线程Handler,执行我们上一步设置的绘制监听任务,并移除同步屏障。
  • 为何要设置同步屏障
    • 在等待ASYNC信号的时候主线程什么事都没干?是的。这样的好处是:保证在ASYNC信号到来之时,绘制任务可以被及时执行,不会造成界面卡顿。

2.4 同步屏障的代价

  • 同步屏障的代价
    • 同步消息最多可能被延迟一帧的时间,也就是16ms,才会被执行。
    • 主线程Looper造成过大的压力,在VSYNC信号到来之时,才集中处理所有消息。
  • 改善这个问题办法就是:使用异步消息。
    • 发送异步消息到MessageQueue中时,在等待VSYNC期间也可以执行我们的任务,让我们设置的任务可以更快得被执行且减少主线程Looper的压力。
  • 异步消息机制本身就是为了避免界面卡顿,那我们直接使用异步消息,会不会有隐患?
    • 什么情况的异步消息会造成界面卡顿:异步消息任务执行过长、异步消息海量。若异步消息海量到达影响界面绘制,那么即使是同步任务,也是会导致界面卡顿的;原因是MessageQueue是一个链表结构,海量的消息会导致遍历速度下降,也会影响异步消息的执行效率。
    • 所以我们应该注意的一点是:不可在主线程执行重量级任务,无论异步还是同步。

2.5 哪里用同步屏障

  • 比如,在View更新时,draw、requestLayout、invalidate等很多地方都调用了。ViewRootImpl#scheduleTraversals()。
    @UnsupportedAppUsage
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            // 开启同步屏障
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // 发送异步消息
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
  • 在这里,mChoreographer.postCallback最终会执行到了Choreographer#postCallbackDelayedInternal()
    • 代码流程:postCallback--->postCallbackDelayed--->postCallbackDelayedInternal
    • 可以看到,这里就开启了同步屏障,并且发送了异步消息。由于UI相关的消息是优先级最高的,这样系统就会优先处理这些异步消息。
    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);
            }
        }
    }
  • 当然要移除同步屏障的时候,调用ViewRootImpl#unscheduleTraversals
    void unscheduleTraversals() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            // 移除同步屏障
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            mChoreographer.removeCallbacks(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        }
    }
  • 在ViewRootImpl中的doTraversal()方法中也会移除同步屏障,这里移除是因为requestLayout或者invalidate的时候,刷新之后,在doTraversal()中就会移除同步屏障,因为此时消息已经发送并且处理了。

03.同步屏障源码解读

3.1 postSyncBarrier开启同步屏障

  • 首先看一下,MessageQueue#postSyncBarrier(),源码如下所示
    • 可以看到,Message对象初始化的时候并没有给target赋值,因此target==null的来源就找得到了。
    • 这样就可以插入一条target==null的消息,这个消息就是一个同步屏障。
    @TestApi
    public int postSyncBarrier() {
        // 这里传入的时间是从开机到现在的时间戳
        return postSyncBarrier(SystemClock.uptimeMillis());
    }
    /**
     * 这就是创建的同步屏障的方法
     * 同步屏障就是一个同步消息,只不过这个消息的target为null
     */
    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++;
            // 从消息池中获取Message
            final Message msg = Message.obtain();
            msg.markInUse();
            // 初始化Message对象的时候,并没有给Message.target赋值,
            // 因此Message.target==null
            msg.when = when;
            msg.arg1 = token;
    
            Message prev = null;
            Message p = mMessages;
            if (when != 0) {
                // 这里的when是要加入的Message的时间
                // 这里遍历是找到Message要加入的位置
                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;
        }
    }

3.2 如何判断同步屏障

  • 通常通过Handler发送消息handler.sendMessage(),最终都会调用Handler.java中的enqueueMessage()方法。
    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }
  • enqueueMessage()方法里为msg设置了target字段。
  • 而上面的postSyncBarrier(),也是从Message消息对象池中获取一个msg,插入到消息队列中,唯一的不同是没有设置target字段。
  • 所以从代码层面上讲,屏障消息就是一个target为空的Message。

3.3 屏障消息工作原理

  • 消息的最终处理其实都是在消息轮询器Looper#loop()中,
    • 而loop()循环中会调用MessageQueue#next()从消息队列中进行取消息。
  • 然后看MessageQueue.next方法
    • 可以看出,当消息队列开启同步屏障的时候(即标识为msg.target==null),消息机制在处理消息的时候,会优先处理异步消息。
    • 这样,同步屏障就起到了一种过滤(只会处理异步消息)和优先级(异步消息的优先级要高于同步消息)的作用。
    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的消息
                // 如果target==null,那么它就是屏障,需要循环遍历,
                // 一直往后找到第一个异步消息,即msg.isAsynchronous()为true
                // 这个target==null的消息,不会被取出处理,一直会存在
                // 每次处理异步消息的时候,都会从头开始轮询
                // 都需要经历从msg.target开始的遍历
                if (msg != null && msg.target == null) {
                    // 使用一个do..while循环
                    // 轮询消息队列里的消息,这里使用do..while循环的原因
                    // 是因为do..while循环中取出的这第一个消息是target==null的消息
                    // 这个消息是同步屏障的标志消息
                    // 接下去进行遍历循环取出Message.isAsynchronous()为true的消息
                    // isAsynchronous()为true就是异步消息
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    // 如果有消息需要处理,先判断时间有没有到,如果没有到的话设置阻塞时间
                    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;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        // 返回拿到的消息
                        return msg;
                    }
                } else {
                    // 没有消息,nextPollTimeoutMillis复位
                    nextPollTimeoutMillis = -1;
                }
    
                ...
            }
        }
    }

3.3 如何处理有同步和异步混合消息

  • 先看一张图
    • image
      image
  • 在消息队列中有同步消息和异步消息(黄色部分)以及一道墙(同步屏障--红色部分)。
    • 有了同步屏障的存在,msg_2和msg_M这两个异步消息可以被优先处理,而后面的msg_3等同步消息则不会被处理。
  • 那么这些同步消息什么时候可以被处理呢?
    • 需要先移除这个同步屏障,即调用MessageQueue#removeSyncBarrier()
    @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) {
                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);
            }
        }
    }

04.最后总结一下

  • ViewRootImpl 将遍历view树包装成一个Runnable并抛到Choreographer, 在抛之前会向主线程消息队列中抛同步屏障
  • 同步屏障也是一个Message,只不过 target 等于null
  • 取下一条message的算法中,若遇到同步屏障,则会越过同步消息,向后遍历找第一条异步消息找到则返回(Choreographer抛的异步消息),若没有找到则会执行epoll挂起
  • 当执行到遍历View树的 runnable时,ViewRootImpl会移除同步屏障
贡献者: yangchong211
上一篇
4.6Looper深度解析
下一篇
4.8ThreadLocal分析