Android-Handler同步屏障

一、消息机制之同步屏障

消息机制的同步屏障,其实就是阻碍同步消息,只让异步消息通过。而开启同步屏障的方法就是调用下面的方法:

MessageQueue#postSyncBarrier()

源码如下:

@TestApi
public int postSyncBarrier() {
    // 这里传入的时间是从开机到现在的时间戳
    return postSyncBarrier(SystemClock.uptimeMillis());
}
/**
 * 这就是创建的同步屏障的方法
 * 同步屏障就是一个同步消息,只不过这个消息的target为null
 */
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++;
        // 从消息池中获取Message
        final Message msg = Message.obtain();
        msg.markInUse();
        // 初始化Message对象的时候,并没有给Message.target赋值,
        // 因此Message.target==null
        msg.when = when;
        msg.arg1 = token;

        Message prev = null;
        Message p = mMessages;
        if (when != 0) {
            // 这里的when是要加入的Message的时间
            // 这里遍历是找到Message要加入的位置
            while (p != null && p.when <= when) {
                // 如果开启同步屏障的时间(假设记为T)T不为0,且当前的同步
                // 消息里有时间小于T,则prev也不为null
                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的来源就找得到了。这样就可以插入一条target==null的消息,这个消息就是一个同步屏障。
那么开启消息屏障后,所谓的异步消息又是如何处理的呢?
消息的最终处理其实都是在消息轮询器Looper#loop()中,而loop()循环中会调用MessageQueue#next()从消息队列中进行取消息。

Message next() {
    ...

    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    // 1.如果nextPollTimeoutMillis=-1,一直阻塞不会超时
    // 2.如果nextPollTimeoutMillis=0,不会阻塞,立即返回
    // 3.如果nextPollTimeoutMillis>0,最长阻塞nextPollTimeoutMillis毫秒(超时)
    int nextPollTimeoutMillis = 0;
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }

        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
            // 获取系统开机到现在的时间戳
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            // 取出target==null的消息
            // 如果target==null,那么它就是屏障,需要循环遍历,
            // 一直往后找到第一个异步消息,即msg.isAsynchronous()为true
            // 这个target==null的消息,不会被取出处理,一直会存在
            // 每次处理异步消息的时候,都会从头开始轮询
            // 都需要经历从msg.target开始的遍历
            if (msg != null && msg.target == null) {
                // 使用一个do..while循环
                // 轮询消息队列里的消息,这里使用do..while循环的原因
                // 是因为do..while循环中取出的这第一个消息是target==null的消息
                // 这个消息是同步屏障的标志消息
                // 接下去进行遍历循环取出Message.isAsynchronous()为true的消息
                // isAsynchronous()为true就是异步消息
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                // 如果有消息需要处理,先判断时间有没有到,如果没有到的话设置阻塞时间
                if (now < msg.when) {
                    // 计算出离执行时间还有多久赋值给nextPollTimeoutMillis
                    // 表示nativePollOnce方法要等待nextPollTimeoutMillis时长后返回
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // 获取到消息
                    mBlocked = false;
                    // 链表操作,获取msg并且删除该节点
                    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 {
                // 没有消息,nextPollTimeoutMillis复位
                nextPollTimeoutMillis = -1;
            }

            ...
        }
    }
}

从上面的MessageQueue.next方法可以看出,当消息队列开启同步屏障的时候(即标识为msg.target==null),消息机制在处理消息的时候,会优先处理异步消息。这样,同步屏障就起到了一种过滤和优先级的作用。


Handler消息机制同步屏障.png

如果上图所示,在消息队列中有同步消息和异步消息(黄色部分)以及一道墙(同步屏障--红色部分)。有了同步屏障的存在,msg_2和msg_M这两个异步消息可以被优先处理,而后面的msg_3等同步消息则不会被处理。那么这些同步消息什么时候可以被处理呢?就需要先移除这个同步屏障,即调用MessageQueue#removeSyncBarrier()

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

二、同步屏障的应用场景

同步屏障一般在日常开发中比较少用,而在系统源码中就有使用。Android系统中的UI更新相关的消息即为异步消息,需要优先处理。
16ms左右刷新UI,而是60hz的屏幕,即1s刷新60次。
在Android中什么是异步消息?即给:

Message.setAsynchronous(true);

比如,在View更新时,draw、requestLayout、invalidate等很多地方都调用了。ViewRootImpl#scheduleTraversals()。

@UnsupportedAppUsage
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        // 开启同步屏障
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // 发送异步消息
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

在这里,mChoreographer.postCallback最终会执行到了Choreographer#postCallbackDelayedInternal()

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);
        }
    }
}

可以看到,这里就开启了同步屏障,并且发送了异步消息。由于UI相关的消息是优先级最高的,这样系统就会优先处理这些异步消息。
最后,当然要移除同步屏障的时候,调用ViewRootImpl#unscheduleTraversals

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

在ViewRootImpl中的doTraversal()方法中也会移除同步屏障,这里移除是因为requestLayout或者invalidate的时候,刷新之后,在doTraversal()中就会移除同步屏障,因为此时消息已经发送并且处理了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容