Android消息机制之同步屏障

1、Message

Android中的Message分为三种:

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

它们都是Message,只是成员变量有些区别。

一般我们通过 Handler 发送消息(如调用Handler.sendMessage(@NonNull Message msg)等),最终都会调用 Handler.enqueueMessage()让消息入队,如下:

java 复制代码
 private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

这里msg.target 会被赋值为this, 而 this 即为我们的 Handler 对象,并且Message的成员变量 mAsynchronous 默认为 false,这种消息是同步消息,我们一般发送的消息都是同步消息。

相对应也应该有异步消息吧?的确,还有一种很容易被忽略的 异步消息,因为除了系统源码外,我们一般很少会使用异步消息。那么,如何发送一个异步消息呢?

简单来说有两种方式。

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

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

还有一种需要用到 Handler 的构造方法,不过该方法已被标记为@hide了,普通应用无法使用:

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

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

2、同步屏障

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

同步屏障为handler消息机制提供了一种优先级策略,让异步消息的优先级高于同步消息。如何开启同步屏障呢?通过MessageQueue.postSyncBarrie()可以开启同步屏障:

java 复制代码
    /**
     * Posts a synchronization barrier to the Looper's message queue.
     *
     * Message processing occurs as usual until the message queue encounters the
     * synchronization barrier that has been posted.  When the barrier is encountered,
     * later synchronous messages in the queue are stalled (prevented from being executed)
     * until the barrier is released by calling {@link #removeSyncBarrier} and specifying
     * the token that identifies the synchronization barrier.
     *
     * This method is used to immediately postpone execution of all subsequently posted
     * synchronous messages until a condition is met that releases the barrier.
     * Asynchronous messages (see {@link Message#isAsynchronous} are exempt from the barrier
     * and continue to be processed as usual.
     *
     * This call must be always matched by a call to {@link #removeSyncBarrier} with
     * the same token to ensure that the message queue resumes normal operation.
     * Otherwise the application will probably hang!
     *
     * @return A token that uniquely identifies the barrier.  This token must be
     * passed to {@link #removeSyncBarrier} to release the barrier.
     *
     * @hide
     */
    @UnsupportedAppUsage
    @TestApi
    public int postSyncBarrier() {
        return postSyncBarrier(SystemClock.uptimeMillis());
    }

postSyncBarrier()方法前面有一大段注释,大概意思是该方法会往MessageQueue中插入一条同步屏障message,遇到该message后,MessageQueue中的同步消息会延迟执行,直到通过调用removeSyncBarrier()方法移除同步屏障;而异步消息有豁免权,可以正常执行。

