Handler系列——同步屏障(三)

看同步屏障之前首先要对Handler有一定的了解,可以先看这篇文章:
Handler又是什么鬼东西(附源码)

定义

大家应该知道,线程的消息都是放到同一个MessageQueue里面,取消息的时候是互斥取消息,而且只能从头部取消息,而添加消息是按照消息的执行的先后顺序进行的排序。那么问题来了,同一个时间范围内的消息,如果它是需要立刻执行的,那我们应该怎么办,按照常规的办法,我们需要等到队列轮询到我自己的时候才能执行,但是显然达不到我们立即执行的需求,所以,我们需要给紧急需要执行的消息创建一个绿色通道,这个绿色通道就是同步屏障的概念。

顾名思义,同步屏障就是给同步消息设置屏障,让异步消息(需要加急处理得消息)优先执行。消息分为同步消息(也就是普通消息)异步消息屏障消息

同步消息:我们平常正常使用,发送得消息其实就是同步消息,在enqueueMessage中将handler赋值给了msg的target,msg.target=handler
异步消息:设置了async异步消息标志的消息
屏障消息target==null的消息就是屏障消息

1. 同步屏障如何开启

我们可以在MessageQueuepostSyncBarrier()方法中开启同步屏障,源码如下:

// MessageQueue.java
/**
 * @hide
 */
public int postSyncBarrier() {
    return postSyncBarrier(SystemClock.uptimeMillis());
}
 
private int postSyncBarrier(long when) {
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        msg.markInUse();
        //就是这里!!!初始化Message对象的时候,并没有给target赋值,因此 target==null
        msg.when = when;
        msg.arg1 = token;
 
        Message prev = null;
        Message p = mMessages;
        // 把当前需要执行的Message全部执行
        if (when != 0) {
        //如果开启同步屏障的时间(假设记为T)T不为0,且当前的同步消息里有时间小于T,则prev也不为null
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        // 根据prev是不是为null,将 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,也就是往消息队列中插入了一个屏障消息。

2. 开启同步屏障后,异步消息是怎么处理的呢?

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

// MessageQueue.java
Message next() {
    ...
 
    // 阻塞时间
    // 1.如果nextPollTimeoutMillis=-1,一直阻塞不会超时。
    // 2.如果nextPollTimeoutMillis=0,不会阻塞,立即返回。
    // 3.如果nextPollTimeoutMillis>0,最长阻塞nextPollTimeoutMillis毫秒(超时)
    // 如果期间有程序唤醒会立即返回。
    int nextPollTimeoutMillis = 0;
    for (;;) {
        ...
        // 阻塞对应时间 
        nativePollOnce(ptr, nextPollTimeoutMillis);
  // 对MessageQueue进行加锁,保证线程安全
        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
             //关键!!!
             //如果target==null,那么它就是屏障,需要循环遍历,一直往后找到第一个异步的消息
            if (msg != null && msg.target == null) {
                // 同步屏障,找到下一个异步消息
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
            	//如果有消息需要处理,先判断时间有没有到,如果没到的话设置一下阻塞时间,
                //场景如常用的postDelay
                if (now < msg.when) {
                    //计算出离执行时间还有多久赋值给nextPollTimeoutMillis,
                    //表示nativePollOnce方法要等待nextPollTimeoutMillis时长后返回
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // 获得消息且现在要执行,标记MessageQueue为非阻塞
                    mBlocked = false;
                    
                    // 一般只有异步消息才会从中间拿走消息,同步消息都是从链表头获取
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    msg.markInUse();
                    return msg;
                }
            } else {
                // 没有消息,进入阻塞状态
                nextPollTimeoutMillis = -1;
            }
 
            // 当调用Looper.quitSafely()时候执行完所有的消息后就会退出
            if (mQuitting) {
                dispose();
                return null;
            }
            ...
        }
        ...
    }
}

从上面可以看出,当消息队列开启同步屏障的时候(即标识为msg.target == null),那么会循环遍历整个链表找到标记为异步消息的Message,即isAsynchronous返回true,其他的消息会直接忽视,那么异步消息就会提前被执行了。这样,同步屏障就起到了一种过滤和优先级的作用。

如下图所示,屏障消息插入后,优先处理异步消息:
同步屏障

注意,同步屏障不会自动移除,使用完成之后需要手动进行移除,即调用removeSyncBarrier(),不然会造成同步消息无法被处理。从源码中可以看到如果不移除同步屏障,那么他会一直在那里,这样同步消息就永远无法被执行了。
唤醒:有了同步屏障,那么唤醒的判断条件就必须再加一个:MessageQueue中有同步屏障且处于阻塞中,此时在所有异步消息前插入新的异步消息。这个也很好理解,跟同步消息是一样的。如果把所有的同步消息先忽视,就是插入新的链表头且队列处于阻塞状态,这个时候就需要被唤醒了。
再来看一下唤醒模块代码:

// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
    ...
 
    // 对MessageQueue进行加锁
    synchronized (this) {
        ...
        if (p == null || when == 0 || when < p.when) {
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // 1. 当线程被阻塞,且目前有同步屏障,且入队的消息是异步消息
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    break;
                }
                // 2. 如果找到一个异步消息,说明前面有延迟的异步消息需要被处理,不需要被唤醒
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            msg.next = p; 
            prev.next = msg;
        }
  
        // 如果需要则唤醒队列
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

注释1处表明,如果当前处于阻塞状态,有同步屏障,并且插入的是一个异步消息,那么就需要唤醒。
注释2处表示,如果当前插入的异步消息的位置不是所有异步消息之前,那么不需要唤醒。

3. 如何发送一个异步消息

发送异步消息有两种办法:

  • 使用异步类型的Handler发送的全部Message都是异步的
  • 给Message标志异步

Handler有一系列带Boolean类型的参数的构造器,这个参数就是决定是否是异步Handler:

// Handler.java
/**
  *
  * @hide
  */
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    // 这里赋值
    mAsynchronous = async;
}

