Android 消息机制之 MessageQueue.next() 消息取出的深入源码分析 [ 八 ]

Android 消息机制深入源码分析 [ 一 ]
Android 消息机制之 ThreadLocal 深入源码分析 [ 二 ]
Android 消息机制之 Looper 深入源码分析 [ 三 ]
Android 消息机制之 Message 与消息对象池的深入源码分析 [ 四 ]
Android 消息机制之 MessageQueue 深入源码分析 [ 五 ]
Android 消息机制之初识Handler [ 六 ]
Android 消息机制之 Handler 发送消息的深入源码分析 [ 七 ]
Android 消息机制之 MessageQueue.next() 消息取出的深入源码分析 [ 八 ]
Android 消息机制之消息的其他处理深入源码分析 [ 九 ]
Android 消息机制总结 [ 十 ]

紧接上一章消息的发送, 本章内容为消息的取出分析学习.

消息的取出主要是通过 Looperloop 方法. 这个方法在第三章的时候已经分析过, 分为以下几步

  1. 获取Looper对象
  2. 获取MessageQueue消息队列对象
  3. 死循环遍历
  4. 通过queue.next()来从MessageQueue的消息队列中获取一个Message msg对象
  5. 通过msg.target,dispatchMessage(msg)来处理消息
  6. 通过msg.recycleUnchecked() 方法来回收Message到消息对象池中.

其中 Message.recycleUnchecked() 在第四章的时候已经分析过, 那么现在就剩下 MessageQueue.next()handler.dispatchMessage() . 那么先来看 MessageQueue.next()

1. MessageQueue.next()

MessageQueue.java 310 行, 代码过多, 将分段分析.

