Android:同步屏障的简单理解和使用

news2024/9/28 3:23:35

同步屏障的简单理解和使用

  • 1、背景
  • 2、何为同步屏障?
    • 2.1、 发送屏障消息——postSyncBarrier
    • 2.2、发送异步消息
    • 2.3、处理消息
    • 2.4、移除屏障消息——removeSyncBarrier
  • 2、系统什么时候添加同步屏障?
  • 参考

1、背景

这里我们假设一个场景:我们向主线程发送了一个UI绘制操作Message,而此时消息队列中的消息非常多,那么这个Message的处理可能会得到延迟,绘制不及时造成界面卡顿。同步屏障机制的作用,是让这个绘制消息得以越过其他的消息,优先被执行。

2、何为同步屏障?

Handler的message分为三种

  • 同步消息
  • 异步消息
  • 屏障消息

通常我们使用handler发送消息,都是使用默认的构造函数构造handler,然后使用send方法发送。这样发送的消息都是普通消息也就是同步消息,发出去的消息就会在MessageQueue中排队。异步消息正常情况下跟同步消息没有区别,只有在设置了同步屏障之后,才会出现差异。

什么是同步屏障?

  • 开启同步屏障的第一步需要发送一个特殊消息作为屏障消息,当消息队列检测到了这种消息后,就会从这个消息开始,遍历后续的消息,只处理其中被标记为“异步”的消息,忽略同步消息(所以叫“同步屏障”),相当于给一部分消息开设了“VIP”优先通道。当使用完同步屏障后我们还注意移除屏障。
    在这里插入图片描述

那么同步屏障如何使用,如何运行的呢,主要分为以下四步:

  • 发送屏障消息(Message中target为空,target的类型是Handler)
  • 发送异步消息(发送被标记为asynchronous的消息)
  • 处理消息
  • 移除屏障消息(通过发送屏障消息时返回的token来删除消息)

图示Android系统原理之Handler同步屏障

2.1、 发送屏障消息——postSyncBarrier

MessageQueue中的postSyncBarrier方法:

public int postSyncBarrier() {
    // uptimeMillis会返回从系统启动开始到现在的时间(不包括深度睡眠的时间):milliseconds of non-sleep uptime since boot
    return postSyncBarrier(SystemClock.uptimeMillis());
}
    
private int postSyncBarrier(long when) {
    // Enqueue a new sync barrier token.
    // We dont need to wake the queue because the purpose of a barrier is to stall it.
    // 上面的意思是放一个新的同步屏障消息到队列中,它就会一直在那挡着(直到你移除它)
    synchronized (this) {
        // 屏障消息的token,作为唯一标识,用于移除屏障消息
        final int token = mNextBarrierToken++;
        // 循环利用Message对象
        final Message msg = Message.obtain();
        // 标记为正在使用,记录时间与token
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;

        // 下面代码的目的就是把屏障消息按时间排序插入到消息队列中,
        // 前面的是早于自己的消息,后面的是晚于自己的消息
        // 1、找到两个相邻的消息,使得 prev.when < msg.when < p.when
        Message prev = null;
        Message p = mMessages;
        if (when != 0) {
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        // 2、插入屏障消息到prev与p之间
        if (prev != null) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}

该方法会返回一个token,在移除屏障消息的时候使用。

  • 这里插一句,感觉很多人其实不理解token到底是什么意思,我们在用户登录的时候也会用到token,其实token的意义就是一个唯一标识,token意思是“已经发生了”,就是给一个已发生的事物一个唯一标识

postSyncBarrier的作用就是在消息队列中插入一个屏障消息,插入到什么位置呢,按消息的先来后到,排到对应的位置(消息都有记录when的,按when大小排队)。这里注意了,这个消息是没有给Message中的target赋值的,这个会作为后面判断是否开启同步屏障的依据。

2.2、发送异步消息

添加异步消息有两种办法:

  • 使用异步类型的Handler发送的全部Message都是异步的
  • 给Message标志异步

给Message标记异步是比较简单的,通过setAsynchronous方法即可。

Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);

public void setAsynchronous(boolean async) {
    if (async) {
        flags |= FLAG_ASYNCHRONOUS;
    } else {
        flags &= ~FLAG_ASYNCHRONOUS;
    }
}

Handler有一系列带Boolean类型的参数的构造器,这个参数就是决定是否是异步Handler:

public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    // 这里赋值
    mAsynchronous = async;
}

在发送消息的时候就会给Message赋值:

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);
}

2.3、处理消息

