熟悉又陌生的Handler-2

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

熟悉又陌生的Handler-2

接上文:熟悉又陌生的Handler-1

enqueueMessage消息如何进入队列

Handler发送一个消息,最终就是把消息放入消息队列中:

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;
        }
        // 标记当前Message正在被使用。
        msg.markInUse();
        // when就是前面传入的希望消息被处理的时间,一样打包给message对象
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        // caseA:队列里边无消息了,或者消息预期被处理的时间是最早的,则当前插入的消息是队列头
        // Handler的sendMessageAtFrontOfQueue这个API就是通过when=0,
        // 来达到往消息队列头部插入消息的功能
        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 {
            // caseB: 队列里边还有消息
            // 这个时候是否需要唤醒队列的条件:
            // 队列处于挂起态 && 消息头是一个同步屏障消息(target为null)&& 新入队的消息是异步消息
            // 这个时候因为异步消息可以通过同步屏障,如果队列挂起了
            // 就需要唤醒队列保证异步消息能被取出
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            // 遍历整个消息队列
            for (;;) {
                prev = p;
                p = p.next;
                // 到了消息队列尾,或者新入队列的Message预期被处理的时间早于正在遍历的消息
                // 那么就找到了对应的位置
                if (p == null || when < p.when) {
                    break;
                }
                // 如果遍历的过程中发现新消息前面已经有了异步消息,那么不需要唤醒
                // 因为此时消息队列已经处于运行状态
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            // 队列插入
            msg.next = p; // invariant: p == prev.next
            prev.next = msg;
        }
        // 如果需要唤醒,调用nativeWake唤醒消息队列 ,实际实现后文再做分析
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}
复制代码

SyncBarrier同步屏障

上面的分析过程中,接触到了同步屏障这个概念,所谓的同步屏障,就是一种特殊的消息,当消息队列遇到同步屏障的时候,会优先取异步消息处理,屏蔽掉同步消息。

在ViewRootImpl.java中,有这样一行调用:

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        // 往MainLooper中发送一个同步屏障
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
            Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}
复制代码

postSyncBarrier实现如下:

private int postSyncBarrier(long when) {
    synchronized (this) {
        // token表示同步屏障的标识
        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;
    }
}
复制代码

有发送同步屏障的API,那么也就会有移除同步屏障的API(当然同步屏障系列API不允许被App调用),removeSyncBarrier:

public void removeSyncBarrier(int token) {
    synchronized (this) {
        Message prev = null;
        Message p = mMessages;
        // 根据token,找到待移除的同步屏障
        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.");
        }
        // needWake为false
        final boolean needWake;
        // 如果同步屏障是在队列中间,移除同步屏障。此时队列已经在循环了
        // 不需要执行唤醒操作
        if (prev != null) {
            prev.next = p.next;
            needWake = false;
        } else {
            // 如果同步屏障是在队列头部,将队列头指向同步屏障的下一条message
            mMessages = p.next;
            // 移除完队列头的同步屏障后
            // 如果消息队列空了,或者下一个消息不是同步屏障,唤醒消息循环
            needWake = mMessages == null || mMessages.target != null;
        }
        p.recycleUnchecked();
        // 如果需要唤醒消息循环,那么调用nativeWake 唤醒
        if (needWake && !mQuitting) {
            nativeWake(mPtr);
        }
    }
}
复制代码

next消息如何从队列中取出

分析完了在消息是如何进入队列的,我们再来看下,队列中的消息是如何被取出来的,上文在Looper.loop的分析中,我们知道了在loop方法中,通过调用next取出一条消息:

Message next() {
    // mPtr=0表示消息队列已经退出了,队列中自然也就没有消息需要被处理了
    final long ptr = mPtr;
    if (ptr == 0) {
        return null;
    }
    // 标识有多少IdleHandler需要处理
    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    // 下一个消息来到之前,需要等待的时长
    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,对应的message是同步屏障
            // 当遇到同步屏障的时候
            if (msg != null && msg.target == null) {
                // 取出第一条异步消息赋值给msg字段
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                // 如果当前时间戳小于正要被处理的消息的预期被处理时间
                // 那么表示此时队列还不应该去取出这个消息,设置等待时间
                if (now < msg.when) {
                     nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // 到这里就表示:取出了一条可被处理的消息
                    // 标记队列的挂起状态为false
                    mBlocked = false;
                    // 消息取出了,更新队列头
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    // 掐掉取出消息的next字段
                    msg.next = null;
                    // 标记这个消息正在被使用然后返回这个Message对象给loop去分发
                    msg.markInUse();
                    return msg;
                }
            } else {
                // 如果队列中没有消息了,那么就可能需要一直等待
                nextPollTimeoutMillis = -1;
            }
            // 如果队列正在推出,那么返回null
            if (mQuitting) {
                dispose();
                return null;
            }
            // 接下来进入idlehandler时间,当消息队列处于空闲状态的时候
            // 会消费这些idlehandler。
            // pendingIdleHandlerCount初始值为-1
            // 第二个条件表示没有消息或者消息队列处于空闲状态
            if (pendingIdleHandlerCount < 0
                    && (mMessages == null || now < mMessages.when)) {
                // 那么从ArrayList<IdleHandler>中知道有多少idleHandler需要处理
                pendingIdleHandlerCount = mIdleHandlers.size();
            }
            // 如果连idleHandler也没有,那么队列真的可以休息挂起了
            if (pendingIdleHandlerCount <= 0) {
                mBlocked = true;
                continue;
            }
            // 初始化一下mPendingIdleHandlers这个字段,size最大为4
            if (mPendingIdleHandlers == null) {
                mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
            }
            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
        }
        // 开始处理idleHandlers
        for (int i = 0; i < pendingIdleHandlerCount; i++) {
            // 一个一个取出idleHandlers
            final IdleHandler idler = mPendingIdleHandlers[i];
            mPendingIdleHandlers[i] = null; // release the reference to the handler
            boolean keep = false;
            try {
                // 执行idleHandler的任务,并根据queueIdle的返回值
                // 决定这个idleHandler的任务执行完了之后是不是要
                // 从消息队列的idleHandles中移除
                keep = idler.queueIdle();
            } catch (Throwable t) {
                Log.wtf(TAG, "IdleHandler threw exception", t);
            }
            // 如果需要移除,就remove
            if (!keep) {
                synchronized (this) {
                    mIdleHandlers.remove(idler);
                }
            }
        }
        // 重置idleHandler个数为0,以保证不会再次重复运行
        pendingIdleHandlerCount = 0;
        // 当调用处理了idleHandler时,新的Message可能已经被发送进来了
        // 或者其它消息的待处理时间到了,这个时候消息队列便不需要继续挂起
        nextPollTimeoutMillis = 0;
    }
}
复制代码

IdleHandler队列空闲时的Hook

上面提到了IdleHandler,我们可以通过这个API,实现当消息队列空闲的时候,做一些事情,使用也很简单,IdleHandler本身就是一个接口:

private MessageQueue.IdleHandler myIdleHandler = new MessageQueue.IdleHandler() {
    @Override
    public boolean queueIdle() {
        doSomething();
        return false;
    }
};
Looper.myQueue().addIdleHandler(myIdleHandler)
复制代码

需要注意的是,idleHandler是在Looper空闲的时候才会执行,执行时机是不可控的,使用的时候你应该明确知道你需要这个场景,举个例子比如一些必须的初始化任务,就不适合用IdleHandler来做,因为初始化任务都应该有一个明确的执行时机,使用IdleHandler去实现,如果MessageQueue中一直有消息要处理,那么没办法保证在对应的时机到了,对应的任务被执行。

可以使用IdleHandler的场景举例如下:

  1. Activity启动优化:onCreate,onStart,onResume中耗时较短但非必要的代码可以放到IdleHandler中执行,减少启动时间。
  2. 以往当我们希望在View绘制完了之后做一些操作的时候,我们可能是通过post系列API实现,实际上也可以通过IdleHandler实现。

小结:

至此,Java层的消息循环机制,大体流程已经结束了,其中涉及到了几个native调用,我们后文再做分析,先用流程图的方式总结一下整个消息机制在Java层是如何运转的。

Handler-2.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享