Android Handler同步屏障
  mkIZDEUN7jdf 2023年11月02日 93 0

什么是同步屏障机制

同步屏障机制是一套为了让某些特殊的消息得以更快被执行的机制。 这里我们假设一个场景:我们向主线程发送了一个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。

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
mkIZDEUN7jdf