MessageQueuenext方法中进行了消息读取,在这里做了同步屏障的相关判断:

    ......
    int nextPollTimeoutMillis = 0;
    for (;;) {
        ......
        
        // 阻塞,nextPollTimeoutMillis为等待时间,如果为-1则会一直阻塞
        nativePollOnce(ptr, nextPollTimeoutMillis);
        
        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            // 下面的代码目的是获取一个可用的消息,如果找到就return,
            // 没找到就继续后面我省略的代码(省略了IdHandler的相关代码)
            
            // 获取时间,还是通过uptimeMillis这个方法
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            // 如果队列头部消息为屏障消息,即“target”为空的消息,则去寻找队列中的异步消息
            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) {
                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.
                    // 一个标记,表示next循环是不是还在被阻塞着
                    mBlocked = false;
                    // 移除消息
                    if (prevMsg != null) {
                        // 移除异步消息
                        prevMsg.next = msg.next;
                    } else {
                        // 移除同步消息
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    
                    // 标记为正在使用
                    msg.markInUse();
                    return msg;
                }
            } else {
                // No more messages.
                // 没有获取到消息,接下来运行下面省略的代码,nextPollTimeoutMillis为“-1”,在循环开始的nativePollOnce方法将会一直阻塞。
                nextPollTimeoutMillis = -1;
            }
        
            // Process the quit message now that all pending messages have been handled.
            if (mQuitting) {
                dispose();
                return null;
            }
            ......// IdleHandler
        }// end of synchronized
        ......// IdleHandler
    }
}

可以看到,在next方法的无限循环中,首先是nativePollOnce阻塞,然后是取消息的同步代码块(使用synchronized包裹),在这其中,首先取消息队列的头部消息(即mMessages),如果是屏障消息(消息的target为空),则寻找队列中的异步消息进行处理,否则直接处理这条头部消息。

在找到合适的消息后(if(msg != null)),会将即将处理的消息移除队列并返回;当然,如果没有找到就会将nextPollTimeoutMillis置为-1,让循环进入阻塞状态。

在next方法返回消息后,Looper会调用HandlerdispatchMessage回调到对应的方法中,我们来看看Looper.loop()方法:

public static void loop() {
    .....
    for (;;) {
        // 无限取消息,“might block” 指的就是nativePollOnce的阻塞
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            // 如果返回的消息是空的,则会退出循环。如果循环没退出并且没有消息,则会被nativePollOnce阻塞着。
            return;
        }
        ......
        // 分发消息,target即是发送消息的Handler
        msg.target.dispatchMessage(msg);
        ......
        // 回收消息
        msg.recycleUnchecked();
}

最后Handler的dispatchMessage就会调用到handleMessage或者Messagecallbackcallback为Runnable对象),运行消息所指向的内容:

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

2.4、移除屏障消息——removeSyncBarrier

使用同步屏障一定要记得移除消息,消息队列是不会自动移除的。我们通过MessageQueueremoveSyncBarrier方法移除屏障消息:

    // 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.");
        }
        
        // 移除屏障消息,并判断是否需要唤醒队列(nativePollOnce用于阻塞,nativeWake用于唤醒)
        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);
        }
    }
}

removeSyncBarrier方法中,首先是去寻找屏障消息,找不到会抛出异常;

然后是移除屏障消息,并且判断是否需要唤醒消息队列继续取消息(唤醒next方法,这里的nativeWake用于唤醒,next方法中的nativePollOnce用于阻塞)。

2、系统什么时候添加同步屏障?

在请求监听Vsync信号时,阻塞Handler消息队列中的同步消息,优先保证接收Vsync信号的异步消息,及时生成新的屏幕数据,供屏幕显示。
在这里插入图片描述关于Handler同步屏障你可能不知道的问题

我们的手机屏幕刷新频率有不同的类型,60Hz120Hz等。60Hz表示屏幕在一秒内刷新60次,也就是每隔16.6ms刷新一次。屏幕会在每次刷新的时候发出一个 VSYNC 信号,通知CPU进行绘制计算。具体到我们的代码中,可以认为就是执行onMesure()、onLayout()、onDraw()这些方法。

1、view绘制的起点是在 viewRootImpl.requestLayout() 方法开始,这个方法会去执行上面的三大绘制任务,就是测量布局绘制。但是,重点来了:

2、调用requestLayout()方法之后,并不会马上开始进行绘制任务,而是会给主线程设置一个同步屏障,并设置 VSYNC 信号监听。

3、当 VSYNC 信号的到来,会发送一个异步消息到主线程Handler,执行我们上一步设置的绘制监听任务,并移除同步屏障

  • 这里我们只需要明确一个情况:调用requestLayout()方法之后会设置一个同步屏障,直到VSYNC信号到来才会执行绘制任务并移除同步屏障。
@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            //校验主线程
            checkThread();
            mLayoutRequested = true;
            //调用这个方法启动绘制流程
            scheduleTraversals();
        }
    }

//在调用scheduleTraversals()的时候 postSyncBarrier添加同步消息屏障

