Android线程间通信(二):MessageQueue(中)

5.next()

  next()是消息队列中最为核心的方法,Looper从消息队列中取消息就是通过next()实现的。next()保证每次调用都能让Looper得到一个消息,除非消息队列正在退出或者已经废弃(此时返回null)。也就是说如果暂时取不出消息,next()并不会返回!此时为了节省资源,next()会根据消息队列的情况设定阻塞时长然后再阻塞线程。
  消息队列(未退出、未被废弃)在以下四种情况下,next()会选择阻塞线程:
  1)队列中没有任何消息 – 永久阻塞:这个时候不能返回null,因为next()的目的是取出一个消息,队列中现在没有消息并不代表一段时间后也没有消息。消息队列还在可用中,随时都有可能有Handler发布新的消息给它。那么问题来了,为了节省资源准备阻塞线程但是多少时间后唤醒它呢?臆断一个时长并不是很好的解决方案。我们知道消息队列是用来管理消息的,既然确定不了阻塞时长那么不如先永久阻塞,等新消息入队后主动唤醒线程。
  2)队首的消息执行时间未到 – 定时唤醒:每个消息的when字段都给出了希望系统处理该消息的时刻。如果在next()方法取消息时,发现消息队列的队首消息处理时间未到,next()同样需要阻塞。因为消息队列是按照消息的when字段从小到大排列的,如果队首消息的处理时间都没到那么整个队列中都没有能够立即取出的消息。这个时候因为知道下一次处理的具体时间,结合当前时间就可以确定阻塞时长。
  3)队首消息是同步障碍器(SyncBarrier),并且队列中不含有异步消息 – 永久阻塞:因为对消息队列施加了同步障碍,所有晚于队首同步障碍器处理时间的同步消息都变得不可用,next()在选取返回消息时会完全忽略这些消息。这和第一种情况相似,所以采取的阻塞方案也是永久阻塞。
  4)队首消息是同步障碍器(SyncBarrier),队列中含有异步消息但执行时间未到 – 定时唤醒:因为对消息队列施加了同步障碍,所有晚于队首同步障碍器处理时间的同步消息都变得不可用,next()在选取返回消息时会完全忽略这些消息。也就是说对于next(),它只会考虑队列中的异步消息。这和第二种情况相似,所以采取的阻塞方案是设置阻塞时长再阻塞。


  在这个基础上,我们再以验证的方式来看next()方法的源代码:

