今天看啥  ›  专栏  ›  GaoGao

Android 消息循环(Handler/Looper/MessageQueue)原理总结

GaoGao  · 掘金  ·  · 2020-02-13 03:38
阅读 21

Android 消息循环(Handler/Looper/MessageQueue)原理总结

注:以下内容基于Android API Version 27(Android 8.1)Linux Kernel 3.18.0

概述

HandlerLooperMessageQueue组成了Android的消息循环系统。消息循环系统是Android App的神经中枢,无论是与AMS/WMS打交道,还是UI绘制,亦或是手机输入事件的派发都依赖于消息循环系统。

Android的消息循环运行于底层,我们在上层开发遇到的一系列的组件生命周期回调,比如ActivityonCreateViewonTouchEvent,都是从主线程的消息循环(Looper.loop经过层层调用过来的。我们在这些生命周期里填写代码最终形成了可以使用的App,而这一切都是被底层的消息循环驱动着,因此我们可以说Android App是一种基于消息驱动模型的应用程序。

MessageQueue

消息队列,用于存储和获取消息,包括Java层消息、Native层消息和各种文件描述符(FD)就绪消息。

Looper

消息循环,内部包含一个MessageQueue对象,通过一个无限循环的loop方法,持续从MessageQueue获取并处理消息。

Handler

Java层帮助类,内部包含一个Looper对象,Handler通过Looper中的MessageQueue往Java层的消息队列发送消息。Handler同时提供了消息处理的回调函数,Handler收到消息回调后再决定将消息派发到哪里去。

消息的发送与处理

消息循环起始于Looperloop方法调用,此方法内部是一个无限的for循环,线程一旦进入loop就再也出不去了,除非主动退出。以后线程所做的事就三件:取消息、执行消息处理函数和等待,而线程的所有有意义的工作都发生在消息处理函数中。

Android的消息循环同时兼顾了Java层和Native层,Jave层和Native各自维护者自己的消息队列,Native层同时还监控着一批FDFD就绪也是作为一种消息进行处理的。消息循环首先尝试消费Native层的消息(包括普通消息和FD消息),消费完后再消费Java层的消息,如果Java层没有可供消费的消息了,线程就会在下一次消费Native时层阻塞在Native的epoll_wait调用上,epoll_wait用于等待其所监控的FD就绪,如果Java层发送了新消息或者Native层发送了新消息,再或者有FD就绪了,线程就会被唤醒,从而消息循环就会继续处理消息,直到Java、FD和Native都没有消息了线程就又再次陷入阻塞。 Java层的消息在Java层处理,Native层的消息在Native层处理,Java层的消息和Native层的消息的唯一联系是他们在同一个循环线程中处理,线程阻塞是发生在Native层,线程唤起是发生在Java层和Native层,线程阻塞的时长和当前是否有消息或者下一个消息的处理时间有关。

Java层消息派发流程

Java应用程序通过Handler将消息发送给MessageQueue,消息按照时间从先到后进行排序放入链表中,如果消息要立即被处理,也就是说没有延迟的消息,此时如果线程处于阻塞状态,Java层通过jni调用Native层的接口唤醒线程,线程唤醒后会从Nativie的Looer.pollOnce返回到Java层的nativePollOnce,然后继续处理Java层的消息,消息处理完后根据是否还有消息或者下一个消息等待处理的延迟时间来决定Native层下次是一直阻塞还是使用一个超时时间进行阻塞。大致流程就是:阻塞-发消息-唤醒线程-处理消息-阻塞。

Native层消息派发流程

Java层的MessageQueue对象持有一个Native层的MessageQueue对象,Native层的MessageQueue持有一个Native层的Looper对象,Java层的MessageQueue调用Native的nativePollOnce最终调到Native的Lopper.pollOnce。Native的Lopper创建时会将自己加入到Native的线程本地存储中去(TLS),以便以后在线程执行的任意地方拿到Looper对象向Native消息队列发送消息。

Native层的Looper使用eventfd进行线程的唤醒,eventfd是linux系统调用,通过eventfd创建一个FD,然后将这个FD和其他真正要监控的FD加入到同一个epoll监控列表中,当需要唤起线程时向这个FD写入一个字符,这样epoll检测到有FD就绪就会从阻塞中唤醒。当Java层需要唤醒Native层的epoll阻塞时只需要调用Native层的方法向event FD写入一个字符,这样epoll_wait就返回了。

Native层的Lopper.pollOnceepoll_wait返回后首先处理Native消息队列中的消息,Native消息队列的消息是通过Native Lopper.sendMessage函数添加的,每一个消息都绑定了一个MessageHandler对象,处理消息就是调用MessageHandlerhandleMessage函数将消息对象本身传给发送消息者。处理完普通消息后,Native Looper接着处理FD就绪的消息,每一个FD消息在添加的时候(Looper.addFD)会关联一个LooperCallback对象,Native Looper就是通过回调FD所绑定的LopperCallback来由使用方自己处理FD就绪的消息的。 处理完所有的这两种消息后Native Looper就返回了,接着线程就走到了Java层的消息处理逻辑中去了。

线程局部存储

我们之所以在创建了Looper的线程中的任意位置可以访问这个Looper原因是Looper对象是借助ThreadLocal存储的。ThreadLocal即线程局部存储。Looper内部声明了一个静态的ThreadLocal对象,TheadLocal内部是以线程对象为key,线程局部数据为value存储在一个类map的结构中的。只要线程调用Looper.prepare将当前创建的Looper对象存在ThreadLocal中,以后在线程执行的任意位置都可以调用Looper.myLooper将存储的Looper对象拿出来,并且每个线程对应各自的Looper对象。

关于线程局部存储可以参阅:Android 线程局部存储ThreadLocal原理总结

Android消息循环在以下重要场景中都有应用

  • 子线程做完任务后通过消息循环转到主线程去刷新UI。
  • AMSWMS对App进程的回调从binder线程转到App主线程。
  • vsync信号的处理。ViewRootImpl刷新布局基于vsync信号,而vsync信号是通过读取socketpairFD获得的,将socketpairFD加入到Native的消息轮询中可以直接使vsync的回调发生在主线程,从而避免了一次线程切换。
  • 设备输入事件(Input)同样是通过将socketpairFD加入到Native的消息轮询中从而实现了消息直接于主线程处理。

参考
blog.csdn.net/luoshengyan…
androidxref.com/8.1.0_r33/x…
androidxref.com/8.1.0_r33/x…
linux.die.net/man/2/event…
linux.die.net/man/2/socke…
chao-tic.github.io/blog/2018/1…




原文地址:访问原文地址
快照地址: 访问文章快照