@UnsupportedAppUsage
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            //1. 往主线程的Handler对应的MessageQueue发送一个同步屏障消息
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //2.将mTraversalRunnable保存到Choreographer中
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }    
    ...
     //在doTraversal方法中移除同步消息屏障
     void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            //移除同步屏障
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            ...
        }
    }

在这个方法中,涉及到三个比较重要的信息

  • mTraversalRunnable
    • 首先看mTraversalRunnable,它的作用就是从ViewRootImpl 从上往下执行performMeasureperformLayoutperformDraw
  • Choreographer编舞者
  • 同步屏障消息

Choreographer主要是为了配合Vsync信号,给上层app的渲染提供一个稳定的Message处理时机,也就是Vsync信号到来时,系统通过对Vsync信号的调整,来控制每一帧绘制操作的时机。当Vsync信号到来时,会往主线程的MessageQueue中插入一条异步消息,由于在scheduleTraversals中给MessageQueue中插入了同步屏障消息,那么当执行到同步屏障时,会取出异步消息执行。

看下Choreography中插入消息的方法是如何实现的:

private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        synchronized (mLock) {
            ...
            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);
            }
        }
    }

参考

1 、什么是Handler的同步屏障机制?
2、关于Handler同步屏障你可能不知道的问题
3、图示Android系统原理之Handler同步屏障(三)
4、源码阅读#Handler(下)同步屏障与IdleHandler

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/366758.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网狐服务端C++引入http功能剖析

旗舰版本的网狐服务端及以前的版本都是没有http解析功能的&#xff0c;导致就是web后台改了配置不能及时通知到游戏里面去&#xff0c;以至于很多小公司拿这种框架来开发的变通的方案就是用定时器不定时去刷数据库&#xff0c;导致多少个功能就有多少个定时去刷新&#xff0c;代…

0基础学习软件测试难么

0基础开始学习任何一样事情都是有一定难度的&#xff0c;但是也要有对比&#xff0c;软件测试相比于IT行业其他学科已经算是容易入门的了&#xff0c;就看你个人的学习方法&#xff0c;找的学习资源以及你的自制力。 正确学习方法路径 “我一听就懂&#xff0c;一敲就废&…

【Spring的事务传播行为有哪些呢?Spring事务的隔离级别?讲下嵌套事务?】

如果你想寻求一份与后端相关的开发工作&#xff0c;那么关于Spring事务相关的面试题你就不能说不会并且不能不知道&#xff1f; 人生如棋&#xff0c;我愿为卒&#xff0c;行动虽慢&#xff0c;可谁曾见我后退一步&#xff1f; 一.Spring中声明事务的方式 1.1 编程式事务 编程…

Android Gradle脚本打包

1、背景资料 1.1 Android-Gradle-Groovy-Java-JVM 之间的关系 1.2 Android Gradle Plugin Version版本 与 Gradle Version版本的对应关系 Android Gradle Plugin Version版本Gradle Version版本1.0.0 - 1.1.32.2.1 - 2.31.2.0 - 1.3.12.2.1 - 2.91.5.02.2.1 - 2.132.0.0 -…

WFP网络过滤驱动-限制网站访问

文章目录前言WFP入门介绍WFP基本架构名词解释代码基本结构代码示例前言 WFP Architecture - Win32 apps | Microsoft Learn是一个网络流量处理平台。WFP 由一组连接到网络堆栈的钩子和一个协调网络堆栈交互的过滤引擎组成。 本文使用WFP&#xff0c;实现了一个网络阻断的demo…

Guna UI WinForms 2.0.4.4 Crack

Guna.UI2 WinForms is the suite for creating groundbreaking desktop app UI. It is for developers targeting the .NET Windows Forms platform. 50 多个 UI 控件 具有广泛功能的综合组件可帮助您开发任何东西。 无尽的定制 只需拖放即可创建视觉效果命令和体验。 出色的…

服务端开发之Java备战秋招面试篇2-HashMap底层原理篇

现在Java应届生和实习生就业基本上必问HashMap的底层原理和扩容机制等&#xff0c;可以说是十分常见的面试题了&#xff0c;今天我们来好好整理一下这些知识&#xff0c;为后面的秋招做足准备&#xff0c;加油吧&#xff0c;少年。 目录 1、HashMap集合介绍 2、HashMap的存储…

S2-001漏洞分析

首发于个人博客&#xff1a;https://bthoughts.top/posts/S2-001漏洞分析/ 一、简介 1.1 Struts2 Struts2是流行和成熟的基于MVC设计模式的Web应用程序框架。 Struts2不只是Struts1下一个版本&#xff0c;它是一个完全重写的Struts架构。 1.2 S2-001 Remote code exploit o…

【保姆级】手把手捋动态代理流程(JDK+Cglib超详细源码分析)

