相关文章推荐
深沉的刺猬  ·  Android ...·  3 周前    · 
开心的刺猬  ·  Android ...·  3 周前    · 
拉风的绿豆  ·  lua定时任务-CSDN博客·  2 周前    · 
好帅的冲锋衣  ·  Unity funtion call ...·  6 月前    · 
力能扛鼎的钢笔  ·  K8S Flannel_not ...·  1 年前    · 
活泼的大葱  ·  Qt之QCheckBox_qcheckbox ...·  1 年前    · 

使用handler发送消息时有两种方式, post(Runnable r) post(Runnable r, long delayMillis) 都是将指定Runnable(包装成PostMessage)加入到MessageQueue中,然后Looper不断从MessageQueue中读取Message进行处理。

然而我在使用的时候就一直有一个疑问,类似Looper这种「轮询」的工作方式,如果在每次读取时判断时间,是无论如何都会有误差的。但是在测试中发现Delay的误差并没有大于我使用 System.out.println(System.currentTimeMillis()) 所产生的误差,几乎可以忽略不计,那么Android是怎么做到的呢?

Handler.postDelayed()的调用路径

一步一步跟一下 Handler.postDelayed() 的调用路径:

  • Handler.postDelayed(Runnable r, long delayMillis)
  • Handler.sendMessageDelayed(getPostMessage(r), delayMillis)
  • Handler.sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis)
  • Handler.enqueueMessage(queue, msg, uptimeMillis)
  • MessageQueue.enqueueMessage(msg, uptimeMillis)
  • 最后发现Handler没有自己处理Delay,而是交给了MessageQueue处理,我们继续跟进去看看MessageQueue又做了什么:

    msg . markInUse ();

    MessageQueue中组织Message的结构就是一个简单的单向链表,只保存了链表头部的引用(果然只是个Queue啊)。在 enqueueMessage() 的时候把应该执行的时间(上面Hanlder调用路径的第三步延迟已经加上了现有时间,所以叫when)设置到msg里面,并没有进行处理……WTF?

    继续跟进去看看Looper是怎么读取MessageQueue的,在 loop() 方法内:

    for (;;) {

    可以看到,在这个方法内,如果头部的这个Message是有延迟而且延迟时间没到的(now < msg.when),会计算一下时间(保存为变量nextPollTimeoutMillis),然后在循环开始的时候判断如果这个Message有延迟,就调用 nativePollOnce(ptr, nextPollTimeoutMillis) 进行阻塞。 nativePollOnce() 的作用类似与 object.wait() ,只不过是使用了Native的方法对这个线程精确时间的唤醒。

    精确延时的问题到这里就算是基本解决了,不过我又产生了一个新的疑问:如果Message会阻塞MessageQueue的话,那么先postDelay10秒一个Runnable A,消息队列会一直阻塞,然后我再post一个Runnable B,B岂不是会等A执行完了再执行?正常使用时显然不是这样的,那么问题出在哪呢?

    再来一步一步顺一下Looper、Handler、MessageQueue的调用执行逻辑,重新看到 MessageQueue.enqueueMessage() 的时候发现,似乎刚才遗漏了什么东西:

    msg . markInUse ();

    就是这里了,在 next() 方法内部,如果有阻塞(没有消息了或者只有Delay的消息),会把mBlocked这个变量标记为true,在下一个Message进队时会判断这个message的位置,如果在队首就会调用 nativeWake() 方法唤醒线程!

    现在整个调用流程就比较清晰了,以刚刚的问题为例:

  • postDelay() 一个10秒钟的Runnable A、消息进队,MessageQueue调用 nativePollOnce() 阻塞,Looper阻塞;
  • 紧接着 post() 一个Runnable B、消息进队,判断现在A时间还没到、正在阻塞,把B插入消息队列的头部(A的前面),然后调用 nativeWake() 方法唤醒线程;
  • MessageQueue.next() 方法被唤醒后,重新开始读取消息链表,第一个消息B无延时,直接返回给Looper;
  • Looper处理完这个消息再次调用 next() 方法,MessageQueue继续读取消息链表,第二个消息A还没到时间,计算一下剩余时间(假如还剩9秒)继续调用 nativePollOnce() 阻塞;
  • 直到阻塞时间到或者下一次有Message进队;
  • 这样,基本上就能保证 Handler.postDelayed() 发布的消息能在相对精确的时间被传递给Looper进行处理而又不会阻塞队列了。

    另外,这里在阅读原文的基础上添加一点思考内容:
    MessageQueue会根据post delay的时间排序放入到链表中,链表头的时间小,尾部时间最大。因此能保证时间Delay最长的不会block住时间短的。当每次post message的时候会进入到MessageQueue的next()方法,会根据其delay时间和链表头的比较,如果更短则,放入链表头,并且看时间是否有delay,如果有,则block,等待时间到来唤醒执行,否则将唤醒立即执行。
    所以handler.postDelay并不是先等待一定的时间再放入到 MessageQueue中,而是直接进入 MessageQueue,以MessageQueue的时间顺序排列和唤醒的方式结合实现的。使用后者的方式,我认为是集中式的统一管理了所有message,而如果像前者的话,有多少个delay message,则需要起多少个定时器。前者由于有了排序,而且保存的每个message的执行时间,因此只需一个定时器按顺序next即可。
    作者:
    一点点征服 出处: http://www.cnblogs.com/ldq2016/ 本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利