Android Handler同步屏障

什么是同步屏障机制

同步屏障机制是一套为了让某些特殊的消息得以更快被执行的机制。

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

MessageQueue中的Message,有一个变量isAsynchronous,他标志了这个Message是否是异步消息;标记为true称为异步消息,标记为false称为同步消息。同时还有另一个变量target,标志了这个Message最终由哪个Handler处理。

从Handler源码我们知道,每一个Message在被插入到MessageQueue中的时候,会强制其target属性不能为null,如下代码:

java 复制代码
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,也就是说我们一般发送的消息都为同步消息。

那么什么是异步消息呢,如何发送一个异步消息呢?

简单来说有两种方式。

一种是直接设置消息为异步的:

java 复制代码
Message msg = mMyHandler.obtainMessage();
msg.setAsynchronous(true);
mMyHandler.sendMessage(msg);

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

java 复制代码
public Handler(boolean async) {
     this(null, async);
}

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

java 复制代码
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发出来的消息则全是异步的。

java 复制代码
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()从消息队列中取消息,来看看关键代码

java 复制代码
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(),如下:

java 复制代码
//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()。

java 复制代码
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。

👀关注公众号:Android老皮!!!欢迎大家来找我探讨交流👀

相关推荐
传奇开心果编程23 分钟前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
嘎嘎风2 小时前
MiniMind 学习笔记之 06 一张图、一百行:MiniMind 的骨架
算法·源码
传奇开心果编程3 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程5 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程5 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程5 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓5 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程5 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
supabc1236 小时前
Celium:连接 Windows、Mac、Linux 与 Android,让远程访问和设备管理更简单
android·linux·windows·macos·远程访问·网络管理·celium
Dovis(誓平步青云)6 小时前
导览音频切换太快,旧讲解不能覆盖新展品
android·前端·javascript·ecmascript·音视频·宠物