简介动态代理&#xff0c;通俗点说就是&#xff1a;无需声明式的创建java代理类&#xff0c;而是在运行过程中生成"虚拟"的代理类&#xff0c;被ClassLoader加载。 从而避免了静态代理那样需要声明大量的代理类。上面的简介中提到了两个关键的名词&#xff1a;“静态…

C语言进阶(九)—— 函数指针和回调函数、预处理、动态库和静态库的使用、递归函数

1. 函数指针1.1 函数类型通过什么来区分两个不同的函数&#xff1f;一个函数在编译时被分配一个入口地址&#xff0c;这个地址就称为函数的指针&#xff0c;函数名代表函数的入口地址。函数三要素&#xff1a; 名称、参数、返回值。C语言中的函数有自己特定的类型。c语言中通过…

多元回归分析 | ELM极限学习机多输入单输出预测(Matlab完整程序)

多元回归分析 | ELM极限学习机多输入单输出预测(Matlab完整程序) 目录 多元回归分析 | ELM极限学习机多输入单输出预测(Matlab完整程序)预测结果评价指标基本介绍程序设计参考资料预测结果 评价指标 -----------ELM结果分析-------------- 均方根误差(RMSE):0.55438 测试集…

webgis高德地图

webgis高德地图 首先准备工作,注册一个高德地图账号,然后在创建一个新应用生一个key跟appId 高德开放平台 接着创建一个html页面 高德配置手册 <style>* {margin: 0;padding: 0;}#

如何维护固态继电器?

固态继电器是SSR的简称&#xff0c;是由微电子电路、分立电子器件和电力电子功率器件组成的非接触式开关。隔离装置用于实现控制端子与负载终端之间的隔离。固态继电器的输入端使用微小的控制信号直接驱动大电流负载。那么&#xff0c;如何保养固态继电器呢&#xff1f; 在为小…

Editor工具开发基础一:顶部菜单栏拓展

一.创建脚本路径 不管是那种工具 都需要在工程里创建一个Editor文件夹 来存放工具.cs文件 二.特性 MenuItem 特性 修饰静态方法 三个构造函数 public MenuItem(string itemName); public MenuItem(string itemName, bool isValidateFunction); public MenuItem(string itemN…

手工测试用例就是自动化测试脚本——使用ruby 1.9新特性进行自动化脚本的编写

昨天因为要装watir-webdriver的原因将用了快一年的ruby1.8.6升级到了1.9。由于1.9是原生支持unicode编码&#xff0c;所以我们可以使用中文进行自动化脚本的编写工作。 做了简单的封装后&#xff0c;我们可以实现如下的自动化测试代码。请注意&#xff0c;这些代码是可以正确运…

【2023考研数学二考试大纲】

文章目录I 考试科目II考试形式和试卷结构一、试卷满分及考试时间二、答题方式三、试卷内容结构四、试卷题型结构III考查内容【高等数学】一、函数、极限、连续二、一元函数微分学三、一元函数积分学四、多元函数微积分学五、常微分方程【线性代数】一、行列式二、矩阵三、向量四…

新手入门吉他推荐,第一把吉他从这十款选绝不踩雷!初学者吉他选购指南【新手必看】

一、新手购琴注意事项&#xff1a; 1、预算范围 一把合适的吉他对于初学者来说会拥有一个很好的音乐启蒙。选一款性价比高&#xff0c;做工材料、音质和手感相对较好的吉他自然不会是一件吃亏的事。**初学者第一把琴的预算&#xff0c;我觉得最低标准也是要在500元起&#xf…

数字IC设计需要学什么?

看到不少同学在网上提问数字IC设计如何入门&#xff0c;在学习过程中面临着各种各样的问题&#xff0c;比如书本知识艰涩难懂&#xff0c;有知识问题难解决&#xff0c;网络资源少&#xff0c;质量参差不齐。那么数字IC设计到底需要学什么呢&#xff1f; 首先来看看数字IC设计…

我们来说说蹿红的AIGC到底是什么?ChatGPT又是什么?

近期&#xff0c;人工智能&#xff08;AI&#xff09;领域动作频频&#xff0c;OPENAI公司Chat GPT的出现&#xff0c;标志着人工智能的研究与应用已经进入了一个崭新的发展阶段&#xff0c;国内腾讯、阿里巴巴、百度、易网、国外微软、谷歌、苹果、IBM、Amazon&#xff0c;等互…

YOLOv5的训练调优技巧

本文编译自英文原文https://github.com/ultralytics/yolov5/wiki/Tips-for-Best-Training-Results&#xff0c;文章解释了如何提高Yolov5的mAP和训练效果。大多数时间&#xff0c;在没有改变模型或是训练配置的情况下&#xff0c;如果能够提供足够多的数据集以及好的标注&#…