Android面试Android Framework

Android Handler源码解析-同步屏障

2021-07-15  本文已影响0人  爱是空白日记

上一篇Android Handler源码解析-消息机制

前言

在平时正常开发过程中我们很少会遇到使用同步屏障,甚至可能都不知道有这么个东西,但是要深入了解Handler机制,同步屏障机制却是必不可少的部分。那么什么是同步屏障?

什么是同步屏障

上一篇文章说过,MessageQueue不是一个队列,但是MessageQueue符合队列的出队定义,那么出队的时候由于每条Message的执行时间不同,就有可能造成队列拥挤。本来是某时刻需要执行的一条消息可能就没法准时执行,若这条消息是屏幕触摸,屏幕刷新之类的消息就会造成操作延迟,显然这是系统不愿意看到的,于是同步屏障机制就出现了。

Handler消息分为三种

  1. 同步消息
  2. 异步消息
  3. 屏障消息

我们之前介绍的消息都属于同步消息,而异步消息和屏障消息实际上是组合使用的,使用异步消息的同时也会用到屏障消息。
所谓同步屏障,就是在MessageQueue里设置一道屏障,阻碍同步消息通过,允许异步消息通过。

异步消息

先来看看怎么设置异步消息

//#Handler
    public Handler(@Nullable Callback callback, boolean async) {
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;//
        mCallback = callback;
        //是否异步消息
        mAsynchronous = async;
    }

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();
        //设置msg为异步消息
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

设置异步消息非常简单,Message#setAsynchronous设置为true就是一条异步消息,或者构造Handler时传入async为true,那么通过Handler发送的Message都是异步消息。

屏障消息

//#MessageQueue
    //设置屏障消息
    public int postSyncBarrier() {
        return postSyncBarrier(SystemClock.uptimeMillis());
    }

    //设置屏障消息,插入的位置为when。注意这是一个私有方法,需要反射调用
    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++;//屏障消息的身份令牌,代表一条屏障消息,移除屏障消息时用到
            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            msg.arg1 = token;

            //下面的操作和插入普通消息差不多。判断是否插入头结点,若不是则遍历链表,根据when插入到链表对应的位置
            Message prev = null;
            Message p = mMessages;
            if (when != 0) {
                //根据when遍历链表找到对应位置
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            if (prev != null) { // 插入对应位置
                msg.next = p;
                prev.next = msg;
            } else {//插入头部
                msg.next = p;
                mMessages = msg;
            }
            return token;
        }
    }

MessageQueue提供了一个postSyncBarrier方法设置屏障消息,这个屏障消息的位置在MessageQueue的队首,会拦截MessageQueue里的所有同步消息。若只想拦截在MessageQueue某个节点之后的同步消息,可以通过反射去调用postSyncBarrier有when参数的那个方法。
从代码可以看到,屏障消息是没有设置target的,那么屏障消息实际就是一个没有target的普通消息

同步屏障原理

同步屏障说起来很简单,那么开启同步屏障后到底是怎么过滤屏蔽掉同步消息的呢?

//#MessageQueue
    Message next() {
        int nextPollTimeoutMillis = 0;
        for (;;) {
            nativePollOnce(ptr, nextPollTimeoutMillis);
            synchronized (this) {
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                //#1 这里判断msg的target是否为null,如果是null,那msg就是屏障消息,然后会去取msg后面的第一条异步消息
                if (msg != null && msg.target == null) {
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());//判断msg的next是否是异步消息,如果不是则继续循环拿next,直到拿到异步消息或者没有next
                }
                //#2
                if (msg != null) {
                    if (now < msg.when) {//如果当前时间小于msg的时间点,说明msg执行时间还未到
                        //设置阻塞时间
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {//执行时间已到,从链表中取出msg这个节点并返回
                        mBlocked = false;
                        if (prevMsg != null) {//prevMsg不为空,说明msg是异步消息
                            prevMsg.next = msg.next;//将msg的next设为msg上一个消息的next,等于是从消息链表中取出msg
                        } else {//同步消息,msg就是mMessages
                            mMessages = msg.next;//将msg的next设为新的mMessages,等于是从消息链表中取出头结点msg
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {//MessageQueue没有消息,进入阻塞
                    nextPollTimeoutMillis = -1;
                }
            nextPollTimeoutMillis = 0;
        }
    }

next()方法在上一篇博客里已经介绍过了,但当时还有#1处的代码没有介绍,这处代码就是关于屏障消息的。
可以看到,next的for循环每次都会将mMessages设为msg,所以#1处代码其实就是判断消息链表的头结点是否是屏障消息,如果不是屏障消息,就走正常流程#2。如果是屏障消息,就通过循环去取链表里面的第一条异步消息,之后也是走#2。
#2处代码,同步消息和异步消息不同之处在于,同步消息是断开头结点取出msg,而异步消息是从链表中间断开取出msg。

移除同步屏障

看完上面的文章,想必大家肯定能想到,移除同步屏障只要把屏障消息移除了就行。
的确如此,我们看代码。

    public void removeSyncBarrier(int token) {//token是设置屏障消息时生成的
        synchronized (this) {
            Message prev = null;
            Message p = mMessages;
            while (p != null && (p.target != null || p.arg1 != token)) {//如果不是屏障消息或者token不一致,就继续找next,直到找到要移除的屏障消息p
                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;
            //移除节点p
            if (prev != null) {
                prev.next = p.next;
                needWake = false;
            } else {
                mMessages = p.next;
                needWake = mMessages == null || mMessages.target != null;
            }
            //回收这个消息
            p.recycleUnchecked();
            if (needWake && !mQuitting) {
                nativeWake(mPtr);
            }
        }
    }

上面的代码简单来说,就是根据token找到对应的屏障消息,然后从链表删除这个屏障消息。

上一篇 下一篇

猜你喜欢

热点阅读