java 复制代码
    // The next barrier token.
    // Barriers are indicated by messages with a null target whose arg1 field carries the token.
    //这里有解释:同步屏障是一个target为null并且arg1为这个token的消息
    @UnsupportedAppUsage
    private int mNextBarrierToken;
  
    private int postSyncBarrier(long when) {
        // Enqueue a new sync barrier token
        // We don't need to wake the queue because the purpose of a barrier is to stall it.
        synchronized (this) {
            final int token = mNextBarrierToken++; //为token赋值
            final Message msg = Message.obtain();
            msg.markInUse();

            //这里初始化Message对象的时候没有给target赋值, 即target==null
            msg.when = when;
            msg.arg1 = token;

            Message prev = null;
            Message p = mMessages;

            if (when != 0) {
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            //将msg插入消息队列
            if (prev != null) { // invariant: p == prev.next
                msg.next = p;
                prev.next = msg;
            } else {
                msg.next = p;
                mMessages = msg;
            }
            return token;
        }
    }

可以看到,Message 对象初始化的时候并没有给 target 赋值,因此同步屏障消息的target == null

那么插入同步屏障消息后,异步消息是如何被优先处理的呢?

如果对消息机制有所了解的话,应该知道消息的最终处理是在消息轮询器Looper.loop()中,而loop()循环中会调用MessageQueue.next()从消息队列中取消息,来看看关键代码:

java 复制代码
@UnsupportedAppUsage
Message next() {
        for (;;) {
            nativePollOnce(ptr, nextPollTimeoutMillis);
            synchronized (this) {
                Message msg = mMessages;
                //如果msg.target为null,是一个同步屏障消息,则进入这个判断
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    //先执行do,再执行while,遇到同步消息会跳过,遇到异步消息退出循环
                    //即取出的msg为该屏障消息后的第一条异步消息,屏障消息不会被取出
                    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;
                }
                ...
    }

从上面的代码可以看出,当执行到同步屏障消息(即target == null的Message)时,消息机制优先处理异步消息。由于代码中先执行do再执行while,取出的msg为该屏障消息后的第一条异步消息,而屏障消息不会被取出

下面用示意图简单说明:

如上图所示,在消息队列中有同步消息和异步消息(黄色部分)以及同步屏障消息(红色部分)。当执行到同步屏障消息的时候,msg_2 和 msg_M 这两个异步消息会被优先处理,而msg_3 等同步消息则要等异步消息处理完后再处理。

3、同步屏障的使用场景

在日常的应用开发中,我们很少会用到同步屏障。Android 系统源码中UI更新就是使用的同步屏障,这样会优先处理更新UI的消息,尽量避免造成界面卡顿。

UI更新都会调用ViewRootImpl.scheduleTraversals(),其代码如下:

java 复制代码
public final class ViewRootImpl{

  @UnsupportedAppUsage
  void scheduleTraversals() {
     if (!mTraversalScheduled) {
         mTraversalScheduled = true;
         //往队列中插入同步屏障消息
         mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
         //往队列中插入异步消息
         mChoreographer.postCallback(
                 Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
         if (!mUnbufferedInputDispatch) {
             scheduleConsumeBatchedInput();
         }
         notifyRendererOfFramePending();
         pokeDrawLockIfNeeded();
     }
  }
  
}

Choreographer.postCallback()最终调用了postCallbackDelayedInternal()方法:

java 复制代码
public final class Choreographer {

	private void postCallbackDelayedInternal(int callbackType,
	            Object action, Object token, long delayMillis) {
	        ...
	        synchronized (mLock) {
	            final long now = SystemClock.uptimeMillis();
	            final long dueTime = now + delayMillis;
	            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
	
	            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); 
	            }
	        }
	    }
}

这里就往队列中插入了同步屏障消息,然后又插入了异步消息,UI 更新相关的消息就可以优先得到处理。

前面代码中我们看到,同步屏障消息并不会自己移除,所以需要调用相关代码来移除同步屏障消息,让同步消息可以正常执行。Android源码中在执行绘制流程之前执行了移除同步屏障的代码:

java 复制代码
public final class ViewRootImpl{
   void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            //移除同步屏障消息
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
			...
			//里面执行了绘制的3大流程
            performTraversals();
			...			
        }
    }
}

public final class MessageQueue {
    /**
     * 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
     */
    @UnsupportedAppUsage
    @TestApi
    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;
            //找到同步屏障消息p
            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的下一条消息
                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);
            }
        }
     }
   } 
}

通过以上的分析,对于同步屏障的原理已经了解了吧。在绘制流程中使用同步屏障,保证了在VSYNC信号到来时,绘制任务可以及时执行,避免造成界面卡顿。

相关推荐
m0_471199637 分钟前
【小程序】订单数据缓存 以及针对海量库存数据的 懒加载+数据分片 的具体实现方式
前端·vue.js·小程序
编程大师哥8 分钟前
Java web
java·开发语言·前端
A小码哥10 分钟前
Vibe Coding 提示词优化的四个实战策略
前端
Murrays10 分钟前
【React】01 初识 React
前端·javascript·react.js
大喜xi13 分钟前
ReactNative 使用百分比宽度时,aspectRatio 在某些情况下无法正确推断出高度,导致图片高度为 0,从而无法显示
前端
helloCat14 分钟前
你的前端代码应该怎么写
前端·javascript·架构
电商API_1800790524714 分钟前
大麦网API实战指南:关键字搜索与详情数据获取全解析
java·大数据·前端·人工智能·spring·网络爬虫
康一夏15 分钟前
CSS盒模型(Box Model) 原理
前端·css
web前端12316 分钟前
React Hooks 介绍与实践要点
前端·react.js
我是小疯子6616 分钟前
JavaScriptWebAPI核心操作全解析
前端