Message next() {
     final long ptr = mPtr;
     ……(代码略。如果消息循环正在退出或者已经废弃,直接返回null。)

     int nextPollTimeoutMillis = 0;  //阻塞时长
     for (;;) {
        ……(不相关内容)
        //nextPollTimeoutMillis为0立即返回,为-1则无限等待(必须主动唤醒)。ptr是指针,涉及本地方法不深究。
        nativePollOnce(ptr, nextPollTimeoutMillis);
        synchronized (this) {
           //now等于自系统启动以来到此时此刻的非深度睡眠时长
           final long now = SystemClock.uptimeMillis();
           Message prevMsg = null;
           Message msg = mMessages; //队首消息

           //如果当前队首的消息时设置的同步障碍器(target为null)。
           if (msg != null && msg.target == null) {
               // 因为同步障碍器的原因而进入该分支,找到下一个异步消息之后才会结束while。
               do {
                    prevMsg = msg;
                    msg = msg.next;
               } while (msg != null && !msg.isAsynchronous());
            }

            //此时msg一定是普通消息或者null,一定不是同步障碍器
            if (msg != null) { 
                    if (now < msg.when) {
                        //队首第一个非障碍器的消息执行时间未到,计算阻塞时长
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {//一切正常,开始取消息
                        mBlocked = false;//不阻塞线程
                        if (prevMsg != null) {//如果跳过了队首的同步障碍器取异步消息
                            prevMsg.next = msg.next;
                        } else {//如果当前消息就是队首消息
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (false) Log.v("MessageQueue", "Returning message: " + msg);
                        return msg;
                    }
                } else { //消息队列为空,或者队首是SyncBarrier且队列中无异步消息
                    nextPollTimeoutMillis = -1;//-1表示无限等待
                }

               //所有待处理的消息均处理完成, 接下来处理闲时任务
               ……(因为当前代码块在无限for循环中,此时再次验证一下消息队列是否正在退出。如果是,则废弃消息队列并返回null。)

               ……(初始化闲时任务)
               //闲时任务列表为空,或者不是第一次执行到这里
               if (pendingIdleHandlerCount <= 0) {
                   mBlocked = true;
                   continue; 
               }
               ……(初始化闲时任务)
            }//synchronized结束

            ……(如果是本次调用next()过程中第一次到达这里,则执行闲时任务。如果不是第一次,则不会执行到这里)
            //将待处理的IdleHandler个数设置为0,使得本次next()的调用再也不会到达这个for循环。
            pendingIdleHandlerCount = 0;

            //因为执行了闲时任务花费了一段时间(迭代开始处的阻塞方法还未执行到所以还未阻塞),此时再根据之前
            //计算出的阻塞时长阻塞线程显然不合适。
            nextPollTimeoutMillis = 0;
        }//for(;;)结束
    }

  补充一点细节:大家假设队首是同步障碍器时,代码有没有判断过它的处理时间有没有到?


6.主动唤醒

  针对上一节提到的四种阻塞情况,我们来分析一下对应情况下什么时候才需要主动唤醒:
  1)队列中没有任何消息 – 永久阻塞:新消息入队后便主动唤醒线程,无论新消息是同步消息、异步消息还是障碍器。
  2)队首的消息执行时间未到 – 定时唤醒:如果在阻塞时长未耗尽时,就新加入早于队首消息处理时间的消息,需要主动唤醒线程。
  3)队首消息是同步障碍器(SyncBarrier),并且队列中不含有异步消息 – 永久阻塞:如果新加入的消息仍然是晚于队首同步障碍器处理时间,那么这次新消息的发布在next()层面上是毫无意义的,我们也不需要唤醒线程。只有在新加入早于队首同步障碍器处理时间的同步消息时,或者,新加入异步消息时(不论处理时间),才会主动唤醒被next()阻塞的线程。
  4)队首消息是同步障碍器(SyncBarrier),队列中含有异步消息但执行时间未到 – 定时唤醒:因为队首同步障碍器的缘故,无论新加入什么同步消息都不会主动唤醒线程。即使加入的是异步消息也需要其处理时间早于设定好唤醒时执行的异步消息,才会主动唤醒。

  上面的讨论中,我们只考虑了新增普通消息时,是否需要主动唤醒阻塞中的线程。现在我们来考虑一下,移除普通消息时是否应该唤醒。第一种情况没有消息跳过。第二种情况和第四种情况下,假设我们移除设定好下次被动唤醒时执行的消息,线程被唤醒后就会因为没有需要处理的消息而再次进入阻塞,并不会错过消息所以不需要主动唤醒。第三种情况下移除设定好下次被动唤醒时执行的消息,线程虽然会再次进入阻塞但并不会错过消息,也不需要主动唤醒。所以,移除普通消息在任何情况下都不需要主动唤醒线程。
  如果是新增和移除同步障碍器呢?无论哪种情况,新增的同步障碍器都会在被动唤醒时发挥同步障碍的作用,不会因为没有主动唤醒而多处理不该处理的消息,所以新增同步障碍器之后不需要主动唤醒线程。针对第三种和第四种情况,移除队首障碍器能够使本不可取出的同步消息变得可用,需要主动唤醒线程重新判断是否能够取出消息或者是否需要缩短阻塞时长。除非新的队首消息还是同步障碍器才不需要唤醒!
  至于加长阻塞时长使线程不会被无谓地被动唤醒(因为移除消息或者障碍器),这个设定至少在API22之前还没写入到代码中。


  就着上面给出的结论,我们来看看Android是怎么实现这些设定的。首先目光回到MessagqeQueue.enqueueMessage(),来看看新增普通消息时是怎么判断是否需要主动唤醒的:

