Android Handler同步屏障

什么是同步屏障机制

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

MessageQueue中的Message,有一个变量isAsynchronous,他标志了这个Message是否是异步消息;标记为true称为异步消息,标记为false称为同步消息。同时还有另一个变量target,标志了这个Message最终由哪个Handler处理。
从Handler源码我们知道,每一个Message在被插入到MessageQueue中的时候,会强制其target属性不能为null,如下代码:

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) {
   // 这里要注意target指向了当前Handler
     msg.target = this;
 if (mAsynchronous) {
     msg.setAsynchronous(true);
 }
 // 调用到了queue#enqueueMessage方法
 return queue.enqueueMessage(msg, uptimeMillis);

}
这里msg.target 就会被赋值为this, 而 this 即为我们的 Handler 对象。因此,通过这种方式传进来的消息的 target 肯定也就不为 null,并且 mAsynchronous 默认为 false,也就是说我们一般发送的消息都为同步消息。
那么什么是异步消息呢,如何发送一个异步消息呢?
简单来说有两种方式。
一种是直接设置消息为异步的:

Message msg = mMyHandler.obtainMessage();
msg.setAsynchronous(true);
mMyHandler.sendMessage(msg);

还有一个需要用到 Handler 的一个构造方法,不过该方法已被标记为@Hide了:

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

但在api28之后添加了两个重要的方法:

public static Handler createAsync(@NonNull Looper looper) {
    if (looper == null) throw new NullPointerException("looper must not be null");
    return new Handler(looper, null, true);
}

public static Handler createAsync(@NonNull Looper looper, @NonNull Callback callback) {
    if (looper == null) throw new NullPointerException("looper must not be null");
    if (callback == null) throw new NullPointerException("callback must not be null");
    return new Handler(looper, callback, true);
}

通过这两个api就可以创建异步Handler了,而异步Handler发出来的消息则全是异步的。

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

但是没有同步屏障,异步消息与同步消息的执行并没有什么区别。

同步屏障

同步屏障究竟有什么作用?

同步屏障为handler消息机制提供了一种优先级策略,让异步消息的优先级高于同步消息。如何开启同步屏障呢?

MessageQueue.postSyncBarrie(),该方法会往MessageQueue中插入一条同步屏障message,没有给Message赋值target属性,且插入到Message队列头部。当然源码中还涉及到延迟消息,我们暂时不关心。这个target==null的特殊Message就是同步屏障。

MessageQueue在获取下一个Message的时候,如果碰到了同步屏障,那么不会取出这个同步屏障,而是会遍历后续的Message,找到第一个异步消息取出并返回。这里跳过了所有的同步消息,直接执行异步消息。为什么叫同步屏障?因为它可以屏蔽掉同步消息,优先执行异步消息。
消息的最终处理是在消息轮询器Looper.loop()中,而loop()循环中会调用MessageQueue.next()从消息队列中取消息,来看看关键代码

Message next() {
        for (;;) {
            nativePollOnce(ptr, nextPollTimeoutMillis);
            synchronized (this) {
                Message msg = mMessages;
                //如果msg.target为空,也就是说是一个同步屏障消息,则进入这个判断里面
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    //在这个while循环中,找到最近的一个异步消息
                    //先执行do,再执行while,所以屏障消息不会被取出
                    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) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        //返回消息
                        return msg;
                    }
                } else {
                    // No more messages.
                    //没有找到消息则进入阻塞状态,等待被唤醒
                    nextPollTimeoutMillis = -1;
                }
                //...
    }


从上面可以看出,当执行到同步屏障消息(即标识为msg.target == null)时,消息机制优先处理异步消息。由于代码中先执行do再执行while,第一次就把指针指向了同步屏障消息的下一条消息,所以同步屏障消息会一直在消息队列中。
注意,同步屏障不会自动移除,使用完成之后需要手动进行移除,不然会造成同步消息无法被处理

同步屏障使用场景

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

Android 系统中更新UI就是使用同步屏障。
在 View 更新时,draw、requestLayout、invalidate 等很多地方都调用了ViewRootImpl#scheduleTraversals(),如下:

//ViewRootImpl.java
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            //发送同步屏障消息
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //发送异步消息
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

这里就发送了同步屏障消息,并发送了异步消息,由于 UI 更新相关的消息优先级是最高的,这样系统就会优先处理这些异步消息。
前面我们看到,同步屏障消息并不会自己移除,需要调用相关代码来移除同步屏障消息ViewRootImpl#unscheduleTraversals()。

void unscheduleTraversals() {
     if (mTraversalScheduled) {
         mTraversalScheduled = false;
         //移除同步屏障消息
         mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
         mChoreographer.removeCallbacks(
                 Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
     }
 }

 /**
  * Removes a synchronization barrier.
  *
  * @param token The synchronization barrier token that was returned by
  * {@link #postSyncBarrier}.
  *
  * @throws IllegalStateException if the barrier was not found.
  *
  */
 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;   //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);
         }
     }
 }

在绘制流程中使用同步屏障,保证了在vsync信号到来时,绘制任务可以被及时执行,避免造成界面卡顿。但这样也带来了相对应的代价:

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

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

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

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

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

我们以后怎么选择使用异步Handler来还是同步Handler呢?

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

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题

图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值