Looper究竟在等什么?

在Android的世界里,Looper是个几乎无人不知的角色。大家普遍认为:

Looper就是一个消息机制,一个线程发送,另一个线程处理。

这种理解当然没错,但也远远不够。

如果你翻开它的底层源码,或者观察它在实际系统中的行为,就会发现------消息机制只是挂载在Looper上的一个功能,并非它的全部。Looper的本质,其实是一个事件循环。在它的深处,运行着一个由epoll驱动的高效事件系统:既能处理Java层的Message,也能处理Native层的Message,还能同时监听来自Input、VSync、Binder等多种事件源的文件描述符(file descriptor,fd)。

在Looper的世界里,有一个看似简单却被无数人忽略的问题:

Looper空闲时,到底在等什么?又是如何被唤醒的?

这个问题一旦认真追问下去,你就会发现,它并不是一个单纯的Java问题,而是一个牵扯到epoll、调度、唤醒等内核原理的底层问题。也正是因为太过底层,所以常常被人一笔带过。

事实上,这个问题可以追溯到Unix最核心的设计哲学:一切皆文件(Everything is a file)。在这种思想下,Linux尽可能地将"可等待的事件"抽象为fd。无论是socket、输入设备、定时还是通知。而所谓的等,其实就是等待这些事件的发生,换言之,等待这些fd状态的变化。

现实情况中,我们经常需要在一个线程中等待多个事件,这个概念英文一般叫Multiplexing(多路复用)。而处理多路复用的机制在Linux中也经历过变革,由早期的select、poll最终进化成epoll。select和poll都需要在唤醒后遍历所有被监控的fd,因此随着监控fd的增多,性能会大打折扣。epoll则让每个事件在就绪时将自己推入ready list,因此被唤醒的线程只需要遍历ready list,而不必再扫描所有fd。这一改进使得等待效率从O(n)降为O(1),也让Linux的事件驱动模型真正具备了高并发场景下的可扩展性。

当一个线程在等待epoll事件时,它通常会阻塞在epoll_wait()调用处,此时线程进入睡眠状态,同时被挂入epoll实例的wait queue。

当某个被监控的fd状态发生变化(例如变为可读)时,致使它发生变化的那个线程会通过fd对应的回调函数将事件添加到epoll实例的ready list中,随后唤醒epoll_wait上睡眠的线程。

唤醒的过程在内核层面大致包含以下几个步骤:

  • 从等待队列移除:目标线程从epoll的wait queue中移除。
  • 加入运行队列:目标线程被放入CPU的run queue中。
  • 触发调度检查:resched_curr()标记该CPU需要重新调度。
  • 可能的CPU迁移:如果当前CPU负载过高,调度器可能在负载均衡阶段将该线程迁移到其他CPU。

需要注意的是,唤醒并不意味着线程会立刻得到执行权,而只是将线程标记为"可运行",真正的执行时机取决于调度策略以及当前CPU的负载状态。从这个角度看,唤醒是一种异步的触发行为,被唤醒只是意味着"可以被调度",而非"马上执行"。

当等待的线程恢复运行时,它会遍历ready list,从中取出所有已就绪的事件(数量最多为maxevents),将它们拷贝到用户空间,供上层进一步分发和处理。

对Looper而言,epoll_wait唤醒后的处理逻辑如下。

在每次MessageQueue.next()取出下一条Java消息之前,Looper都会通过nativePollOnce()进入到native层的事件循环。在这里,底层的epoll_wait()会等待或取回所有已就绪的事件,然后依次完成以下几步操作:

  1. 处理所有到期的Native消息:这些消息被封装在mMessageEnvelopes队列中,当epoll_wait()返回后,Looper会一次性处理所有到期的消息。
  2. 执行所有已就绪的fd回调:每个fd对应一个特殊的回调函数,处理它特有的逻辑。
  3. 返回到Java层继续分发消息:从MessageQueue中取出下一条到期的Message,执行它具体的处理方法。

值得注意的是,Native消息和Java消息其实共用同一个唤醒机制。它们都依赖于一个特殊的EventFd------mWakeEventFd,这个fd也被epoll监控着。当任意一方有新消息入队,需要唤醒线程时,就会往这个fd写入一个uint64_t类型的1,以此来唤醒epoll_wait的等待线程。

实际案例

Java消息大家接触的已经足够多了,这里不再赘述。下面说下系统中实际存在的Native消息和fd回调。

Native消息

当动画在底层渲染管线中结束时,RenderThread可以通过JniAnimationEndListener进行回调,将动画结束的事件发送给指定的Looper(通常是主线程的Looper)。这个Looper醒来后,会处理这条Native消息,并在回调中通过JNI调用Java层的AnimatedImageDrawable.callOnAnimationEnd()。

这里的关键是:RenderThread是一个纯Native的渲染线程,它无法也不应该直接调用Java层逻辑。因此,它通过Native消息实现了一种转换:事件仍然源自RenderThread,但最终在UI线程的Java层被分发和执行。

Fd回调

不论是Java消息还是Native消息,它们处理的往往是线程之间的信息传递,而fd回调则一般用于进程间的信息传递。以下是三个典型的用法:

  1. Input事件:每个应用的主线程都监听一个来自InputChannel的socket fd,当用户点击屏幕,system_server的InputDispatcher会往这个socket中写入输入事件,fd状态变为可读,epoll_wait()被唤醒,Looper调用该fd的回调(InputEventReceiver),从而进入ViewRootImpl的输入分发流程。
  2. VSync信号:VSync是整个渲染管线的"节拍器",决定应用每一帧何时开始绘制。应用的主线程Looper会监听DisplayEventReceiver的fd,Surfaceflinger通过EventThread将软件锁相环(DispSync)生成的VSync事件写入这个fd。当信号到达时,epoll_wait()被唤醒,Looper调用回调,触发Choreographer#doFrame(),进入绘制流程。
  3. Binder通信(ServiceManager为例):ServiceManager是一个核心的守护进程,负责注册和查询系统服务。它使用的线程模型与普通应用不同,并非采用binder线程池的方式,而是单线程+Looper的方式,将Binder驱动的fd也纳入统一的epoll监听中。当有其他线程向它发起Binder请求时,驱动会标记该fd为可读并触发唤醒。ServiceManager的主线程随后从epoll的ready list中读取事件,再通过handlePolledCommands进入到Binder驱动中将数据读回并处理。
相关推荐
czhc11400756635 小时前
JAVA1027抽象类;抽象类继承
android·java·开发语言
_Sem6 小时前
KMP实战:从单端到跨平台的完整迁移指南
android·前端·app
從南走到北6 小时前
JAVA国际版任务悬赏发布接单系统源码支持IOS+Android+H5
android·java·ios·微信·微信小程序·小程序
vistaup6 小时前
Android ContentProvier
android·数据库
我是场6 小时前
Android Camera 从应用到硬件之- 枚举Camera - 1
android
4Forsee6 小时前
【Android】View 事件分发机制与源码解析
android·java·前端
咕噜签名分发冰淇淋6 小时前
苹果ios安卓apk应用APP文件怎么修改手机APP显示的名称
android·ios·智能手机
应用市场6 小时前
从零开始打造Android桌面Launcher应用:原理剖析与完整实现
android
叶羽西6 小时前
Android15增强型视觉系统(EVS)
android