handler解析(3)-同步消息、异步消息、同步屏障

news/2024/7/7 4:56:57 标签: Handler, android

Message分为3种:普通消息(同步消息)、屏障消息(同步屏障)和异步消息。我们通常使用的都是普通消息,而屏障消息就是在消息队列中插入一个屏障,在屏障之后的所有普通消息都会被挡着,不能被处理。不过异步消息却例外,屏障不会挡住异步消息,因此可以这样认为:屏障消息就是为了确保异步消息的优先级,设置了屏障后,只能处理其后的异步消息,同步消息会被挡住,除非撤销屏障。

同步消息

也就是我一般使用的Message,再通过Handler进行sendMessage到消息队列,前提是构造Handler时候传的构造参数async为false


Handler mHandler = new Handler()
//或者
Handler mHandler = new Handler(true)

通过以上mHandler发送的Message都是同步消息,且Message会与该mHandler绑定,即:


private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    //将Handler赋值给Message的target变量
    msg.target = this;
    //mAsynchronous为false,为同步消息
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

异步消息

根据前面同步消息的发送流程可以知道,只要通过构造参async为true的Handler发送的Message都为异步消息,即:

        msg.setAsynchronous(true);

同步屏障

同步屏障是通过MessageQueue的postSyncBarrier方法插入到消息队列的。

 private int postSyncBarrier(long when) {
        synchronized (this) {
            final int token = mNextBarrierToken++;
            //1、屏障消息和普通消息的区别是屏障消息没有tartget。
            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            msg.arg1 = token;

            Message prev = null;
            Message p = mMessages;
            //2、根据时间顺序将屏障插入到消息链表中适当的位置
            if (when != 0) {
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            if (prev != null) { // invariant: p == prev.next
                msg.next = p;
                prev.next = msg;
            } else {
                msg.next = p;
                mMessages = msg;
            }
            //3、返回一个序号,通过这个序号可以撤销屏障
            return token;
        }
    }

postSyncBarrier方法就是用来插入一个屏障到消息队列的,可以看到它很简单,从这个方法我们可以知道如下:

  • 屏障消息和普通消息的区别在于屏障没有tartget,普通消息有target是因为它需要将消息分发给对应的target,而屏障不需要被分发,它就是用来挡住普通消息来保证异步消息优先处理的。
  • 屏障和普通消息一样可以根据时间来插入到消息队列中的适当位置,并且只会挡住它后面的同步消息的分发。
  • postSyncBarrier返回一个int类型的数值,通过这个数值可以撤销屏障。
  • 插入普通消息会唤醒消息队列,但是插入屏障不会。

同步屏障的工作原理

通过postSyncBarrier方法屏障就被插入到消息队列中了,那么屏障是如何挡住普通消息只允许异步消息通过的呢?我们知道MessageQueue是通过next方法来获取消息的。

Message next() {
			//1、如果有消息被插入到消息队列或者超时时间到,就被唤醒,否则阻塞在这。
            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {        
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {//2、遇到屏障  msg.target == null
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    //3、遍历消息链表找到最近的一条异步消息
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                	//4、如果找到异步消息
                    if (now < msg.when) {//异步消息还没到处理时间,就在等会(超时时间)
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        //异步消息到了处理时间,就从链表移除,返回它。
                        mBlocked = false;
                        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 = -1;
                }
			//。。。。
        }
    }

可以看到,在注释2如果碰到屏障就遍历整个消息链表找到最近的一条异步消息,在遍历的过程中只有异步消息才会被处理执行到 if (msg != null){}中的代码。可以看到通过这种方式就挡住了所有的普通消息。

移除屏障

移除屏障可以通过MessageQueue的removeSyncBarrier方法:

//注释已经写的很清楚了,就是通过插入同步屏障时返回的token 来移除屏障
/**
     * Removes a synchronization barrier.
     *
     * @param token The synchronization barrier token that was returned by
     * {@link #postSyncBarrier}.
     *
     * @throws IllegalStateException if the barrier was not found.
     *
     * @hide
     */
    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;
            //找到token对应的屏障
            while (p != null && (p.target != null || p.arg1 != token)) {
                prev = p;
                p = p.next;
            }
            final boolean needWake;
            //从消息链表中移除
            if (prev != null) {
                prev.next = p.next;
                needWake = false;
            } else {
                mMessages = p.next;
                needWake = mMessages == null || mMessages.target != null;
            }
            //回收这个Message到对象池中。
            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);//唤醒消息队列
            }
    }

应用

异步消息需要同步屏障的辅助,但同步屏障我们无法手动添加,因此了解系统何时添加和删除同步屏障是非常必要的。只有这样,才能更好地运用异步消息这个功能,知道为什么要用和如何用

了解同步屏障需要简单了解一点屏幕刷新机制的内容。放心,只需要了解一丢丢就可以了。

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

了解过 view 绘制原理的读者应该知道,view绘制的起点是在 viewRootImpl.requestLayout() 方法开始,这个方法会去执行上面的三大绘制任务,就是测量布局绘制。但是,重点来了:

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

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

这里我们只需要明确一个情况:调用requestLayout()方法之后会设置一个同步屏障,知道ASYNC信号到来才会执行绘制任务并移除同步屏障。(这里涉及到Android屏幕刷新以及绘制原理更多的内容,本文不详细展开,感兴趣的读者可以点击文末的连接阅读。)