Message next() {
  //分析 1
  final long ptr = mPtr;
  if (ptr == 0) {
      return null;
  }
  //分析 2
  int pendingIdleHandlerCount = -1; // -1 only during first iteration
  //分析 3
  int nextPollTimeoutMillis = 0;
  • 分析:
    1. 如果消息循环已经退出了, 则在这里直接 return, 因为调用了 disposed()方法后, mPtr = 0;
    1. 记录空闲时间处理的 IdleHandler 的数量. 初始为 -1
    1. native 需要用到的变量. 初始化为 0, 如果大于 0, 表示还有消息待处理(未到执行时间). -1表示阻塞等待
 //分析 4
  for (;;) {
      //分析 5
      if (nextPollTimeoutMillis != 0) {
          Binder.flushPendingCommands();
      }
      //分析 6
      nativePollOnce(ptr, nextPollTimeoutMillis);
      //分析 7
      synchronized (this) {
          final long now = SystemClock.uptimeMillis();
          Message prevMsg = null;
          Message msg = mMessages;
          //分析 8
          if (msg != null && msg.target == null) {
              do {
                  prevMsg = msg;
                  msg = msg.next;
              } while (msg != null && !msg.isAsynchronous());
          }
  • 分析
    1. 开启死循环 (循环内容一直到最后)
    1. 如果还有消息未处理, 就刷新 Binder 命令, 一般在阻塞前调用
    1. 调用 native 方法, 当 nextPollTimeoutMillis == -1 的时候就阻塞等待, 直到下一条消息可用为止. 否则就继续向下执行. 还记得第七章发送消息时候消息入队操作的最后吗? 里面有一个 nativeWake() 唤醒. 就是唤醒此处. 没有消息的时候, 这里就处于阻塞状态. 当我们发送消息的时候, 这里就会被唤醒.
    1. 加上同步锁, 然后获取从开机到现在的时间, 获取消息链表头部元素,
    1. 判断第一个消息是不是障栅. (在前面第五篇中说过: 只有障栅的 tatget 才为 null), 如果第一个消息是障栅, 则又开启一个循环, 取出第一个异步消息, 从 do..while 这段代码中. 可以印证出障栅会拦截所有的同步消息.
      如果 msg != null && ! msg.isAsynchronous() 这个条件成立, 说明就是同步消息, 那么就跳出同步消息继续循环, 直到找到第一条异步消息并赋值给
      .就退出 do..while 循环.
          //分析 9
          if (msg != null) {
              //分析 10
              if (now < msg.when) {
                  nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
              } else { //分析 11
                  mBlocked = false;
                  //分析 12
                  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 {
              //分析 13
              nextPollTimeoutMillis = -1;
          }
  • 分析
    1. 判断消息是否为 null
      如果进入了分析 8 中的 if 逻辑, 那么到这一步, msg 要么是一个异步消息, 要么为null. 不可能是同步消息
      如果没有进入分析 8, 说明头部消息不是障栅, 需要判断是否是可执行的异步消息或者同步消息.
    1. 如果当前时间小于消息的执行时间, 表示当前可执行的消息还未到执行时间, 则记录下剩余时间.
    1. 如果当前时间大于等于消息的执行时间, 表示当前消息的执行时间已经到了, 接着将 MessageQueue.mBlocked 设置为 false 表示 MessageQueue 不阻塞, mBlocked 变量与消息入队时,需要不需要唤醒
    1. 这个 if..esle.. 判断内的逻辑就是将需要立刻执行的消息从消息队列中抽出来, 然后再将消息队列组合起来. 再将要执行消息的 next 赋值为 null,并标记为正在使用. 最后把要执行的消息返回出去. 获取消息结束.
      例如: 消息链表中有三个消息 A -> B -> C, A是障栅, B是异步, C是同步. 分析 12 走完, 就变成了, B 是单独的一个消息, 并将 B.next 置为 null, 最后组合后的消息链表就为 A -> C.
    1. 如果分析9 的判断不成立, 则表示目前没有可执行的消息, 设置 nextPollTimeoutMillis = -1 . 在分析 3 中说过这个变量的作用表示是否阻塞.

          //分析 14
          if (mQuitting) {
              dispose();
              return null;
          }
          //分析 15
          if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) {
              pendingIdleHandlerCount = mIdleHandlers.size();
          }
          //分析 16
          if (pendingIdleHandlerCount <= 0) {
              // No idle handlers to run.  Loop and wait some more.
              mBlocked = true;
              continue;
          }
          //分析 17
          if (mPendingIdleHandlers == null) {
              mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
          }
          mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
      }

注意: 当代码开始执行这里的时候, msg 要么为 null, 要么未到执行时间.

  • 分析
    1. 官方翻译为:现在已处理所有挂起的消息,请处理退出消息。需要关闭消息队列, 返回 null, 通知 Looper 停止循环.
    1. 当第一次循环才会在空闲的时候去执行 IdleHanler, 从代码可以看出所谓的空闲状态指的就是, 目前没有任何可执行的 Message, 这里的可执行有两个要求, 当前 Message 不会被障栅拦截, 当前 Message 到达了执行时间. 才会为变量 pendingIdleHandlerCount 赋值.
    1. 如果没有在空闲时需要执行的 IdleHandler. 这里是消息队列阻塞(死循环)的重点, 在 msg = null 或者未到执行时间的情况下, 表示消息队列空闲, 但是也没有可执行的 idleHandler, 那么就把 mBlock 变量置为 true, 表示需要唤醒, 并开始下一次循环. 就会回到上面的分析 5 和分析 6, 这个时候 nextPollTimeoutMillis 要么为 -1, 要么就为上个消息剩下要执行的时间. 那么分析 5 肯定成立, 接着刷新 binder 命令, 然后在分析 6 中就开始阻塞, 只要不是 0, 就会阻塞. 等要执行的时间到了就会被唤醒. 或者当有新的消息入队的时候. 就会根据 mBlock 的值来判断是否要唤醒消息队列.
    1. 如果有需要在空闲时执行的 IdleHandler, 接着判断是否初始化过 mPendingIdleHandlers 数组, 最小4 个长度. 并把要执行的 IdleHandler 赋值给 mPendingIdleHandlers 数组.
      //分析18
      for (int i = 0; i < pendingIdleHandlerCount; i++) {
          final IdleHandler idler = mPendingIdleHandlers[i];
          mPendingIdleHandlers[i] = null; // release the reference to the handler

          boolean keep = false;
          try {
              keep = idler.queueIdle();
          } catch (Throwable t) {
              Log.wtf(TAG, "IdleHandler threw exception", t);
          }

          if (!keep) {
              synchronized (this) {
                  mIdleHandlers.remove(idler);
              }
          }
      }
      //分析 19
      pendingIdleHandlerCount = 0;
      //分析 20
      nextPollTimeoutMillis = 0;
  }
}

注意: 执行到这里, 说明目前没有可执行的 Message, 但是有可以在空闲时执行的 IdleHandler.

官方对这个循环的注释为:

    1. 执行 idleHandler. 我们只会在第一次迭代时到达此代码块。为什么呢, 这个稍后分析, 先分析完 19 和 20.
    1. IdHandler 只会在消息队列阻塞之前执行一次, 执行之后, pendingIdleHandlerCount 赋值为 0, 之后就不会再执行. 一直到下一次调用 MessageQueue.next() 方法.
    1. 当执行了 IdleHander 之后, 会消耗一段时间, 这时候消息队列里可能已经有消息到达可执行时间, 所以重置 nextPollTimeoutMillis 回去重新检查消息队列.

 
关于分析 18 的疑问. 为什么只会在第一次循环的时候会执行这里呢.

也就是说只有在 MessageQueue.next 方法的死循环, 第一次循环的时候, msgnull 或者 msg 未到执行时间, 并且有可执行的空闲 IdleHandler 的情况下会 执行. 或者下次调用 MessageQueue.next() 方法. 为什么呢? 一起来分析一下,

  • 第一次循环开始,

    • 分析 6 那里肯定不会阻塞, 因为 nextPollTimeoutMillis 初始值为 0.
    • 然后到分析 9 , 如果 msg != null 并且有需要立即执行的消息的话, 就直接跳出死循环了, 我们假设 msgnull 或者 msg 未到执行时间. 那么在分析 9 内, 会对 nextPollTimeoutMillis 赋值,
    • 接着到分析 15, pendingIdleHandlerCount 初始值为 -1, 判断成立, 给 pendingIdleHandlerCount 赋值
    • 到分析 16, 条件不成立, (因为我们假设有可执行的空闲 IdleHandler). 如果没有则直接就进行下次一次循环了, 下一次循环到分析 6 处, 就会阻塞了.
    • 分析 18, 开始执行 IdleHandler. 执行一个, 就从 mIdleHandlers 中移除一个.
    • 分析 19, pendingIdleHandlerCountnextPollTimeoutMillis 都赋值为 0,
  • 第二次循环开始

    • 分析 6 不阻塞, 因为在第一次循环的分析 20 处被重置了, 需要重新检查消息队列.
    • 分析 9 如果有消息到执行时间了, 会直接 return, 没有消息或者还是未到时间就再对 nextPollTimeoutMillis 赋值 -1 或者剩余执行时间. 接着向下走.
    • 分析 15. 这里的判断就不会成立了, 因为在第一次循环最后分析 19 处 pendingIdleHandlerCount 被置为 0 了. 所以跳到分析 16.
    • 分析 16. pendingIdleHandlerCount <= 0 条件成立. 跳出本次循环, 开始进入第三次循环了. 然后在第三次循环中的分析 6, 就开始阻塞. 直到被唤醒.

 
总结
总的来说当在 Looper.loop() 方法的死循环内, 调用MessageQueue.next() 方法获取一个 Message 的时候, 大致会分为以下几步.

  1. MessageQueue 会先判断队列中是否有障栅存在
  • 有: 返回第一个异步消息,
  • 没有: 逐个返回同步消息
  1. MessageQueue 中没有任何消息可以处理或者未到消息的执行时间的时候, 就会进入阻塞状态等待新的消息到来被唤醒. 或者有消息的执行时间到了被唤醒. 在阻塞之前会执行一次 IdleHandler.
  2. MessageQueue 被关闭的时候, 成员变量 mQutting 会被标记为 true, 然后在 Looper 试图从 MessageQueue 取消息的时候返回 null. 而 Message = null就是告诉 Looper 消息队列已经关闭, 应该停止死循环了.(在第三篇 Looper.loop() 方法分析中有说明 )
  3. Handler 中实际上有两个无限循环体, 一个是在 Looper.loop() 中的循环体, 以及 MessageQueue 中的循环体. 真正的阻塞是在 MessageQueue 的循环体中.

好了, 本章分析学习就到这里结束了, 下一章将会分析学习 消息分发处理, 消息的移除, 以及消息的其他操作.

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