美文网首页Android开发经验谈Android开发Android技术知识
Android 消息机制之 MessageQueue 深入源码分

Android 消息机制之 MessageQueue 深入源码分

作者: __Y_Q | 来源:发表于2020-08-24 19:24 被阅读0次

上一章学习了消息机制中的 Message 与 Message 的消息对象池. 本章继续学习消息机制中 MessageQueue 消息队列的相关概念.

尽管 MessageQueue 叫消息队列, 但是它的内部实现却并不是用的队列, 实际上它是通过一个单链表的数据结果来维护消息列表. (单链表在插入与删除上很有优势)

MessageQueue 主要包括两个操作, 插入和读取. 但是读取操作本身会伴随着删除操作. 插入和读取对应的方法分别是 enqueueMessagenext.

  • enqueueMessage 作用是往消息队列中插入一条数据.
  • next 作用是从消息队列中取出一条消息并将其从消息队列中移除. next 是一个无限循环的方法, 如果消息队列中没有消息, 那么 next 方法会一直阻塞, 当有新消息来的时候, next 会返回这条消息并将其从链表中移除.

消息队列中所有的 Message 都是按照时间从前往后有序排列的.

1. 成员变量

  • private final boolean mQuitAllowed

用于表示消息队列是否可以被关闭, 主线程的消息队列不可关闭.

  • private long mPtr; // used by native code

用于保存 native 代码中 MessageQueue 的指针.

  • Message mMessages

在 MessageQueue 中所有的 Message 都是以链表的形式组织在一起的, mMessage 保存了消息链表的头部. 也可以说它就是链表的本身.

  • private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();

当 Handler 线程处于空闲状态的时候(MessageQueue 没有其他 Message), 可以利用它来处理一些事物, 该变量就是用于保存这些空闲时候需要处理的事物.

  • private IdleHandler[] mPendingIdleHandlers;

保存将要被执行的 Idlehandler

  • private boolean mQuitting;

标识 MessageQueue 是否正在关闭.

  • private boolean mBlocked;

标识 MessageQueue 是否阻塞

  • private int mNextBarrierToken;

在 MessageQueue 中有一个概念叫做障栅, 它用于拦截同步的 Message, 阻止这些消息被执行. 只有异步的 Message 才会放行. 障栅本身也是一个 Message, 只是它的 target 为 null, 并且 arg1 属性用于区分不同的障栅, 所以该变量就是用于不断累加生成不同的障栅.

 

2. IdleHandler 接口

在 MessageQueue.java 811 行 中, 有一个内部接口为 IdleHandler.

public static interface IdleHandler {
   boolean queueIdle();
}

这是一个回调接口, 当线程空闲时可以利用它来处理一些业务.

  • boolean queueIdle()

当消息队列内所有的 Message 都执行完之后, 这个方法会被调用.
返回 true, IdleHandler 会一直保持在消息队列中.
返回 false, 会执行完改方法后移除 IdleHandler.

需要注意的是, 当消息队列中还有其他的 Delay Message (延迟消息), 并且这些 Message 还没到执行时间的时候, 由于线程空闲, 所以这个方法也可能会被调用, IdleHandler 也可能会被执行.

IdleHandler 就是一个简单的回调接口, 内部就是一个带有返回值的方法, 在使用的时候只需要实现该接口并加入到 MessageQueue 中就可以了.

//例:
MessageQueue messageQueue = Looper.mQueue();
messageQueue.addIdleHandler(new MessageQueue.IdleHandler(){
  @Override
  public boolean queueIdle(){
        return false;
  }
});

接着看一下 addIdleHandler 方法做了些什么.
MessageQueue.java 117行

public void addIdleHandler(@NonNull IdleHandler handler) {
    if (handler == null) {
        throw new NullPointerException("Can't add a null IdleHandler");
    }
    synchronized (this) {
        mIdleHandlers.add(handler);
    }
}
  • 官方翻译:

添加一个新的 IdleHandler 到消息队列中, 当 IdleHandler 的回调方法为 false 的时候, 该 IdleHandler 在被执行后会立即移除, 也可以通过调用 removeIdleHandler(IdleHandler handler) 方法移除指定的 IdleHandler. 任何线程调用该方法都是安全的.

方法内部很简单, 就是三个步骤

  1. 做非空判断.
  2. 加同步锁.
  3. 将要添加的 IdleHandler 添加至 mIdleHandlers (ArrayList)

 

2. MessageQueue 中的 Message 的分类

在 MessageQueue 中, Message 被分为 3 类.

  1. 同步消息

正常情况下我们通过 Handler 发送的 Message 都属于同步消息, 除非我们在发送的时候指定该消息是一个异步消息. 同步消息会按顺序排列在队列中. 除非指定 Message 的执行时间. 否则 Message 会按顺序执行.

  1. 异步消息

想要往消息队列中发送异步消息, 我们必须在初始化 Handler 的时候通过构造函数 public Handler(boolean async) 指定 Handler 是异步的. 这样的 Handler 在将 Message 加入消息队列的时候, 就会将 Message 设置为异步的.

  1. 障栅(Barrier)

障栅(Barrier), 是一种特殊的 Message, 它的 targetnull, 并且 arge1 属性被用作障栅的标识符来区分不同的障栅.
障栅的作用是用于拦截队列中的同步消息, 放行异步消息. 就好像交警一样, 在道路拥挤的时候决定哪些车可以先通过, 在这里这些先通过的车辆指的就是异步消息.
(只有 障栅的 target 才会为 null, 如果我们自己试图设置 Messagetargetnull的话会报异常)

 

3. 添加障栅 Barrier

首先看一下 障栅的添加. 在 MessageQueu.java 中 障栅的添加方式有两种.
1. postSyncBarrier(). 代码位于 MessageQueu.java 461 行.