那,这样在等待ASYNC信号的时候主线程什么事都没干?是的。这样的好处是:保证在ASYNC信号到来之时,绘制任务可以被及时执行,不会造成界面卡顿。但这样也带来了相对应的代价:

  • 我们的同步消息最多可能被延迟一帧的时间,也就是16ms,才会被执行
  • 主线程Looper造成过大的压力,在VSYNC信号到来之时,才集中处理所有消息

改善这个问题办法就是:使用异步消息。当我们发送异步消息到MessageQueue中时,在等待VSYNC期间也可以执行我们的任务,让我们设置的任务可以更快得被执行且减少主线程Looper的压力。

可能有读者会觉得,异步消息机制本身就是为了避免界面卡顿,那我们直接使用异步消息,会不会有隐患?这里我们需要思考一下,什么情况的异步消息会造成界面卡顿:异步消息任务执行过长、异步消息海量。

如果异步消息执行时间太长,那即时是同步任务,也会造成界面卡顿,这点应该都很好理解。其次,若异步消息海量到达影响界面绘制,那么即使是同步任务,也是会导致界面卡顿的;原因是MessageQueue是一个链表结构,海量的消息会导致遍历速度下降,也会影响异步消息的执行效率。所以我们应该注意的一点是:

不可在主线程执行重量级任务,无论异步还是同步

那,我们以后岂不是可以直接使用异步Handler来取代同步Handler了?是,也不是。

同步Handler有一个特点是会遵循与绘制任务的顺序,设置同步屏障之后,会等待绘制任务完成,才会执行同步任务;而异步任务与绘制任务的先后顺序无法保证,在等待VSYNC的期间可能被执行,也有可能在绘制完成之后执行。因此,我的建议是:如果需要保证与绘制任务的顺序,使用同步Handler;其他,使用异步Handler

参考文章:

Handler机制——同步屏障_maove的博客-CSDN博客_同步屏障


http://www.niftyadmin.cn/n/61608.html

相关文章

【Linux】Rsync基于rsync-daemon认证的使用(rsync服务端与客户端发配置、排错思路、使用示例、优缺点及使用场景)

一、Rsync基于rsync-daemon认证的使用与 ssh 认证不同&#xff0c;rsync 协议认证不需要依赖远程主机的 sshd 服务&#xff0c;但需要远程主机开启 rsyncd 服务&#xff0c;本地 rsyncd 服务可不必开启。另外&#xff0c;rsync 协议认证不是直接使用远程主机的真实系统账号&…

nvm安装后出现‘node‘ 不是内部或外部命令,也不是可运行的程序 或批处理文件

出现这个问题多半是path地址不对。 打开系统环境变量。看看path里面有没有&#xff1f;没有的话&#xff0c;加上就行&#xff01; 我的报错原因就是因为path里没有自动加上nvm的相关路径。 注意项&#xff1a; 1&#xff0c;在安装nvm之前&#xff0c;提前要把本机以前安装…

TS 函数重载你还不会?来!我教你

前言&#xff1a; 今天在项目中遇到了后端接口参数类型和接口返回值需要修改的场景&#xff0c;由于这个函数在很多页面都用到了&#xff0c;就导致改完相关 api 函数的时候 TS 疯狂报错&#xff0c;所有的参数和返回值都需要跟着改&#xff0c;一时间头疼。正当我手足无措的时…

企业实施CMMI中 常见的4大问题

1. CMMI模型的理解和应用不够深入 CMMI模型是一个复杂的模型&#xff0c;它涉及到许多不同的方面&#xff0c;如组织结构、流程、技术、管理等&#xff0c;因此&#xff0c;要想深入理解和应用CMMI模型&#xff0c;需要花费大量的时间和精力。 企业实施CMMI常见4大问题&#xf…

【Python实战】神仙运气—快看看你的彩票:2千多万元大奖无人领,马上就过期了,下一期的中奖者会是你吗?(纯技术交流)

前言 越努力越幸运 哈喽~我是栗子同学&#xff01; 特别注意&#xff1a;不管是沉迷赌球&#xff0c;还是沉迷购彩&#xff0c;都是不可取的。本文纯是一个技术学习内容。 听说关注我的人会暴富哦&#xff01;、 所有文章完整的素材源码都在&#x1f447;&#x1f447; 粉丝…

2023新华为OD机试题 - 压缩报文还原(JavaScript) | 刷完必过

压缩报文还原 题目 为了提升数据传输的效率,会对传输的报文进行压缩处理。 输入一个压缩后的报文,请返回它解压后的原始报文。 压缩规则:n[str],表示方括号内部的 str 正好重复 n 次。 注意 n 为正整数(0 < n <= 100),str只包含小写英文字母,不考虑异常情况。 …

2.13、进程互斥的硬件实现方法

1、中断屏蔽方法 利用 “开/关中断指令” 实现&#xff08;与原语的实现思想相同&#xff0c;即在某进程开始访问临界区到结束访问为止都不允许被中断&#xff0c;也就不能发生进程切换&#xff0c;因此也不可能发生两个同时访问临界区的情况&#xff09; 优点&#xff1a;简单…

sql数据库

1、在表里stock_code_no后加入 stock_code字段ALTER TABLE tb_rawdata_pdsepo_asac_otc_posi ADD stock_code VARCHAR ( 16 ) NOT NULL AFTER stock_code_no;2、删除tb_rawdata_pdsepo_asac_posi表中stock_code_no索引DROP INDEX idx_tb_rawdata_pdsepo_asac_posi_1 ON tb_rawd…