app卡顿系列一 :Handler同步屏障

2,925 阅读3分钟

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」。

什么是Handler同步屏障

什么是Hander同步屏障,准确的说应该是消息队列的同步屏障。同步屏障能够过滤消息队列中的同步消息,让异步消息优先被处理。

同步屏障的插入

MessageQueue通过postSyncBarrier来给消息队列初入屏障。

public int postSyncBarrier() {
        return postSyncBarrier(SystemClock.uptimeMillis());
    }
​
    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;
​
            Message prev = null;
            Message p = mMessages;
            if (when != 0) {
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            if (prev != null) { // invariant: p == prev.next
                msg.next = p;
                prev.next = msg;
            } else {
                msg.next = p;
                mMessages = msg;
            }
            return token;
        }
    }

可以看到,插入屏障消息的实现逻辑非常简单,就是按照时间规则按顺序将msg 插入到消息队列。那么为什么这个msg能够成为同步屏障呢?

普通消息插入

所有通过handler插入消息队列的msg最后都会调用Handler#enqueueMessage

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

MessageQueue#enqueueMessage

 boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }
​
        synchronized (this) {
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }
​
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }
​
            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

可以看到普通消息在插入的时候Msg.target 必须不为空,同时按照时间的先后顺序插入消息队列。

同步&异步消息

要将一个消息设置成异步的,可以调用Message#setAsynchronous

public void setAsynchronous(boolean async) {
        if (async) {
            flags |= FLAG_ASYNCHRONOUS;
        } else {
            flags &= ~FLAG_ASYNCHRONOUS;
        }
    }

同步屏障的工作过程

我们知道当lopper循环后,会一直调用MessageQueue#next方法来获取下一个消息

Message next() {
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported.
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }
​
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
​
            nativePollOnce(ptr, nextPollTimeoutMillis);
​
            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                //如果存在同步屏障消息,那么向下查找一个合适的异步消息进行处理。
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    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;
                }
//省略代码
        }
    }

代码的逻辑是:如果有同步屏障消息,那么循环消息队列,获取一个异步消息并进行处理。

注意:同步屏障的添加和移除应该是成对出现的,如果过同步屏障一直存在那么同步消息不会被处理。

系统对于同步屏障的使用

在ViewRootImpl中一旦需要进行ui更新,就会插入一个同步屏障消息,来保证ui消息在主线程中优先被处理。

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
       //省略代码
    }
}

移除同步屏障

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        //进行布局
        performTraversals();
    }
}

总结:

系统利用同步屏障使ui消息优先被处理,来提升ui的流程性,我们平时开发的过程中,对应同步屏障的插入,系统是用@hide进行隐藏的。同时对于异步消息,应该谨慎使用,因为它比同步消息更加容易造成app卡顿。