但是异步类型的Handler构造器是标记为hide,我们无法使用,所以我们使用异步消息只有通过给Message设置异步标志:

// Handler.java
// 源码
public void setAsynchronous(boolean async) {
    if (async) {
        flags |= FLAG_ASYNCHRONOUS;
    } else {
        flags &= ~FLAG_ASYNCHRONOUS;
    }
}

// 代码中设置
Message msg = mMyHandler.obtainMessage();
msg.setAsynchronous(true);
mMyHandler.sendMessage(msg);

4. 同步屏障应用场景

在我们日常的开发中,其实很少用到同步屏障,那么系统中什么时候会用到同步屏障呢?Android 系统中的 UI 更新相关的消息即为异步消息,每16ms刷新一次页面,需要优先处理。
比如,在 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();
    }
}

先通过mHandler.getLooper().getQueue().postSyncBarrier();开启同步屏障,再通过mChoreographer.postCallback()最终调用到Choreographer.postCallbackDelayedInternal(),在注释处发送异步消息。
由于 UI 更新相关的消息是优先级最高的,这样系统就会优先处理这些异步消息。

//Choreographer.java

private void postCallbackDelayedInternal(int callbackType,Object action, Object token, long delayMillis) {
    if (DEBUG_FRAMES) {
        Log.d(TAG, "PostCallback: type=" + callbackType
                + ", action=" + action + ", token=" + token
                + ", delayMillis=" + 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);
        }
    }
}

5. 移除同步屏障

同步屏障既然有添加,肯定要有移除,否则同步消息永远无法执行,调用ViewRootImpl.unscheduleTraversals()最终调用removeSyncBarrier()移除同步屏障。

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

/**
 * @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;
        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;
            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);
        }
    }
}

6. 小结

同步屏障就是屏蔽同步消息,让异步消息优先处理的逻辑。
MessageQueue.enqueueMessage()中可以看到,当msg.target == null的时候,轮询遍历消息队列寻找异步消息。

所以开启同步屏障需要msg.target == null,我们可以通过调用MessageQueuepostSyncBarrier()开启同步屏障。

发送异步消息需要为msg设置async,即调用msg的setAsynchronous(true)设置消息为异步消息,这样就可以使用同步屏障了。

当处理完成异步消息后,记得关闭同步屏障,调用MessageQueueremoveSyncBarrier(),移除同步屏障,使同步消息正常运行。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值