public int postSyncBarrier() {
    return postSyncBarrier(SystemClock.uptimeMillis());
}
  • 官方翻译:
  • Looper 的消息队列中发送一个同步的 Barrier
  • 如果没有发送同步的 Barrier, 消息处理像往常一样, 该怎么处理就怎么处理. 当发现 Barrier 后, 队列中后续的同步消息会被阻塞, 直到通过调用 removeSysncBarrier() 释放指定的 Barrier.
  • 这个方法会导致立即退出所有后续发布的同步消息, 直到满足释放指定的 Barrier, 而异步消息不受 Barrier的影响. 并按照之前的流程继续处理.
  • 必须使用相同的 token 去调用 removeSysncBarrier(), 来保证插入的 Barrier 和移除的是同一个, 这样可以确保消息队列可以正常运行, 否则应用程序可能会挂起.
  • 返回值是 Barrier 的唯一标识符, 持有这个 token 去调用 removeSysncBarrier() 方法才能达到真正的释放 Barrier

可以看到方法内部又调用了另外一个同名有参的方法, 并传入了手机开机到现在的时间.

2. postSyncBarrier(long when). 代码位于 MessageQueu.java 465 行.

private int postSyncBarrier(long when) {
    synchronized (this) {
        //分析 1
        final int token = mNextBarrierToken++;
        //分析 2
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;
        //分析 3
        Message prev = null;
        Message p = mMessages;
        if (when != 0) {
            //分析 4
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        //分析 5
        if (prev != null) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
       //分析 6
        return token;
    }
}
  • 分析
  1. 获取障栅的唯一标识, 然后自增该变量作为下一个障栅的标识, 通过 mNextBarrierToken++ 可以得知, 这些唯一标识是从 0 开始自增的.
  2. Message 的消息对象池中获取一个 Message 对象赋值给 msg 作为障栅. (上一章学习过了消息对象池, 忘记的同学可以去翻看一下).
    并且赋值 whenarg1 的值. (arg1 为障栅的标识符), 同时调用 msg.makeInUse() 标记 msg 正在被使用,
  3. 创建变量 prevp, 并把 消息队列的第一个元素 mMessage 赋值给 p, 为第四步做准备. 此时 p 就为消息队列的第一个元素.
  4. 通过消息队列中的第一个 Message 对象 pwhen 属性与要添加障栅的 when 属性比较, 如果比要添加障栅的时间小, 就把第一个消息向后移动一位, 依次类推, 从而决定障栅在消息队列中的位置. 比如是放在消息队列的头部, 还是第二个位置. 如果在头部, 则拦截后面所有的同步消息, 如果在第二个位置, 则会放过第一个, 然后拦截后面的同步消息.
  5. 将障栅对象 msg 插入到消息队列中.
    prev != null 代表障栅的位置不会插入在消息队列的头部, 那就需要考虑到障栅的后面位置和前面位置 Message 的衔接. prev == null 表示障栅要插入的位置在消息队列的头部, 这样的话, 只需要与后面位置的 Message 进行衔接即可. 然后再赋值给 mMessage.
  6. 返回 token
  • 解析

简单来说每次添加障栅到消息队列的时候, 都会先通过 when 进行比较, 根据不同的情况会将障栅插入到不同的位置.

  1. Message.when < Barrier.when 的时候, 可以理解为第一个 Message 的执行时间早于障栅.
    消息队列中第一个消息的执行时间早于障栅的情况
  2. Message.when > Barrier.when 的时候, 也就是第一个 Message 的执行时间晚于障栅.
    消息队列中第一个消息的执行时间晚于障栅的情况

 
 
注意: 在上面的添加障栅的代码中, 可以看到 msg 这个障栅的 target 都是 null, 自始至终都没有被赋值过, 上面也说过自有障栅的 target 才会为 null. 这也是后面在移除障栅的时候通过这个来判断的条件之一.
 


4. 移除障栅

移除障栅的方法为 removeSyncBarrier(), 代码位于 MessageQueue.java 504 行

public void removeSyncBarrier(int token) {
    synchronized (this) {
        Message prev = null;
        //还是获取消息队列中的第一个元素
        Message p = mMessages;
        //遍历消息队列所有元素, 找到与 token 对应的障栅
        //如果有障栅, 那么 prev 要么为 null, 表示消息队列的第一位就是障栅
        //如果 prev 不为 null, 则 prev.next 为障栅, 而 p 的第一个元素为障栅
        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 回收放入消息对象池.
        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);
        }
    }
}
  • 解析

移除障栅的方法也很简单, 就是不断的遍历整个消息队列, 找到与 token 匹配的障栅后移除, 移除就是跳过障栅, 将障栅的上一个消息与障栅的下一个消息衔接起来.

 

  • MessageQueue.removeMessages(Handler h, int what, Object object): 从消息队列中删除所有符合指定条件的 Message
  • MessageQueue.removeAllFutureMessagesLocked(): 删除所有未来的消息.
  • MessageQueue.quit(boolean safe): 关闭消息队列(在 Looper 篇中已经分析过)
  • MessageQueue.next() 从消息队列中取出消息交给 Looper. (核心)
  • MessageQueue.hasMessages(Handler h, int what, Object object): 查看消息是否存在

这些方法都会放到 Handler 中有关消息的发送, 取出以及其他操作的文章中. 本篇文章主要是了解一下在 MessageQueue 中对 Message 的几种分类, 以及如何添加障栅(Barrier), 移除障栅的操作. 下一章正式开始学习 消息机制中的 Handler.

相关文章

网友评论

    本文标题:Android 消息机制之 MessageQueue 深入源码分

    本文链接:https://www.haomeiwen.com/subject/hpsnjktx.html