boolean enqueueMessage(Message msg, long when) {
    ……(msg的合法性判断,不合法会终止入队)

    synchronized (this) {
        ……(判断当前消息队列是否正在退出或者已经废弃)
       msg.markInUse();
       msg.when = when;
       Message p = mMessages;
       boolean needWake;
       //如果队列首部为null,或者入队消息需要马上执行,或者入队消息执行时间早于队首消息,且线程已阻塞则都需要唤醒。
       //如果 p!=null&&when!=0&&when>p.when,则不需要唤醒。
       if (p == null || when == 0 || when < p.when) {
           msg.next = p;
           mMessages = msg;
           needWake = mBlocked;//mBlocked记录消息循环是否阻塞
       } else {
           /*在队列中间插入一个消息。一般情况下不需要唤醒队列(不是加到队首为什么要唤醒呢?),除
            * 非队首是一个同步障碍器而且新插入的消息是 1)异步消息 2)执行时间是队列中最早 时。*/

           //此处mBlocked值需要根据情况决定。当线程已经阻塞且队首消息是同步障碍器是新加入异步消息,needWake
           //才可能(!!)为true。这还要判断消息队列中是否有异步消息,以及异步消息的处理时间早于还是晚于新加入的异步消息。
           needWake = mBlocked && p.target == null && msg.isAsynchronous(); //如果是true也是暂时的,还有考验在等着呢!
           //寻找位置
           Message prev;
           for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                   break;
                }

                if (needWake && p.isAsynchronous()) {
                    //能到达这里,说明msg.when > p.when。既然needWake是true,毫无疑问此时消息队列是
                    //处于阻塞的。这只有一种可能,p这个异步消息的执行时间还没到(情况4)!msg的执行时间还
                    //更晚(不更晚早break了),那就没有必要唤醒消息队列了。
                    needWake = false;
                }
            }//for结束

            //插入新消息
            msg.next = p; // invariant: p == prev.next
            prev.next = msg;
       }

       if (needWake) {
           nativeWake(mPtr);//唤醒消息队列
       }
    }
    return true;
}

  在这个方法中,出现了一种我们前文都没提到的一种主动唤醒情况 —— when==0(立即执行),实际上这也是毫无疑问需要主动唤醒的一种情况。对于第一种情况,新加入消息肯定需要主动唤醒;对于第二种,不主动唤醒会错过;对于第三、第四种情况,队首的同步障碍器不能影响早于它执行的消息,所以新加入when为0的消息无论如何都能够执行,如果不主动唤醒也会错过!所以,无论什么情况,只要新入队的消息when字段为0,都要主动唤醒线程!
  移除消息的MessageQueue.removeMessages()系列和MessageQueue.removeCallbacksAndMessages()方法,虽然可能导致线程下次被动唤醒时没有消息执行,但是都不会错过消息所以不需要主动唤醒。
  接下来目光转到同步障碍器上,MessageQueue.removeSyncBarrier()代码:

void removeSyncBarrier(int token) {
        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);
            }
        }
    }

  ”needWake = mMessages == null || mMessages.target != null”,这个语句含有一种有趣的情况。当消息队列中不含普通消息只含一个同步障碍器时,移除这个障碍器后整个消息队列都空了。按理说,移除之前next()线程已经处于无限阻塞中,移除后再唤醒结果还是无线阻塞。从消息处理上来讲,这是一个可以轻松避免且毫无意义的唤醒。从空闲任务的管理上来讲,next()方法在阻塞线程之前都会执行空闲任务然后再迭代一次判断是否阻塞,阻塞后再唤醒也不可能在本次next()中再执行一次空闲任务,依然是一个可以轻松避免且毫无意义的唤醒。
  另外一点是,这段代码并没有融合mBlocked(记录当前线程是否阻塞)变量的值。可能出现线程未阻塞时主动唤醒线程的无谓举动。
  这两个问题也许在看到本地方法nativeWake()的定义之后才能解开,暂时留下疑问。

7.消息队列的退出与废弃

  当Looper对象退出循环处理时,会调用MessageQueue的同包成员方法quit(safe)通知消息队列开始退出操作。如果boolean型的参数safe是true,消息队列会清除when晚于当前时间的所有同步/异步消息与同步障碍器,留下本应处理完的消息继续处理;如果safe是false,则完全不顾虑,清除消息队列中的所有消息。
  在next()方法执行过程中,如果处理完队列中全部消息发现该消息队列的quit()方法被调用过,则直接调用dispose()废弃消息队列并返回null给Looper。当GC回收消息队列之前,会调用消息队列重载的finalize()方法,在这个方法中同样能够执行废弃消息队列的操作(如果还未废弃)。

注:过段时间再更新下篇,主要是MessageQueue本地方法的介绍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值