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驱动中将数据读回并处理。
相关推荐
夏沫琅琊1 小时前
Android 各类日志全面解析(含特点、分析方法、实战案例)
android
程序员JerrySUN2 小时前
OP-TEE + YOLOv8:从“加密权重”到“内存中解密并推理”的完整实战记录
android·java·开发语言·redis·yolo·架构
TeleostNaCl3 小时前
Android | 启用 TextView 跑马灯效果的方法
android·经验分享·android runtime
TheNextByte13 小时前
Android USB文件传输无法使用?5种解决方法
android
quanyechacsdn5 小时前
Android Studio创建库文件用jitpack构建后使用implementation方式引用
android·ide·kotlin·android studio·implementation·android 库文件·使用jitpack
程序员陆业聪5 小时前
聊聊2026年Android开发会是什么样
android
编程大师哥6 小时前
Android分层
android
极客小云7 小时前
【深入理解 Android 中的 build.gradle 文件】
android·安卓·安全架构·安全性测试
Juskey iii7 小时前
Android Studio Electric Eel | 2022.1.1 Patch 2 版本下载
android·ide·android studio
Android技术之家7 小时前
2025年度Android行业总结:AI驱动生态重构,跨端融合开启新篇
android·人工智能·重构