第二章里,Binder callback在客户端的Binder接收线程到达,更新界面前还需要mainHandler.post(...)。这一行代码为什么能把工作交给主线程?主线程没有消息时在做什么,一条新消息又怎样把它叫醒? 
本章从一个HandlerThread示例出发,跟随消息完成"选择目标线程 → 入队 → 等待与唤醒 → 执行 → 清理"的完整过程。先认识架构并运行程序,再读Java、JNI、Native Looper、Bionic和Kernel的关键代码。延迟、同步屏障、IdleHandler与文件描述符事件,都放回同一条主线解释。
3.1 准备设备并确认 MessageQueue 版本
本章用户空间沿用前两章的android17-release源码基线。该版本有多套MessageQueue实现,下文分别解释链表和并发队列,固定提交列在3.13的"固定源码基线"中。
沿用第一章的设备与工具
需要已启动的Android设备或SDK Emulator,以及第一章准备的AOSP Host工具。无需重新编译系统镜像;本文示例是普通的可调试应用。
在Host的Bash终端定义源码、记录目录与设备。源码不在默认位置时先修改第一行,serial从实际设备列表选择:
bash
chapter3_top="$HOME/aosp"
chapter3_notes="$chapter3_top/Android源码学习笔记/章节学习/第三章-Handler与Looper"
chapter3_run=$(mktemp -d "$HOME/aosp-chapter3.XXXXXX")
chapter3_adb="$chapter3_top/out/host/linux-x86/bin/adb"
"$chapter3_adb" devices -l
read -r -p '输入本次设备 serial:' chapter3_serial
"$chapter3_adb" -s "$chapter3_serial" shell getprop sys.boot_completed
"$chapter3_adb" -s "$chapter3_serial" shell getprop ro.build.fingerprint
"$chapter3_adb" -s "$chapter3_serial" shell uname -a
预期boot_completed为1,并能取得fingerprint与Kernel版本。设备未启动时回到第一章运行步骤,不要把设备连接问题当成Handler故障。
本章先用Legacy的有序队列读通一次消息往返。设备支持时,再展开进阶部分比较DeliQueue;两者都遵守Handler选择目标Looper、目标线程分派回调的基本约定。
版本核对 为什么源码里有多套MessageQueue
同名 MessageQueue 可能来自不同实现
旧文章常从core/java/android/os/MessageQueue.java开始,但当前源码已把它拆到不同目录。它们声明的仍是android.os.MessageQueue,构建时选择其中一套作为最终类,应用API并没有改成这些目录名。
| 源码位置 | 内部模型 | 阅读时要留意 |
|---|---|---|
| core/java/android/os/LegacyMessageQueue/MessageQueue.java | 受锁保护的有序链表 | 适合理解传统模型;当前还有尾指针等优化 |
| core/java/android/os/CombinedMessageQueue/MessageQueue.java | Legacy与并发队列分支 | 目录中的类不是同时执行两套next |
| core/java/android/os/CombinedDeliMessageQueue/MessageQueue.java | Legacy与DeliQueue分支 | DeliQueue使用并发提交栈、堆和原子等待状态 |
| core/java/android/os/MessageStack.java、MessageHeap.java、WaitState.java | DeliQueue的配套结构 | 分别管理并发提交、顺序选择和等待协调 |
在AOSP根目录核对构建选择:
bash
cd "$chapter3_top"
rg -n 'messagequeue-gen|release_package_messagequeue_implementation' frameworks/base/core/java/Android.bp
rg -n 'string_value' build/release/flag_declarations/RELEASE_PACKAGE_MESSAGEQUEUE_IMPLEMENTATION.textproto
rg -n 'string_value' build/release/flag_values/trunk_staging/RELEASE_PACKAGE_MESSAGEQUEUE_IMPLEMENTATION.textproto
rg -n 'getUseConcurrent|setUseDeliQueue|Message next\(' \
frameworks/base/core/java/android/os/CombinedDeliMessageQueue/MessageQueue.java
本基线的messagequeue-gen按release配置选择输入;trunk_staging的值是CombinedDeliMessageQueue/*.java。声明文件的默认值、构建规则的兜底值和具体release的覆盖值需要分别阅读,不能只取第一个搜到的值。本机已有的生成文件与CombinedDeliMessageQueue输入一致,校验记录见文末。
构建时选中CombinedDeliMessageQueue,并不等于每个进程都运行DeliQueue分支。 setUseDeliQueue、兼容性变更及进程启动参数还会影响分支选择。本章使用公共Looper.dump观察示例进程,本轮默认应用实际显示Legacy,另用兼容性开关验证DeliQueue;命令与恢复在3.12给出。
构建依据:MessageQueue生成规则、release配置。DeliQueue的设计说明也随本基线提供:源码内README。
3.2 用整体架构理解线程与消息
Handler 选择目的地 Looper 驱动执行
先给结论:Handler关联一个Looper,Looper属于一个线程;调用post的线程负责提交,最终回调由那个Looper所在线程执行。 Handler自身不会因为收到消息就创建线程。
java
Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.post(() -> updateUi());
这是教学片段,updateUi()代表界面操作。即使这两行在工作线程中执行,只要明确传入主线程的Looper,任务的目标仍是主线程。同一Looper可以关联多个Handler,它们共用同一个MessageQueue,并由同一线程串行分派。
| 对象 | 它是什么 | 本章中的用途 |
|---|---|---|
| Message | 一次待分派工作及其元数据 | what、obj、target、callback、when |
| Handler | 关联Looper的提交与分派入口 | post Runnable或发送Message |
| MessageQueue | 管理待处理工作,决定哪条消息现在可以执行 | 比较时间、屏障和退出状态 |
| Java Looper | 在线程里反复取消息并分派 | next → dispatchMessage → 下一轮 |
| NativeMessageQueue | JNI创建的Native桥接对象 | Java mPtr指向它,连接Native Looper |
| Native Looper | 管理epoll、唤醒fd和Native事件回调 | 等待就绪事件与超时 |
| epoll / eventfd | Kernel提供的事件等待与通知机制 | epoll等待;eventfd承载唤醒通知 |
整体架构里有两条路径
下面是同一进程内的架构示意。图中分别标出发送线程、目标Looper线程与Kernel等待机制;跨线程共享的是进程中的Java对象,内核中的eventfd不保存Runnable内容。 
消息路径负责"做什么",唤醒路径负责"现在重新检查"。 发送线程把工作放进Java MessageQueue;需要唤醒时写eventfd,让目标线程的等待结束。目标线程返回Java,再从队列挑选可执行的Message。没有把Java代码复制到内核执行的过程。
图中的排队操作与目标线程的取消息操作会并发发生,真正的可见性和先后关系由锁或原子操作协调。架构图不表示每次入队都会产生nativeWake,也不表示接收线程一定已经睡着。
双向连线表示同一目标线程进入等待接口并从接口返回,图中省略执行结束后的循环回边;下面的时序图再把先后过程串起来。
用时序图区分谁正在执行
下面以"目标线程正等待一条未来消息,发送线程插入更早消息"为例;这是机制示意,具体延迟另看实测。 
Java → JNI → Native是同一线程中的代码层次变化;eventfd通知和调度器使等待线程获得执行机会。被唤醒不等于立刻占用CPU,更不等于绕过正在执行的回调。
3.3 运行一个可观察的 Handler 示例
先知道四种模式要观察什么
示例使用一个APK、一个进程;主线程和HandlerThread的TID不同。每条日志都打印uptime、PID、TID、线程名与动作,方便把消息的提交和执行分开。
| 模式 | 改变的条件 | 预期现象 |
|---|---|---|
| normal | 先投递未来任务,再从主线程插入更早任务 | earlier先执行,future接近原定时间执行;另比较Runnable、Callback、handleMessage |
| block | 目标线程先处理1100 ms的任务 | 后续消息即使已到期也要等待;主线程仍能打印自己的日志 |
| quit | 在当前任务暂停期间提交已到期与未来任务,再请求退出 | quit丢弃待处理消息;quitSafely处理可分派的已到期工作,丢弃未来工作 |
| fd | 在目标线程注册pipe的可读监听,再向pipe写一个字节 | Java fd listener在注册队列所属线程收到事件 |
为了便于观察,block模式用SystemClock.sleep模拟慢回调,它占住消息循环但不代表持续使用CPU。quit模式的CountDownLatch只是控制实验条件,使用超时避免无限等候。
构建与安装
完整工程在示例目录,源码包作为本地配套资料保留。本章沿用第二章的独立Host工具构建方式:javac编译Java,D8生成dex,aapt2打包manifest,再用测试证书签名。它生成APK,不修改系统镜像。
bash
python3 "$chapter3_notes/示例/构建示例APK.py" \
--aosp-top "$chapter3_top" \
--module "$chapter3_notes/示例/Chapter3HandlerDemo" \
--name Chapter3HandlerDemo \
--out "$chapter3_run/build"
"$chapter3_adb" -s "$chapter3_serial" install -r \
"$chapter3_run/build/Chapter3HandlerDemo.apk"
前置材料包括已有的AOSP Host工具、公共SDK android.jar、JDK与测试签名材料;缺失时脚本会列出路径,先按第一章补齐。输出目录必须为空,每轮构建使用新的记录目录。预期最后输出APK路径,安装返回Success;脚本把实际构建命令保存在build-commands.json。
已具备可用lunch配置时,也可使用Soong。仅选择一种构建路径即可:
bash
cd "$chapter3_top"
chapter3_module="$chapter3_top/packages/apps/Chapter3HandlerDemo"
mkdir -p "$chapter3_module"
cp -r "$chapter3_notes/示例/Chapter3HandlerDemo/." "$chapter3_module/"
cp "$chapter3_module/Android.bp.example" "$chapter3_module/Android.bp"
source build/envsetup.sh
# 沿用第一章已选择的有效 lunch 配置,再执行:
m Chapter3HandlerDemo -j2
固定目录用于后续重复构建,不要每次创建一个仍声明同名模块的新目录。若曾按旧步骤复制到随机后缀目录,先确认并移走那些旧副本中的Android.bp,使源码树只保留一个Chapter3HandlerDemo模块。
展开目录中的Android.bp.example是模板,源码包里已命名为Android.bp。知识库采用模板名,避免它被Soong再扫描成一个同名模块。Soong产物位置按本次构建输出确认,不能把上面独立脚本的成功当成Soong构建成功。
运行 normal 并分清日志中的两端
bash
"$chapter3_adb" -s "$chapter3_serial" shell am force-stop com.example.handlerdemo
"$chapter3_adb" -s "$chapter3_serial" shell am start -W \
-n com.example.handlerdemo/.MainActivity --es scenario normal
"$chapter3_adb" -s "$chapter3_serial" logcat -v threadtime -s 'HandlerDemo:I' '*:S'
观察约三秒后,在Host按Ctrl+C停止读取日志。预期主线程打印POST、SENDER,chapter3-worker打印RUN、CALLBACK或HANDLE,最后MAIN result=15回到主线程。PID相同而TID不同说明这次是进程内线程交接。每次切换模式先force-stop再启动,避免前一轮消息影响结果。
accepted=true只表示成功提交;退出、取消等条件仍可能让任务不执行。日志先后也有并发:工作线程可能在发送线程打印POST之前就开始执行到期任务,不应把两条日志的输出顺序当作入队协议本身。
下面这一行来自2026-10-19的历史normal记录,保留了应用打印的完整字段;logcat外层的日期、级别和标签省略:
text
uptime=249125 pid=3954 tid=3954 thread=main POST future due=250725 accepted=true
它说的是:主线程成功提交了future,最早到uptime=250725 ms才允许执行。它没有说future已经执行。
| 字段 | 在这一行中怎么读 |
|---|---|
| uptime=249125 | 打日志时的开机运行时间,单位ms;不累计深度睡眠 |
| pid=3954 | 这一轮所属的进程;不要把不同启动轮次混在一起 |
| tid=3954、thread=main | 当前打印者是主线程;判断Linux线程以tid为准 |
| POST future | postAt提交动作已返回,任务名叫future |
| due=250725 | 传给postAtTime的最早可执行时间;不是保证启动的时刻 |
| accepted=true | 队列接受了提交;取消、退出仍可能使它不执行 |
这些字段由下方完整MainActivity中的mark方法记录。后文历史节选保留uptime、tid和事件,省去相同pid、线程名与logcat外层前缀;不同实验分别标明日期和线程身份,不拼成一次运行。
示例的三个完整文件
工程只有构建声明、manifest与MainActivity,完整内容如下。运行后可回到3.4,沿短代码摘要理解每一层。
Android.bp
bp
android_app {
name: "Chapter3HandlerDemo",
manifest: "AndroidManifest.xml",
srcs: ["src/**/*.java"],
sdk_version: "current",
min_sdk_version: "28",
certificate: "testkey",
optimize: { enabled: false },
}
AndroidManifest.xml
xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.handlerdemo">
<uses-sdk android:minSdkVersion="28" />
<application android:label="Handler Demo" android:debuggable="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
MainActivity.java
展开完整MainActivity.java,包含四组实验与退出清理
java
package com.example.handlerdemo;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.SystemClock;
import android.os.Trace;
import android.system.Os;
import android.util.Log;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public final class MainActivity extends Activity {
private static final String TAG = "HandlerDemo";
private final Handler main = new Handler(Looper.getMainLooper());
private final ArrayList<HandlerThread> threads = new ArrayList<>();
private final ArrayList<Handler> handlers = new ArrayList<>();
private final Object lifecycleLock = new Object();
// Guarded by lifecycleLock. Only main executes the accepted callbacks.
private boolean destroyed;
private Handler fdHandler;
private Runnable fdCleanup;
private boolean postToMain(Runnable task, long delayMillis) {
synchronized (lifecycleLock) {
if (destroyed) return false;
return main.postDelayed(() -> {
synchronized (lifecycleLock) {
if (destroyed) return;
}
task.run();
}, delayMillis);
}
}
private static void mark(String event) {
Log.i(TAG, "uptime=" + SystemClock.uptimeMillis()
+ " pid=" + Process.myPid() + " tid=" + Process.myTid()
+ " thread=" + Thread.currentThread().getName() + " " + event);
}
private Handler newWorker(String name) {
HandlerThread thread = new HandlerThread(name);
thread.start();
Handler handler = new Handler(thread.getLooper());
threads.add(thread);
handlers.add(handler);
return handler;
}
private static void postAt(Handler handler, String name, long due, long workMillis) {
boolean accepted = handler.postAtTime(() -> {
Trace.beginSection("Chapter3/" + name);
try {
mark("RUN " + name + " due=" + due
+ " late=" + (SystemClock.uptimeMillis() - due));
if (workMillis > 0) SystemClock.sleep(workMillis);
mark("END " + name);
} finally {
Trace.endSection();
}
}, due);
mark("POST " + name + " due=" + due + " accepted=" + accepted);
}
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
TextView hint = new TextView(this);
hint.setText("查看 HandlerDemo 日志。每次选择前先退出并重新启动,避免实验相互影响。");
layout.addView(hint);
for (String scenario : new String[]{"normal", "block", "quit", "fd"}) {
Button button = new Button(this);
button.setText(scenario);
button.setOnClickListener(view -> runScenario(scenario));
layout.addView(button);
}
Button exit = new Button(this);
exit.setText("结束实验并退出");
exit.setOnClickListener(view -> finish());
layout.addView(exit);
setContentView(layout);
String scenario = getIntent().getStringExtra("scenario");
if (scenario != null) runScenario(scenario);
int finishAfter = getIntent().getIntExtra("finish_after_ms", 0);
if (finishAfter > 0) postToMain(this::finish, finishAfter);
}
private boolean started;
private void runScenario(String scenario) {
if (started) {
mark("SKIP: exit and relaunch before another scenario");
return;
}
started = true;
mark("START " + scenario);
switch (scenario) {
case "normal": normal(); break;
case "block": blocked(); break;
case "quit": quitting(false); quitting(true); break;
case "fd": fileDescriptor(); break;
default: mark("UNKNOWN scenario");
}
}
private void normal() {
Handler worker = newWorker("chapter3-worker");
Looper looper = worker.getLooper();
worker.post(() -> {
looper.dump(line -> mark("QUEUE " + line), "");
Looper.myQueue().addIdleHandler(() -> {
mark("IDLE");
return false;
});
});
Handler dispatch = new Handler(looper, msg -> {
mark("CALLBACK what=" + msg.what);
return msg.what == 10;
}) {
@Override public void handleMessage(Message msg) {
mark("HANDLE what=" + msg.what);
}
};
handlers.add(dispatch);
dispatch.post(() -> mark("RUNNABLE"));
dispatch.sendEmptyMessage(10);
dispatch.sendEmptyMessage(20);
long start = SystemClock.uptimeMillis();
postAt(worker, "future", start + 1600, 0);
postToMain(() -> {
Trace.beginSection("Chapter3/sendEarlier");
try {
mark("SENDER earlier message");
postAt(worker, "earlier", SystemClock.uptimeMillis(), 0);
} finally {
Trace.endSection();
}
}, 400);
postToMain(() -> {
worker.post(() -> {
int result = 7 + 8;
mark("WORKER result=" + result);
postToMain(() -> mark("MAIN result=" + result), 0);
});
}, 1900);
}
private void blocked() {
Handler worker = newWorker("chapter3-block");
long start = SystemClock.uptimeMillis();
postAt(worker, "long", start + 100, 1100);
postAt(worker, "deadline", start + 300, 0);
postToMain(() -> mark("MAIN remains responsive"), 500);
}
private void quitting(boolean safely) {
String name = safely ? "safe" : "immediate";
Handler worker = newWorker("chapter3-" + name);
HandlerThread thread = threads.get(threads.size() - 1);
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
AtomicBoolean gateValid = new AtomicBoolean(true);
worker.post(() -> {
mark("GATE " + name);
entered.countDown();
boolean released = false;
try {
released = release.await(5, TimeUnit.SECONDS);
if (!released) {
gateValid.set(false);
mark("INVALID gate timeout: " + name);
}
} catch (InterruptedException e) {
gateValid.set(false);
mark("INVALID gate interrupted: " + name);
Thread.currentThread().interrupt();
}
mark("GATE_END " + name + " released=" + released);
});
new Thread(() -> {
boolean quitRequested = false;
try {
if (!entered.await(3, TimeUnit.SECONDS)) {
gateValid.set(false);
mark("INVALID gate not entered: " + name);
return;
}
if (!gateValid.get()) {
mark("INVALID gate already expired: " + name);
return;
}
long now = SystemClock.uptimeMillis();
postAt(worker, name + "_due", now, 0);
postAt(worker, name + "_future", now + 5000, 0);
quitRequested = safely ? thread.quitSafely() : thread.quit();
boolean after = worker.post(() -> mark("ERROR after quit: " + name));
mark("QUIT " + name + " requested=" + quitRequested
+ " afterAccepted=" + after);
} catch (InterruptedException e) {
gateValid.set(false);
mark("INVALID control interrupted: " + name);
Thread.currentThread().interrupt();
} finally {
if (!quitRequested) thread.quit();
release.countDown();
}
try {
thread.join(3000);
mark("STOPPED " + name + " alive=" + thread.isAlive());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "chapter3-control-" + name).start();
}
private void fileDescriptor() {
Handler worker = newWorker("chapter3-fd");
int registrationPause = Math.max(0, Math.min(2000,
getIntent().getIntExtra("fd_registration_pause_ms", 0)));
int registrationDelay = Math.max(0, Math.min(2000,
getIntent().getIntExtra("fd_registration_delay_ms", 0)));
try {
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
fdHandler = worker;
fdCleanup = () -> {
worker.getLooper().getQueue().removeOnFileDescriptorEventListener(
pipe[0].getFileDescriptor());
for (ParcelFileDescriptor fd : pipe) {
try { fd.close(); } catch (Exception e) { mark("CLOSE " + e); }
}
mark("FD_CLEANED");
};
worker.postDelayed(() -> {
MessageQueue queue = Looper.myQueue();
queue.addOnFileDescriptorEventListener(pipe[0].getFileDescriptor(),
MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT,
(fd, events) -> {
Trace.beginSection("Chapter3/fdCallback");
try {
if ((events & MessageQueue.OnFileDescriptorEventListener
.EVENT_INPUT) != 0) {
byte[] data = new byte[1];
int count = Os.read(fd, data, 0, 1);
mark("FD_READ bytes=" + count + " value=" + data[0]);
} else {
mark("FD_ERROR events=" + events);
}
} catch (Exception e) {
mark("FD_EXCEPTION " + e);
} finally {
Trace.endSection();
}
return 0;
});
mark("FD_REGISTERED");
if (registrationPause > 0) {
mark("TEST registration pause=" + registrationPause);
SystemClock.sleep(registrationPause);
}
boolean accepted = postToMain(() -> {
try {
int count = Os.write(pipe[1].getFileDescriptor(), new byte[]{42}, 0, 1);
mark("FD_WRITE bytes=" + count);
} catch (Exception e) {
mark("FD_WRITE_EXCEPTION " + e);
}
}, 500);
mark("FD_WRITE_SCHEDULED accepted=" + accepted);
}, registrationDelay);
} catch (Exception e) {
mark("PIPE_EXCEPTION " + e);
}
}
@Override protected void onDestroy() {
synchronized (lifecycleLock) {
destroyed = true;
main.removeCallbacksAndMessages(null);
}
for (Handler handler : handlers) handler.removeCallbacksAndMessages(null);
if (fdHandler != null) fdHandler.post(fdCleanup);
for (HandlerThread thread : threads) thread.quitSafely();
mark("DESTROY");
super.onDestroy();
}
}
实验中的UI只是模式入口,主要证据是日志与Trace。退出按钮调用finish,由onDestroy关闭新投递入口、移除待处理回调、安排fd监听清理,并请求工作线程quitSafely;force-stop是重置实验的另一种手段,不会按正常Activity生命周期走完onDestroy。
3.4 Looper 怎样绑定线程并创建 Native 对象
HandlerThread 在自己的 run 中准备 Looper
normal历史日志中,同一PID=3954内,START由主线程TID=3954打印,RUNNABLE由worker TID=3970打印:
text
uptime=249121 tid=3954 START normal
uptime=249127 tid=3970 QUEUE (MessageQueue is using Legacy implementation)
uptime=249127 tid=3970 RUNNABLE
QUEUE行来自demo调用的Looper.dump,确认这轮实际使用Legacy;它是当时的队列快照,不是证明线程随后一直在运行或已经睡眠的记录。
先从3.3的normal与newWorker认清变量;以下分别是这两个方法中的连续示例节选,不是另一个App版本:
java
Handler worker = newWorker("chapter3-worker");
Looper looper = worker.getLooper();
java
HandlerThread thread = new HandlerThread(name);
thread.start();
Handler handler = new Handler(thread.getLooper());
worker是返回的Handler,thread才是HandlerThread对象;后面传给Handler构造函数的是thread所关联的Looper。
调用thread.start()之后,run在新线程执行。创建HandlerThread对象的线程和运行Looper的线程要区分;示例在主线程创建它,真正的循环在chapter3-worker内运行。
来源:HandlerThread.java · public void run()。以下为该函数内的连续原文摘录。
java
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
prepare在新线程建立Looper;loop也在这个新线程中进入循环。 getLooper的实现会在已启动但尚未发布Looper时等待,因此示例先start,再取得Looper。传入明确Looper的Handler构造过程只是保存引用:
来源:Handler.java · public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async,。以下为该函数内的连续原文摘录。
java
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
mAsynchronous = async;
mIsShared = shared;
执行目的地由mLooper决定,不由Handler构造位置决定。 多个Handler可以把mQueue指向同一队列,消息的target再区分由哪个Handler分派。
ThreadLocal 让当前线程找到自己的 Looper
前面HandlerThread.run中的Looper.prepare()和Looper.myLooper()没有传入线程参数。它们怎么知道正在操作哪条线程?关键就在 ThreadLocal:让同一个ThreadLocal对象,在不同线程上关联各自的值。
先把"线程局部"理解为每个值班员自己的一本登记簿。同一个栏目名写在不同人的登记簿上,查到的内容可以不同;这个比喻对应的是每条线程自己的map,以及作为键的ThreadLocal对象。保存进去的仍是Java引用,不会自动复制对象。
Looper用的是同一个静态ThreadLocal:
frameworks/base/core/java/android/os/Looper.java · sThreadLocal字段,连续源码节选。
java
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
static说明这只ThreadLocal对象共享,并不说明所有线程只有一个Looper值。 访问哪个值,还要看当前调用线程。下面的表先演示普通ThreadLocal的set/get,指定顺序只是教学说明,没有新增设备日志:
java
ThreadLocal<String> label = new ThreadLocal<>();
| 执行位置 | 对同一只label的操作 | 这个线程看到什么 |
|---|---|---|
| main | label.set("main") | 给main关联字符串 "main" |
| worker | label.get() | 默认初值null,读不到main刚存的字符串 |
| worker | label.set("worker") | 给worker关联字符串 "worker" |
| main | label.get() | 仍是 "main" |
这里使用普通new ThreadLocal<>(),默认initialValue返回null;自定义初值的ThreadLocal可以不同。隔离的是关联项,不是给value加锁:如果两条线程set的是同一个可变对象引用,它们仍然共享那个对象。 这与后文"队列安全不等于业务对象安全"是同一条引用边界。
值存在哪里:先选当前 Thread,再用 ThreadLocal 作键
真实数据结构的方向容易想反:不是ThreadLocal内部保存一张"线程 → 值"的总表,而是每个Thread对象持有自己的ThreadLocalMap。 这张map还能保存本线程的其他ThreadLocal项。
以下libcore源码固定于提交4ebfb391ee0109d16abe6e2bc965724b940928c5,与本工程文件核对;本节只追普通HandlerThread使用的当前线程路径。定位:Thread.threadLocalsr1、ThreadLocal的set/get与mapr2。
libcore/ojluni/src/main/java/java/lang/Thread.java · threadLocals字段,连续源码节选。
java
ThreadLocal.ThreadLocalMap threadLocals = null;
set入口先取得当前线程,再把线程和值交给私有重载。末尾TRACE分支是诊断,不改变选取当前线程map的步骤。
libcore/ojluni/src/main/java/java/lang/ThreadLocal.java · set(T value),连续源码节选。
java
public void set(T value) {
set(Thread.currentThread(), value);
if (TRACE_VTHREAD_LOCALS) {
dumpStackIfVirtualThread();
}
}
再看接收这两个实参的私有方法。这里t是上面传入的当前线程,this是调用set的那只ThreadLocal;在Looper场景中,它就是sThreadLocal。
同一文件 · set(Thread t, T value),连续源码节选。
java
private void set(Thread t, T value) {
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
getMap取的是t所属的map;map尚未建立时,createMap才创建它,把当前ThreadLocal作为首个键、value作为对应值。
同一文件 · getMap(Thread t) 与createMap(Thread t, T firstValue),以下分别为连续源码节选。
java
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
java
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
读值同样先按当前线程选map。注意本工程版本的公开get() 先委托给私有get(Thread),不能把后者误认成另一个公开API:
同一文件 · get(),连续源码节选。
java
public T get() {
return get(Thread.currentThread());
}
同一文件 · get(Thread t),连续源码节选。
java
private T get(Thread t) {
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
return setInitialValue(t);
}
map.getEntry(this)用同一只ThreadLocal作键,e.value才是取回的值。找不到关联项时进入setInitialValue(t):本版会调用initialValue(),登记并返回初值;普通ThreadLocal的默认值是null。所以"还没有Looper"不意味着整张map不存在,也不意味着get会自动创建Looper。
把set/get的共同方向记住:当前线程 → 它的threadLocals → 以当前ThreadLocal对象为键的项 → value。
放回 Looper:prepare 保存,myLooper 读取
Looper.prepare的核心是检查并设置线程局部变量。ThreadLocal的值属于当前线程,不能理解成全进程只有一个Looper。
来源:Looper.java · private static void prepare(boolean quitAllowed)。以下为该函数内的连续原文摘录。
java
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
在worker中执行这段代码,get检查的就是worker的关联值。已有Looper则拒绝重复prepare;没有时,new Looper(quitAllowed)创建对象,再由set保存到worker的map。因此"一条线程只能prepare一个Looper"是这段检查实现的约束,不是ThreadLocal本身禁止覆盖值。
来源:Looper.java · myLooper(),连续源码节选。
java
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
myLooper只是读取调用线程的值。main调用时读到main Looper,worker在prepare后调用时读到worker Looper;普通新线程若尚未prepare,会读到null。主线程的准备由ActivityThread.main调用prepareMainLooper完成,Activity不应重复调用prepare或loop。prepare建立并登记对象,loop才驱动消息循环;有值不等于已经在处理消息。
先看动图:始终只有上方那只共享的sThreadLocal,观察当前线程改变时,箭头选择了哪张map;最后再看主线程怎样从HandlerThread的对象字段取得worker Looper。这是概念过程,main已准备好,worker从尚未关联Looper开始;动画顺序不承诺真实线程调度先后。 
主线程为什么能取得 worker 的 Looper
这正是demo中new Handler(thread.getLooper())容易误解的地方。Looper.myLooper() 按"谁在调用"查询,thread.getLooper() 按"哪只HandlerThread对象"取得它保存的Looper。 后者没有让main的ThreadLocal去读取worker的map。
前面的HandlerThread.run已经展示:worker先prepare,再在synchronized (this)内把自己的myLooper() 保存到对象字段mLooper,随后notifyAll。主线程调用同一对象的getLooper,使用的也是这把对象锁:
来源:HandlerThread.java · getLooper(),连续源码节选。
java
public Looper getLooper() {
if (!isAlive()) {
return null;
}
boolean wasInterrupted = false;
// If the thread has been started, wait until the looper has been created.
synchronized (this) {
while (isAlive() && mLooper == null) {
try {
wait();
} catch (InterruptedException e) {
wasInterrupted = true;
}
}
}
/*
* We may need to restore the thread's interrupted flag, because it may
* have been cleared above since we eat InterruptedExceptions
*/
if (wasInterrupted) {
Thread.currentThread().interrupt();
}
return mLooper;
}
已启动且仍存活、但mLooper尚未发布时,调用方会在这里wait;worker发布并通知后,调用方重新检查条件,再取得字段引用。未启动或已不存活会返回null,getLooper本身不负责启动线程。它也可能在worker真正进入Looper.loop之前返回,不应把"拿到Looper"当作"循环已开始执行"的证据。
因此主线程能够把 worker的Looper引用交给Handler构造函数,Handler再保存该Looper和队列,后续post的目的地才是worker。这里跨线程交付引用依靠HandlerThread的字段与同步;ThreadLocal负责的是各线程定位自己的Looper。
| 在main调用 | 从哪里取得引用 | 本例的结果 |
|---|---|---|
| Looper.myLooper() | main的ThreadLocalMap | main Looper |
| Looper.getMainLooper() | 框架保存的静态sMainLooper,访问受锁保护 | main Looper |
| thread.getLooper() | 指定HandlerThread对象的mLooper | 运行正常且准备完成时为worker Looper |
| worker.getLooper() | 名为worker的Handler保存的mLooper | 构造时指定的worker Looper |
Looper.getMainLooper读取的是单独保存的主Looper引用,因此也能在worker调用时取得main Looper;它与"当前线程"的myLooper语义不同。新建普通ThreadLocal不会让子线程自动获得父线程的值;本例HandlerThread正是自己执行prepare建立关联。
ThreadLocal的remove() 删除的是当前线程对这只ThreadLocal的关联项。自行在线程池任务中保存上下文时,可用finally清理自己的项,避免影响后续任务;不要移除或改写框架用于登记Looper的内部sThreadLocal。这一节也不把Java ThreadLocal与后面Native Looper的线程局部存储当成同一个实现。
同一线程不能重复prepare;不同线程可以分别持有自己的Looper。 Looper构造时把队列和线程保存下来:
来源:Looper.java · private Looper(boolean quitAllowed)。以下为该函数内的连续原文摘录。
java
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
这也解释了两个常见错误:未prepare的普通线程使用隐式Handler构造会报错;仅prepare而不进入loop,消息会排队但没有循环来执行。应用主线程由Framework在ActivityThread.main中准备并启动Looper,业务Activity不应再次prepare或手动loop。
Java mPtr 指向的是 NativeMessageQueue
选定MessageQueue实现的构造过程都会建立Native关联。本文CombinedDeliMessageQueue构造中有:
来源:MessageQueue.java · MessageQueue(boolean quitAllowed)。以下为该函数内的连续原文摘录。
java
mPtr = nativeInit();
nativeInit的返回值从这里产生:
来源:android_os_MessageQueue.cpp · static jlong android_os_MessageQueue_nativeInit(。以下为该函数内的连续原文摘录。
cpp
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
if (!nativeMessageQueue) {
jniThrowRuntimeException(env, "Unable to allocate native queue");
return 0;
}
nativeMessageQueue->incStrong(env);
return reinterpret_cast<jlong>(nativeMessageQueue);
mPtr存的是本进程NativeMessageQueue的地址表示,不能把它当作epoll fd,也不是另一个线程的Java对象地址。 incStrong让Native对象在Java队列持有期间保持有效。
NativeMessageQueue构造时再取得或建立当前线程的Native Looper:
来源:android_os_MessageQueue.cpp · NativeMessageQueue::NativeMessageQueue()。以下为该函数内的连续原文摘录。
cpp
NativeMessageQueue::NativeMessageQueue() :
mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
mLooper = Looper::getForThread();
if (mLooper == NULL) {
mLooper = new Looper(false);
Looper::setForThread(mLooper);
}
}
因此关系为:Java Looper持有Java MessageQueue,MessageQueue.mPtr关联NativeMessageQueue,后者持有Native Looper。Java Looper与Native Looper是两个不同类,共同服务于同一个目标线程;它们的消息结构也不能混成一个队列。
3.5 post 怎样变成带时间的 Message
先追 future:POST、RUN、END 各记在哪
normal先用postAt(worker, "future", start + 1600, 0)提交未来任务,约400 ms后,主线程再提交立刻到期的earlier。先只看future在demo中的记录位置:
MainActivity.java · postAt,示例连续节选,对应3.3的完整方法。
java
private static void postAt(Handler handler, String name, long due, long workMillis) {
boolean accepted = handler.postAtTime(() -> {
Trace.beginSection("Chapter3/" + name);
try {
mark("RUN " + name + " due=" + due
+ " late=" + (SystemClock.uptimeMillis() - due));
if (workMillis > 0) SystemClock.sleep(workMillis);
mark("END " + name);
} finally {
Trace.endSection();
}
}, due);
mark("POST " + name + " due=" + due + " accepted=" + accepted);
}
postAt是demo自己的辅助方法。在postAt(worker, "future", start + 1600, 0)这次调用中,参数handler接住worker,name接住 "future",due接住start + 1600,workMillis接住0;内部调用的postAtTime才是Handler API。
POST在提交方、RUN和END在回调内部。 RUN处的late是"开始执行时的uptime减去due",并非任务执行耗时;RUN到END才覆盖本段业务。mark与late各自读取时钟,核对时不要把两个采样点强求为每次完全相等。
这里也藏着一个日志陷阱:POST写在postAtTime返回之后,接收线程可能已经开始RUN;跨线程打印顺序不是内部入队协议。现在再问:这段lambda在提交时去了哪里?
post 与 postAtTime 的公共入口
普通worker.post(r)先把Runnable包装成Message,再以零相对延迟进入sendMessageDelayed:
来源:Handler.java · post(Runnable),连续源码节选。
java
public final boolean post(@NonNull Runnable r) {
return sendMessageDelayed(getPostMessage(r), 0);
}
示例的postAt辅助方法调用postAtTime(r, due),这里due已是绝对uptime时间,不再做相对延迟换算:
来源:Handler.java · postAtTime(Runnable, long),连续源码节选。
java
public final boolean postAtTime(@NonNull Runnable r, long uptimeMillis) {
return sendMessageAtTime(getPostMessage(r), uptimeMillis);
}
两者都先调用下面的getPostMessage,再汇合到sendMessageAtTime;相对延迟怎样转换成时间点,紧接着展开。
Runnable 先被装进 Message.callback
在发送线程执行handler.post(runnable)时,getPostMessage创建Message,并保存Runnable:
来源:Handler.java · private static Message getPostMessage(Runnable r)。以下为该函数内的连续原文摘录。
java
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
post传递的是Runnable引用,执行发生在以后分派callback的位置。 sendMessage则通常通过what、arg1、arg2、obj等字段携带工作信息。两者最终都走消息入队。
延迟转换成 uptime 时间点
sendMessageDelayed把相对延迟转换成SystemClock.uptimeMillis()时间基准上的绝对值:
来源:Handler.java · public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis)。以下为该函数内的连续原文摘录。
java
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
when表示最早可执行的时间,不是一个保证准点触发的闹钟。 uptimeMillis不累计深度睡眠时间,设备休眠会增加现实世界中的等待时长。跨日志计算时要保持时钟基准一致,不要直接拿日历时间减when。Handler的时间约定
sendMessageAtTime取得Handler关联的队列,再调用内部enqueueMessage:
来源:Handler.java · public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis)。以下为该函数内的连续原文摘录。
java
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
这里没有点号的enqueueMessage(queue, msg, uptimeMillis)仍是当前Handler的私有方法;下一段带queue.的调用才进入MessageQueue对象。boolean沿发送调用栈返回,由示例postAt的accepted接住;3.7的next返回Message则发生在目标线程的另一条调用栈上。
target 决定由哪个 Handler 分派
入队前的关键字段在Handler内设置:
来源:Handler.java · private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,。以下为该函数内的连续原文摘录。
java
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
onBeforeEnqueue(queue, msg, uptimeMillis);
return queue.enqueueMessage(msg, uptimeMillis);
msg.target是Handler,msg.callback才可能是Runnable。 mAsynchronous为true时会把此Handler发送的消息自动标为异步;队列最终依据Message的异步标记选择候选,普通Handler也能发送显式setAsynchronous(true) 的消息。这不表示会另开线程。队列中还会有Framework自己建立的屏障消息,它的target为空,但普通Handler入队要求target非空。
此时仍处于发送线程。到queue.enqueueMessage为止,工作是提交给目标队列,发送线程并没有替目标线程执行handleMessage。通常post很快返回,任务可能在返回前、返回后或者很久以后执行,取决于两条线程的调度和队列状态。
入队线程安全 不等于对象自动复制
下面是一个有意展示问题的教学片段,worker是已经绑定工作Looper的Handler:
java
int[] value = {1};
worker.post(() -> show(value[0]));
value[0] = 2;
发送的是引用,worker不会自动得到值为1的快照。 两个线程对入队之后的修改没有这里所需的额外同步,不能承诺读到1或2。队列的锁或原子发布机制负责让提交之前的初始化对接收者可见,不会保护提交后的所有业务读写。
需要固定本次值时,先取不可变快照,再提交:
java
final int snapshot = value[0];
worker.post(() -> show(snapshot));
show代表结果展示;复杂对象可采用不可变数据、必要的防御性复制,或独立的锁与并发协议。入队后的Message本身也不应继续修改或再次提交。这里仍走前面post → getPostMessage → callback引用 → 队列发布的同一条入口。
3.6 MessageQueue 怎样入队并决定是否唤醒
先分清两条实现分支
本节的两套代码都来自同一固定版本的CombinedDeliMessageQueue。先用Legacy理解"按时间排队,必要时叫醒",再看DeliQueue怎样减少多线程提交时的锁竞争。
第一次阅读可以先看本节Legacy入队,再接到3.7的Legacy等待与Native路径;读通一次完整往返后,再返回DeliQueue,比较它如何用原子状态完成相同的协调目标。
来源:MessageQueue.java · boolean enqueueMessage(Message msg, long when)。以下为该函数内的连续原文摘录。
java
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
if (sUseDeliQueue) {
return enqueueMessageDeliQueue(msg, when);
} else {
return enqueueMessageLegacy(msg, when);
}
}
这是二选一的分支。不要把enqueueMessageLegacy和enqueueMessageDeliQueue串成先后经过的两层。 后面的Native等待和唤醒接口可以复用,但并发协调方式不同。
Legacy 用锁保护有序链表
带着postAt的记录位置,看同一次normal中的两份提交和两次执行。以下仍是2026-10-19的PID=3954,主线程TID=3954,worker TID=3970:
text
uptime=249125 tid=3954 POST future due=250725 accepted=true
uptime=249540 tid=3954 SENDER earlier message
uptime=249540 tid=3954 POST earlier due=249540 accepted=true
uptime=249543 tid=3970 RUN earlier due=249540 late=3
uptime=249543 tid=3970 END earlier
uptime=250726 tid=3970 RUN future due=250725 late=1
uptime=250726 tid=3970 END future
future先提交,earlier却先开始。关键不在名字,而在earlier的due=249540早于future的250725;两次RUN都在worker。earlier这次晚3 ms、future晚1 ms,只描述这一轮,不能作为系统计时保证。
这组日志支持"本轮较早到期者先执行",还看不到内部采用链表还是堆。下面结合该轮QUEUE显示的Legacy,读它如何把消息放到合适位置。
enqueueMessageLegacy在synchronized (this)内检查退出、设置when并插入链表。先看锁内的拒绝与时间赋值;这一段只省略前面的重复使用检查:
来源:MessageQueue.java · enqueueMessageLegacy锁内入口,连续源码节选。
java
if (mQuitting) {
logDeadThread(msg);
return false;
}
msg.markInUse();
msg.when = when;
when从Handler传来的uptimeMillis进入队列,再保存在msg.when。下面仍在同一把锁内,新消息成为更早的头部时:
来源:MessageQueue.java · private boolean enqueueMessageLegacy(。以下为教学删节,注释标明省略位置;只展示本段讨论的分支。
java
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
if (p == null) {
mLast = mMessages;
}
} // 笔记删节:省略其余插入分支。
如果目标线程正在等待,新的队头可能让原来的等待时间过长,因此needWake取mBlocked。 插入中部通常不需要唤醒;存在队头屏障和异步消息时,还要判断它是否成为最早可执行的异步工作。该版本用mLast优化尾部插入,不能笼统说所有提交都必须从头遍历。
最终,只有判断为需要时才调用Native唤醒入口:
来源:MessageQueue.java · private boolean enqueueMessageLegacy(。以下为该函数内的连续原文摘录。
java
if (needWake) {
nativeWake(mPtr);
}
这是锁内最终的唤醒条件。先在锁内发布消息,再决定唤醒,取消息的一方也通过对应的锁检查队列,避免"唤醒了却看不到已入队消息"。
Legacy的入队部分到这里已经接上nativeWake。首次阅读先继续3.7、3.8,跟着同一条消息完成等待、取出和执行,再回来比较下面的实现。
进阶DeliQueue怎样用栈 堆和CAS协调并发提交
DeliQueue 让多条发送线程先并发提交
DeliQueue把并发提交和按时间选取拆开。发送线程先为消息记录时间与序号:
来源:MessageQueue.java · private boolean enqueueMessageUnchecked(。以下为该函数内的连续原文摘录。
java
long seq = when != 0 ? ((long) sNextInsertSeq.getAndAdd(this, 1L) + 1L)
: ((long) sNextFrontInsertSeq.getAndAdd(this, -1L) - 1L);
msg.when = when;
msg.insertSeq = seq;
msg.markInUse();
when相同的普通消息由insertSeq打破平局;FrontOfQueue使用另一组序号,具有特殊的头插语义。之后调用MessageStack.pushMessage,提交失败时撤销本次消息计数并返回:
来源:MessageQueue.java · private boolean enqueueMessageUnchecked(。以下为该函数内的连续原文摘录。
java
if (!mStack.pushMessage(msg)) {
logDeadThread(msg);
decAndTraceMessageCount();
return false;
}
进入MessageStack的实际发布逻辑:
来源:MessageStack.java · public boolean pushMessage(Message m)。以下为该函数内的连续原文摘录。
java
public boolean pushMessage(Message m) {
Message current;
do {
current = mTopValue;
if (isQuittingMessage(current)) {
return false;
}
m.next = current;
} while (!sTop.weakCompareAndSetRelease(this, current, m));
return true;
}
CAS可以理解为"只有共享值仍是我刚看到的值,才把它换成新值"。竞争失败时重新读取并重试。提交栈是后进先出的结构,但执行顺序不直接照搬栈顺序。
目标Looper线程处理消息前,先完成sweep和待删除节点清理:
来源:MessageQueue.java · Message nextMessage(boolean peek, boolean returnEarliest)。以下为该函数内的连续原文摘录。
java
mStack.heapSweep();
mStack.drainFreelist();
MessageStack.heapSweep把尚未处理的新节点加入对应的最小堆,堆按Message的时间与序号排序:
来源:MessageStack.java · public void heapSweep()。以下为该函数内的连续原文摘录。
java
if (!current.isRemoved()) {
getHeap(current).add(current);
}
current = current.next;
同步消息与屏障进入同步堆,异步消息进入异步堆。nextMessage再比较可执行候选。这样多发送者主要竞争原子栈,只有消费者线程操作堆;减少入队锁竞争不等于同一Looper会并行运行多个Runnable 。 
这是DeliQueue的结构与处理方向示意。heapSweep把节点登记到堆,并不立即把它们从提交栈摘掉;执行或删除时还有相应的移除处理。nextMessage取两组候选的入口是:
来源:MessageQueue.java · Message nextMessage(boolean peek, boolean returnEarliest)。以下为该函数内的连续原文摘录。
java
Message msg = mStack.peek(false);
Message asyncMsg = mStack.peek(true);
final long now = SystemClock.uptimeMillis();
没有屏障时比较最早候选,有屏障时按3.9的规则选择异步候选。队列策略变化了,Handler的业务接口与单线程分派约定保持一致。
等待状态协调解决临睡前的新消息
DeliQueue的mWaitState可以表示"当前活跃时的新提交计数",也可以表示"准备等待到哪个时间点"。它的意义是协调并发,不是业务消息数量的精确统计。
mWaitState是当前MessageQueue的volatile字段,sWaitState是访问该字段的VarHandle;后文compareAndSet(this, 旧状态, 新状态) 的this指当前队列对象,不是发送线程。
发送者按四种情况计算新状态:消费者仍活跃、消息不早于截止时间、可绕过屏障的更早异步消息,以及需要检查屏障的更早同步消息。下面保留完整条件,注意各分支的needWake:
来源:MessageQueue.java · private boolean enqueueMessageUnchecked(。以下为该函数内的连续原文摘录。
java
if (WaitState.isCounter(waitState)) {
// Looper is already awake
newWaitState = WaitState.incrementCounter(waitState);
checkBarrier = null;
needWake = false;
} else if (msg.when >= WaitState.getTSMillis(waitState)) {
// The enqueued message is not earlier than the current wake
// deadline, so we don't need to wake.
newWaitState = WaitState.incrementDeadline(waitState);
checkBarrier = null;
needWake = false;
} else if (msg.isAsynchronous()) {
// The enqueued message has an earlier deadline.
// It is async, so it can bypass barriers.
newWaitState = WaitState.initCounter();
checkBarrier = null;
needWake = true;
} else {
// We may need to wake up, depending on the state of the sync barrier.
Message barrier = WaitState.hasSyncBarrier(waitState) ? mSyncBarrier : null;
boolean blockedByBarrier =
barrier != null && Message.compareMessages(barrier, msg) < 0;
if (blockedByBarrier) {
newWaitState = WaitState.incrementDeadline(waitState);
checkBarrier = barrier;
needWake = false;
} else {
newWaitState = WaitState.initCounter();
checkBarrier = null;
needWake = true;
}
}
异步消息早于截止时间时可以要求唤醒;同步消息还需考虑是否被当前屏障挡住。算出新状态后通过CAS提交,必要时concurrentWake:
来源:MessageQueue.java · private boolean enqueueMessageUnchecked(。以下为该函数内的连续原文摘录。
java
if (sWaitState.compareAndSet(this, waitState, newWaitState)) {
if (checkBarrier != null && checkBarrier != mSyncBarrier) {
/*
* If barrier state changed underneath us and we chose not to wake the
* looper thread, we have to recheck to ensure that the barrier we saw was
* actually in place while we did the CAS.
*/
continue;
}
if (needWake) {
concurrentWake();
}
return true;
}
CAS成功后,如果此前因屏障决定不唤醒,还会确认那道屏障是否变化;发生变化就重新判断,因此这里不只是比较一次时间。concurrentWake用Native指针引用计数保护跨线程访问:
来源:MessageQueue.java · private void concurrentWake()。以下为该函数内的连续原文摘录。
java
private void concurrentWake() {
if (incrementMptrRefs()) {
try {
nativeWake(mPtr);
} finally {
decrementMptrRefs();
}
}
}
消费者在真正开始等待前,也要CAS发布等待截止时间:
来源:MessageQueue.java · Message nextMessage(boolean peek, boolean returnEarliest)。以下为该函数内的连续原文摘录。
java
if (!sWaitState.compareAndSet(this, oldWaitState,
WaitState.composeDeadline(nextDeadline, syncBarrier != null))) {
continue;
}
如果发送者恰在检查队列与进入等待之间提交了新消息,等待状态会变化,消费者的CAS就失败并重新检查。 如果消费者已公布等待状态,提交者则判断是否需要写eventfd。这两个方向配合,避免消费者带着过时截止时间睡过去。 
3.7 从 nativePollOnce 与 nativeWake 读到 Kernel
next 先选择等待时间 再返回可执行消息
Legacy的nextLegacy是一个内部循环。入口先从当前队列取得Native指针;队列已dispose时直接返回null:
来源:MessageQueue.java · nextLegacy() 入口,连续源码节选。
java
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
首次timeout为0,先做一次不阻塞的Native轮询:
来源:MessageQueue.java · private Message nextLegacy()。以下为教学删节,注释标明省略位置;只展示本段讨论的分支。
java
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
// 笔记删节:下面检查消息、处理 idle 与退出,必要时更新 timeout 后继续。
}
随后在锁内检查消息。now、prevMsg、msg都是本次取消息过程中的局部变量,来源如下:
来源:MessageQueue.java · nextLegacy() 锁内候选,连续源码节选。
java
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
队头若是屏障,后续代码沿next寻找异步候选;未到期时计算剩余时间:
来源:MessageQueue.java · private Message nextLegacy()。以下为教学删节,注释标明省略位置;只展示本段讨论的分支。
java
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} // 笔记删节:到期分支在下一段展开。
没有可选消息时使用 -1:
来源:MessageQueue.java · private Message nextLegacy()。以下为该函数内的连续原文摘录。
java
// No more messages.
nextPollTimeoutMillis = -1;
0表示不等待,正数表示最多等待对应毫秒数,-1表示无限等待。 等待会被其他就绪事件提前结束,因此循环返回后还要再次检查队列。
进阶DeliQueue怎样计算等待时间
DeliQueue使用同样的nativePollOnce桥接,但等待值由nextMessage和原子等待状态计算,成功取出消息后返回:
来源:MessageQueue.java · private Message nextDeliQueue()。以下为该函数内的连续原文摘录。
java
nativePollOnce(ptr, mNextPollTimeoutMillis);
Message msg = nextMessage(false, false);
if (msg != null) {
msg.markInUse();
decAndTraceMessageCount();
return msg;
}
本基线的DeliQueue未请求退出、也没有下一条候选消息时,使用一个很大的有限超时,而不是 -1:
来源:MessageQueue.java · Message nextMessage(boolean peek, boolean returnEarliest)。以下为该函数内的连续原文摘录。
java
mNextPollTimeoutMillis = INDEFINITE_TIMEOUT_MS;
nextDeadline = now + INDEFINITE_TIMEOUT_MS;
这里的INDEFINITE_TIMEOUT_MS在本文件定义为600_000_000毫秒,用于该实现的长时间等待。"空队列一定向Native传 -1"只适用于相应分支,不能当成所有版本的定义。
到期消息怎样从队列交给 Looper
轮询返回后,nextLegacy在同一把队列锁内选择候选。没有屏障时通常选择队头;有屏障时先找到可执行的异步消息。消息到期后先解除阻塞标记,并从链表摘除这个节点:
来源:MessageQueue.java · private Message nextLegacy()。以下为该函数内的连续原文摘录。
java
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
if (prevMsg.next == null) {
mLast = prevMsg;
}
} else {
mMessages = msg.next;
if (msg.next == null) {
mLast = null;
}
}
msg.next = null;
prevMsg不为空表示选中的消息位于链表中间;否则移动队头。 mLast同步维护尾指针,msg.next清空后,这条Message不再作为待处理节点连接后面的消息。
随后标记消息正在使用、更新计数并返回:
来源:MessageQueue.java · private Message nextLegacy()。以下为该函数内的连续原文摘录。
java
msg.markInUse();
if (msg.isAsynchronous()) {
mAsyncMessageCount--;
}
decAndTraceMessageCount();
return msg;
return msg才是Java队列向Looper的交接点。 它不会在nextLegacy内执行Runnable;返回到3.8的loopOnce后,才由msg.target分派。未到期时则更新timeout,再进入下一轮轮询。 
图表示Legacy的普通消息选择与交接,省略IdleHandler与退出分支;锁只覆盖队列检查和节点更新,业务回调在返回队列之后执行。
JNI 把 mPtr 还原成 NativeMessageQueue
执行者现在是目标Looper线程,nativePollOnce不会创建另一条接收线程。
来源:android_os_MessageQueue.cpp · static void android_os_MessageQueue_nativePollOnce(。以下为该函数内的连续原文摘录。
cpp
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
jlong ptr, jint timeoutMillis) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}
NativeMessageQueue::pollOnce进一步把timeout交给Native Looper,同时记录本轮可用于Java fd回调的JNI上下文:
来源:android_os_MessageQueue.cpp · void NativeMessageQueue::pollOnce(。以下为该函数内的连续原文摘录。
cpp
mPollEnv = env;
mPollObj = pollObj;
mLooper->pollOnce(timeoutMillis);
mPollObj = NULL;
mPollEnv = NULL;
mPollEnv / mPollObj服务于本轮事件回调,pollOnce返回前会清空它们。 这些值不是跨线程任意使用的Java引用。普通消息路径最终仍要返回Java next,再选择Message。
Native Looper 建立唤醒 fd 与 epoll
Native Looper构造时建立eventfd:
来源:Looper.cpp · Looper::Looper(bool allowNonCallbacks)。以下为该函数内的连续原文摘录。
cpp
mWakeEventFd.reset(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC));
LOG_ALWAYS_FATAL_IF(mWakeEventFd.get() < 0, "Could not make wake event fd: %s", strerror(errno));
std::scoped_lock _l(mLock);
rebuildEpollLocked();
rebuildEpollLocked创建epoll实例,并把唤醒eventfd的可读事件注册进去:
来源:Looper.cpp · void Looper::rebuildEpollLocked()。以下为该函数内的连续原文摘录。
cpp
mEpollFd.reset(epoll_create1(EPOLL_CLOEXEC));
LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance: %s", strerror(errno));
epoll_event wakeEvent = createEpollEvent(EPOLLIN, WAKE_EVENT_FD_SEQ);
int result = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, mWakeEventFd.get(), &wakeEvent);
LOG_ALWAYS_FATAL_IF(result != 0, "Could not add wake event fd to epoll instance: %s",
strerror(errno));
eventfd是通知计数器,epoll是等待多个fd就绪的机制;两者不是同一个fd。 eventfd可读表示需要重新检查工作,并不对应"刚好一条Java Message"。多次通知可以合并。
Native Looper.pollOnce通过下面一行调用pollInner,后者负责等待并处理就绪事件:
来源:Looper.cpp · int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData)。以下为该函数内的连续原文摘录。
cpp
result = pollInner(timeoutMillis);
pollInner返回后,外层pollOnce根据结果返回,或交付尚未读取的fd标识;Java桥接使用的回调模式会在pollInner中执行,3.10展开这一入口。originalTimeout保存进入pollInner时的timeout,epollFd是本轮使用的epoll fd,hasRequests表示是否有其他fd请求。该版本还支持在原始timeout为0且无其他fd请求时跳过epoll_wait,因此不能把nativePollOnce与一次必然发生的系统调用画等号:
来源:Looper.cpp · int Looper::pollInner(int timeoutMillis)。以下为该函数内的连续原文摘录。
cpp
if (!sSkipEpollWaitIfPossible || originalTimeout != 0 || hasRequests) {
// We are about to idle.
std::atomic_store_explicit(&mPolling, true, std::memory_order_relaxed);
eventCount = epoll_wait(epollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
// No longer idling.
std::atomic_store_explicit(&mPolling, false, std::memory_order_relaxed);
}
Bionic 把 epoll_wait 接到实际系统调用
源码里看到的是Native的epoll_wait;本基线的Bionic实现继续调用epoll_pwait64:
来源:sys_epoll.cpp · int epoll_wait(int fd, struct epoll_event* events, int max_events, int timeout)。以下为该函数内的连续原文摘录。
cpp
int epoll_wait(int fd, struct epoll_event* events, int max_events, int timeout) {
return epoll_pwait64(fd, events, max_events, timeout, nullptr);
}
来源:sys_epoll.cpp · int epoll_pwait64(int fd, epoll_event* events, int max_events, int timeout, const sigset64_t* ss)。以下为该函数内的连续原文摘录。
cpp
int epoll_pwait64(int fd, epoll_event* events, int max_events, int timeout, const sigset64_t* ss) {
return __epoll_pwait(fd, events, max_events, timeout, ss, sizeof(*ss));
}
系统调用跟踪中出现epoll_pwait,与上层读到epoll_wait可以是同一条路径。 timeout、信号处理、已就绪事件都会影响返回;并非所有epoll返回都由Handler新消息触发。
Kernel 把线程放进等待队列
以下采用与前两章Emulator Kernel来源线索对应的固定ACK提交,阅读文件为fs/eventpoll.c、fs/eventfd.c和kernel/sched/core.c。它说明等待与唤醒的实现,不把用户空间日志当成执行过每一行Kernel分支的证明。
ep_poll在锁内检查是否已有事件;没有时把当前线程放入epoll等待队列,并通过调度等待事件或超时:
来源:eventpoll.c · static int ep_poll(struct eventpoll *ep,。以下为该函数内的连续原文摘录。
c
eavail = ep_events_available(ep);
if (!eavail)
__add_wait_queue_exclusive(&ep->wq, &wait);
spin_unlock_irq(&ep->lock);
if (!eavail)
timed_out = !schedule_hrtimeout_range(to, slack,
HRTIMER_MODE_ABS);
__set_current_state(TASK_RUNNING);
等待阶段线程可以让出CPU,循环并不需要一直忙着检查Java队列。 超时、信号或就绪事件使它退出相应等待,之后重新收集事件;被唤醒到真正重新运行之间仍可能有调度延迟。
发送线程写 eventfd 使等待者重新可运行
回到发送线程:Java nativeWake还原同一个NativeMessageQueue对象,并调用wake:
来源:android_os_MessageQueue.cpp · static void android_os_MessageQueue_nativeWake(。以下为该函数内的连续原文摘录。
cpp
static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->wake();
}
NativeMessageQueue::wake直接调用其mLooper->wake(),后者向唤醒fd写入64位计数:
来源:Looper.cpp · void Looper::wake()。以下为该函数内的连续原文摘录。
cpp
uint64_t inc = 1;
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd.get(), &inc, sizeof(uint64_t)));
Kernel的eventfd_write更新计数,并向注册的等待者报告可读事件:
来源:eventfd.c · static ssize_t eventfd_write(。以下为教学删节,注释标明省略位置;只展示本段讨论的分支。
c
if (likely(res > 0)) {
ctx->count += ucnt;
current->in_eventfd = 1;
if (waitqueue_active(&ctx->wqh))
wake_up_locked_poll(&ctx->wqh, EPOLLIN);
current->in_eventfd = 0;
} // 笔记删节:省略写入失败与后续返回处理。
epoll注册到该fd上的等待回调ep_poll_callback负责登记就绪事件,并唤醒自己的等待队列;其中的唤醒选择为:
来源:eventpoll.c · static int ep_poll_callback(。以下为该函数内的连续原文摘录。
c
if (sync)
wake_up_sync(&ep->wq);
else
wake_up(&ep->wq);
前面ep_poll安装的等待函数继续接到调度器:
来源:eventpoll.c · static int ep_autoremove_wake_function(。以下为该函数内的连续原文摘录。
c
int ret = default_wake_function(wq_entry, mode, sync, key);
来源:core.c · int default_wake_function(wait_queue_entry_t *curr,。以下为该函数内的连续原文摘录。
c
return try_to_wake_up(curr->private, mode, wake_flags);
try_to_wake_up让满足条件的等待任务进入可运行路径;它没有直接调用Java Runnable。 调度器选中目标线程后,原来的等待调用才能继续。
目标线程消耗通知 再回到 Java 检查队列
Native Looper识别唤醒fd后调用awoken,读取并消耗计数:
来源:Looper.cpp · void Looper::awoken()。以下为该函数内的连续原文摘录。
cpp
uint64_t counter;
TEMP_FAILURE_RETRY(read(mWakeEventFd.get(), &counter, sizeof(uint64_t)));
awoken读取计数后继续当前pollInner;pollInner给pollOnce返回结果,随后退出NativeMessageQueue::pollOnce和JNI nativePollOnce,回到Java next中nativePollOnce的下一行,再检查当前队列状态。通知和Message不要求一一对应:等待期间可能有多条消息提交,也可能发生取消、屏障变化或退出,所以醒来后重新判断,而不是假定一定有一条特定消息 。 
这张图只画由eventfd引起的唤醒。延迟消息也可通过原等待的timeout到期获得检查机会;其他fd事件另见3.10。此链路实际终点是Kernel事件与线程调度,没有需要补入的HAL或物理外设驱动。
3.8 回到 Java 分派消息并交回主线程
Looper 从 next 取得 Message
HandlerThread.run已进入Looper.loop。loop开始先用final Looper me = myLooper();取得当前线程已登记的Looper,未准备时抛异常;这里的me因而是worker的Looper。它反复调用loopOnce,只有后者返回false才正常离开循环:
来源:Looper.java · public static void loop()。以下为该函数内的连续原文摘录。
java
for (;;) {
if (!loopOnce(me, ident, thresholdOverride)) {
return;
}
}
next()是MessageQueue的方法,它按sUseDeliQueue二选一调用nextDeliQueue或nextLegacy;不是Looper自己定义的next。next返回有用消息后,Java Looper.loopOnce继续执行;返回null则表示队列正在退出:
来源:Looper.java · private static boolean loopOnce(。以下为该函数内的连续原文摘录。
java
Message msg = me.mQueue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return false;
}
随后由这条Message的target分派:
来源:Looper.java · private static boolean loopOnce(。以下为该函数内的连续原文摘录。
java
msg.target.dispatchMessage(msg);
取消息和执行回调都在目标Looper线程中。 主线程循环也采用这个模型,它通过消息驱动Activity生命周期、输入和界面工作;Looper.loop本身的循环不等于ANR,耗时回调、等待锁或同步远程调用占住主线程才可能使关键工作迟迟得不到处理。
Runnable Callback 和 handleMessage 有先后顺序
当前版本的公开dispatchMessage先调用dispatchMessageImpl,实际选择逻辑在下面:
来源:Handler.java · public void dispatchMessageImpl(@NonNull Message msg)。以下为该函数内的连续原文摘录。
java
public void dispatchMessageImpl(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
优先级是Message.callback → Handler.Callback → 子类handleMessage。 Message.callback存在时直接执行Runnable;没有时才询问Handler.Callback,返回true表示已处理,返回false才继续handleMessage。
Runnable的最终执行点是:
来源:Handler.java · private static void handleCallback(Message message)。以下为该函数内的连续原文摘录。
java
private static void handleCallback(Message message) {
message.callback.run();
}
因此normal模式应观察到RUNNABLE、CALLBACK what=10、CALLBACK what=20、HANDLE what=20;what=10已由Callback消费,不应再出现HANDLE what=10。Runnable路径也不会先交给Handler.Callback。
2026-10-19这轮的四行都来自worker TID=3970:
text
uptime=249127 tid=3970 RUNNABLE
uptime=249127 tid=3970 CALLBACK what=10
uptime=249127 tid=3970 CALLBACK what=20
uptime=249127 tid=3970 HANDLE what=20
两种callback也要分开:Message.callback是每条消息携带的Runnable;Handler.Callback是构造Handler时保存的mCallback。输入与分支的对应可以解释本例日志,不能把其他应用缺一行日志一概归结为被Callback消费,还需排查过滤、取消与异常。
从工作线程返回界面需要第二次投递
示例先让worker计算,再向主线程Handler投递结果:
java
worker.post(() -> {
int result = 7 + 8;
main.post(() -> mark("MAIN result=" + result));
});
这是两次投递的机制摘要。完整示例的第二次投递经过postToMain,它向同一个主线程Handler提交,同时协调生命周期状态。外层Runnable在worker所属Looper执行,内层Runnable在main所属Looper执行。一次post对应一次目的地选择,工作线程不会在Runnable执行到一半时自动变成主线程。
postToMain包装生命周期检查,再向绑定主Looper的main Handler提交。2026-10-19同一次normal的末尾是:
text
uptime=251025 tid=3970 WORKER result=15
uptime=251026 tid=3954 MAIN result=15
两个15说明这次计算结果被回传;TID从3970变成3954,才说明打印位置从worker回到了main。 它不是同一段调用栈直接走到主线程:worker提交第二份Runnable,主线程的Looper随后取出并分派。
这也接回第二章:Binder callback的接收线程调用main.post,只负责把界面工作放进主线程队列;Binder事务结束与界面回调执行是两个不同阶段。普通Handler不具备跨进程传输能力;Messenger则先用Binder收到消息,再投到Handler,详见后续应用组件章节。
Framework 服务也通过 Looper 选择处理线程
系统服务并不只有Binder接收线程。以FgThread为例,它继承ServiceThread,后者继承HandlerThread;当前版本的构造过程启动线程,再用它的Looper建立共用Handler:
来源:FgThread.java · private FgThread()。以下为该函数内的连续原文摘录。
java
private FgThread() {
super("android.fg", android.os.Process.THREAD_PRIORITY_DEFAULT, true /*allowIo*/);
start();
final Looper looper = getLooper();
looper.setTraceTag(Trace.TRACE_TAG_SYSTEM_SERVER);
looper.setSlowLogThresholdMs(SLOW_DISPATCH_THRESHOLD_MS, SLOW_DELIVERY_THRESHOLD_MS);
mHandler = makeSharedHandler(looper);
mHandlerExecutor = new HandlerExecutor(mHandler);
}
向FgThread的Handler投递工作,目的地就是android.fg线程,而不是投递者当前所在的Binder线程。 setTraceTag与慢消息阈值为服务端观察执行耗时提供入口;共用线程上的任务仍会彼此排队。是否选择主线程、FgThread、专用ServiceThread或Executor,要看服务的线程约定,不能从"这个类是系统服务"直接推出执行线程。
执行结束后的 Message 处理也有版本差异
Looper在一轮正常分派结束后调用msg.recycleUnchecked。传统实现会清理字段并尝试回收到有限大小的对象池,DeliQueue则为并发访问保留必要状态,清掉业务引用。
来源:Message.java · void recycleUnchecked()。以下为该函数内的连续原文摘录。
java
void recycleUnchecked() {
if (MessageQueue.getUseConcurrent()) {
// Once a message has entered the queue, it may still be referenced by removing threads
// forever and may not be recycled. As such, its flags field must not be cleared.
clearReferenceFields();
// However, we should still mark this as in-use.
markInUse();
} else {
clear();
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
}
获取Message时也有对应分支:
来源:Message.java · public static Message obtain()。以下为该函数内的连续原文摘录。
java
public static Message obtain() {
if (!MessageQueue.getUseConcurrent()) {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
}
return new Message();
}
recycleUnchecked展示引用清理和回池条件,obtain展示取池中对象与创建新对象的两个出口。"Message.obtain一定从池中取"与"每次执行后一定回池"都不适用于全部实现。 应用仍优先使用obtain API,入队后不再修改或手动recycle同一个Message,也不要在回调返回后继续持有并复用它。
3.9 延迟 屏障与 IdleHandler 怎样影响执行顺序
延迟只决定最早时间 不会抢占当前任务
block模式在同一Looper上安排:100 ms后开始一个1100 ms的回调,300 ms后执行另一条消息。后者已到期时前者还没返回,因此它会被推迟约900 ms,再叠加调度和日志开销。
MainActivity.java · blocked(),示例完整方法。
java
private void blocked() {
Handler worker = newWorker("chapter3-block");
long start = SystemClock.uptimeMillis();
postAt(worker, "long", start + 100, 1100);
postAt(worker, "deadline", start + 300, 0);
postToMain(() -> mark("MAIN remains responsive"), 500);
}
这里的long和deadline是我们给两条任务起的名字,都是通过同一个worker Handler投递的Runnable。deadline只是任务名,表示我们想观察的到期任务;Handler没有因此为它开启特殊的截止时间调度。
回想前文postAt的四个参数:postAt(接收任务的Handler, 任务名, 到期时间, 回调内sleep时长)。以start为相对零点,上面三次安排分别是:
| 调用 | 由哪条线程执行回调 | 这次安排的含义 |
|---|---|---|
| postAt(worker, "long", start + 100, 1100) | worker | 到 +100 ms才可执行;进入回调后sleep 1100 ms |
| postAt(worker, "deadline", start + 300, 0) | 同一条worker | 到 +300 ms才可执行;自己的回调不主动sleep |
| postToMain(..., 500) | main | 通过主线程Handler延迟500 ms投递一条对照日志 |
start + 100是"什么时候到期",1100是"进入回调后停留多久",两者作用不同。workMillis具体用在postAt的lambda内,以下是该方法中的连续片段:
java
mark("RUN " + name + " due=" + due
+ " late=" + (SystemClock.uptimeMillis() - due));
if (workMillis > 0) SystemClock.sleep(workMillis);
mark("END " + name);
因此long的顺序是 打印RUN → sleep 1100 ms → 打印END → 回调返回 。deadline的workMillis为0,会跳过这次sleep。 
图中的时间是实验设计值。延迟可以来自当前回调耗时、等待锁、屏障或CPU调度;postDelayed不承诺精确计时,也不为每条消息启动一条定时线程。 需要跨休眠的任务调度时应先明确产品需求,再选择相应系统机制。
以下是2026-10-19独立block运行的历史节选:PID=4009,main TID=4009,worker TID=4026。uptime和due都是开机运行时间的毫秒值,不是时钟上的几点几分;这里保留绝对值,便于核对减法。
text
uptime=253453 tid=4009 POST deadline due=253753 accepted=true
uptime=253554 tid=4026 RUN long due=253553 late=1
uptime=253954 tid=4009 MAIN remains responsive
uptime=254660 tid=4026 END long
uptime=254660 tid=4026 RUN deadline due=253753 late=907
沿这五行读下来:
- POST deadline: 主线程在253453 ms完成提交,accepted=true;这只说明被队列接纳。deadline的due=253753,是未来的到期时刻。
- RUN long: worker在253554 ms开始long,随后执行前面的sleep。此时deadline还没到期。
- MAIN remains responsive: main在253954 ms执行了自己的回调;worker此时还没打印END long。它只证明这轮主线程的该回调能够执行,事件名不代表已做界面帧率或输入响应测量。
- END long: 到254660 ms,worker才执行到long的结束日志;随后回调返回。
- RUN deadline: 同一worker随后开始deadline,日志中uptime同样显示254660,说明它们落在同一个毫秒刻度内,执行顺序仍是long在前、deadline在后。
把日志放回前面的相对时间轴:deadline的due=253753,对照start + 300可还原这轮start=253453;实际开始的254660相当于start+1207,也就是比 +300晚了907 ms。
现在直接用日志中的两个时间算late:
text
实际开始时间 − 到期时间 = 开始执行晚了多久
254660 − 253753 = 907 ms
907 ms是deadline从到期到开始执行的等待时长,1100 ms是long请求的sleep时长。 deadline到期时,long已经执行了一段时间,所以这两个数字不应相等。按示意时刻算是1200 − 300 ≈ 900 ms;实际记录为907 ms,还包含sleep返回和线程调度等时间偏差。
当前回调沿Looper.loop → loopOnce → dispatchMessage → callback.run同步调用。外层必须等当前lambda、Handler分派和本次loopOnce返回,才能开始下一轮next;对应源码见3.8。
这里也要区分前面讲的两种等待:
| 线程停留的位置 | 正在等什么 | 结束等待后先做什么 |
|---|---|---|
| MessageQueue.next内的epoll_wait | fd事件或等待超时 | 继续检查队列,寻找可执行消息 |
| 本例long回调内的SystemClock.sleep | 回调自己请求的暂停时间 | 继续执行long的END及返回路径 |
两种等待都会暂停这条线程后续代码的执行,返回后的去向却不同。deadline到期时,本例worker停在第二种位置;消息到期或写入唤醒用的eventfd,都不会使它跳出当前回调、转去执行deadline。
同步屏障只挡住相应位置之后的同步消息
"同步消息"在这里指普通、未标记asynchronous的Message,不是说post会同步等待返回。屏障是一条特殊消息:没有业务Handler target,通过token管理移除。
DeliQueue先取得消息、标记使用并保存token,保留target为空;后续enqueueMessageUnchecked设置入队时间。先看屏障的建立:
来源:MessageQueue.java · private int postSyncBarrierDeliQueue()。以下为该函数内的连续原文摘录。
java
final Message msg = Message.obtain();
msg.markInUse();
msg.arg1 = token;
Legacy分支遇到队头屏障时向后找异步消息:
来源:MessageQueue.java · private Message nextLegacy()。以下为该函数内的连续原文摘录。
java
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
DeliQueue通过同步堆与异步堆选择,遇到屏障时仅把到期异步消息作为候选:
来源:MessageQueue.java · Message nextMessage(boolean peek, boolean returnEarliest)。以下为教学删节,注释标明省略位置;只展示本段讨论的分支。
java
if (isBarrier(msg)) {
if (asyncMsg != null && (returnEarliest || now >= asyncMsg.when)) {
found = asyncMsg;
} else {
syncBarrier = msg;
next = asyncMsg;
}
} // 笔记删节:省略没有屏障时的候选选择。
异步消息的能力是绕过同步屏障,实际执行线程仍是同一个Looper线程。 它不会打断一个尚未结束的同步或异步回调,也不保证早于所有普通消息。
| 待选顺序示意 | 是否可执行 | 原因 |
|---|---|---|
| 屏障之前的到期同步消息 | 可以 | 尚未被该屏障挡住 |
| 屏障之后的到期同步消息 | 等待屏障移除 | 当前选择不能越过屏障处理它 |
| 屏障之后的到期异步消息 | 可以被选出 | asynchronous标记使其不受该屏障限制 |
| 未来才到期的异步消息 | 仍要等到期 | 异步不取消时间条件 |
Framework在ViewRootImpl的遍历调度中使用屏障,再通过Choreographer安排相应工作;移除屏障后普通消息才能继续。这部分与第七章的绘制链相连。ViewRootImpl屏障入口
postSyncBarrier / removeSyncBarrier是隐藏接口,普通示例不通过反射注入。此处以源码和顺序推演解释机制;本轮没有对屏障做设备注入实验。屏障必须成对移除,错误保留可能让普通同步消息长期停滞。
IdleHandler 在当前没有待立即执行工作时运行
normal模式在worker队列添加一次性IdleHandler:
java
Looper.myQueue().addIdleHandler(() -> {
mark("IDLE");
return false;
});
这是完整示例中的连续代码。IdleHandler在队列所属线程执行,不是交给后台线程的空闲任务池。 它可以在队列为空,或者下一条消息还在未来时获得一次空闲处理机会。返回false移除,返回true保留等待以后符合条件的空闲阶段。
它不保证在任意时刻调用,也不适合长时间阻塞:IdleHandler耗时同样会推迟下一条消息。到期屏障挡住同步消息也不能简单理解成"队列没有任何事可做",具体分支有独立的idle判定。
FrontOfQueue 会改变正常时间排序
sendMessageAtFrontOfQueue使用特殊时间值0,DeliQueue再结合专门的头插序号处理。连续头插可能让原有工作长期排不到,应用不应把它当作通用提速方式。跨多个发送线程的同时间提交顺序要以实际提交的线性化与队列比较规则为准,不能用两个线程开始调用API的墙上时间推断严格先后。
3.10 文件描述符事件怎样回到 Java
同一 Native Looper 还能等待其他 fd
到这里见过的是"Java消息 + eventfd通知"。Native Looper也能把pipe、socket等fd注册进epoll;fd就绪后,在目标线程的Native pollOnce内执行对应回调,再通过JNI进入Java的文件事件监听。
fd模式使用公共API:在worker线程的MessageQueue注册pipe读端,500 ms后写入一个字节。监听对象和普通Message.callback是两种不同入口。
bash
"$chapter3_adb" -s "$chapter3_serial" shell am force-stop com.example.handlerdemo
"$chapter3_adb" -s "$chapter3_serial" shell am start -W \
-n com.example.handlerdemo/.MainActivity --es scenario fd
"$chapter3_adb" -s "$chapter3_serial" logcat -v threadtime -s 'HandlerDemo:I' '*:S'
预期FD_REGISTERED和FD_READ在chapter3-fd线程,FD_WRITE在main;FD_READ得到1字节、数值42。并发调度可能让FD_READ的日志早于写入函数返回后的FD_WRITE日志,这并不表示读到了尚未写入的数据。
这次要找三种事件:注册、写入、读取。以下来自2026-10-19的独立fd运行,PID=4098,main TID=4098,worker TID=4115:
text
uptime=265864 tid=4115 FD_REGISTERED
uptime=266366 tid=4115 FD_READ bytes=1 value=42
uptime=266367 tid=4098 FD_WRITE bytes=1
FD_REGISTERED表示worker执行到了监听注册之后;FD_READ里的bytes=1说明读到一个字节,value=42对上主线程写入的数据。它仍在worker TID=4115运行,却没有这次数据到来时显式worker.post一个读取任务。 回调入口从哪里来?
还有一个看似反常的地方:READ的日志比WRITE早1 ms。demo在Os.write返回后才mark写日志,接收方可能已先获得CPU并打印读取;先写入才可读的因果关系没有反转。不能按跨线程日志的显示先后推断系统调用的开始先后。
注册通过 JNI 进入 Native Looper.addFd
Java MessageQueue的更新函数把fd与事件集合交给nativeSetFileDescriptorEvents,再由NativeMessageQueue注册:
来源:android_os_MessageQueue.cpp · void NativeMessageQueue::setFileDescriptorEvents(。以下为该函数内的连续原文摘录。
cpp
mLooper->addFd(fd, Looper::POLL_CALLBACK, looperEvents,
sp<WeakLooperCallback>::make(this),
reinterpret_cast<void*>(events));
这里用POLL_CALLBACK和WeakLooperCallback,Native Looper保存请求并维护epoll注册。返回链和普通Java Message有一个关键区别:fd回调可在nativePollOnce尚未返回时执行。 
这是文件事件的机制示意;pipe的写入和读取机制属于Kernel文件子系统,并不把pipe描述成物理设备驱动。
Native Looper 怎样调用注册的回调
pollInner收集就绪事件后,在回调模式的响应处理中调用登记的LooperCallback:
来源:Looper.cpp · int Looper::pollInner(int timeoutMillis)。以下为该函数内的连续原文摘录。
cpp
int callbackResult = response.request.callback->handleEvent(fd, events, data);
if (callbackResult == 0) {
std::scoped_lock _l(mLock);
removeSequenceNumberLocked(response.seq);
}
这里先进入WeakLooperCallback,它保存的是NativeMessageQueue的弱引用。 实际交接在下面:
来源:android_os_MessageQueue.cpp · int NativeMessageQueue::WeakLooperCallback::handleEvent(int fd, int events, void* data)。以下为该函数内的连续原文摘录。
cpp
int NativeMessageQueue::WeakLooperCallback::handleEvent(int fd, int events, void* data) {
sp<LooperCallback> callback = mCallback.promote();
if (callback != nullptr) {
return callback->handleEvent(fd, events, data);
}
return 0;
}
promote成功取得本轮调用期间的强引用,再调用NativeMessageQueue.handleEvent;对象已不存在时返回0。Native Looper会移除返回0的对应注册,下面继续看有效对象怎样回到Java。
Native 回调调用 Java dispatchEvents
NativeMessageQueue::handleEvent把Native事件转换后,利用本轮pollOnce的上下文回到Java:
来源:android_os_MessageQueue.cpp · int NativeMessageQueue::handleEvent(。以下为该函数内的连续原文摘录。
cpp
int oldWatchedEvents = reinterpret_cast<intptr_t>(data);
int newWatchedEvents = mPollEnv->CallIntMethod(mPollObj,
gMessageQueueClassInfo.dispatchEvents, fd, events);
if (!newWatchedEvents) {
return 0; // unregister the fd
}
if (newWatchedEvents != oldWatchedEvents) {
setFileDescriptorEvents(fd, newWatchedEvents);
}
return 1;
这里的两个返回值不应混淆:Java listener返回下一轮关注的事件集合;Native handleEvent把0作为取消注册,把非零集合转换为保留注册的返回值1,集合改变时先更新关注事件。
Java dispatchEvents在检查当前注册记录后调用listener,实际回调不持有注册记录的那把锁:
来源:MessageQueue.java · private int dispatchEvents(int fd, int events)。以下为该函数内的连续原文摘录。
java
int newWatchedEvents = listener.onFileDescriptorEvents(
record.mDescriptor, events);
返回0表示不再监听该fd,非零表示接下来关注的事件集合。 返回0不会自动关闭应用持有的fd,关闭责任仍归拥有者。示例退出时先取消监听,再关闭pipe两端;真实程序还需处理非阻塞读写、一次事件中多份数据和错误事件。
fd listener同样运行在这条Looper线程上;如果它迟迟不返回,后面的Java消息也会延迟。把普通消息和fd事件放进同一个循环,是统一处理入口,并没有获得额外的并行执行线程。 
3.11 停止循环并处理消息与对象生命周期
quit 和 quitSafely 都不会中断正在执行的业务
HandlerThread的quit / quitSafely委托给对应Looper,再进入MessageQueue.quit。两者都是请求结束循环,已经进入Runnable的Java代码仍要自行返回。
| 行为 | quit | quitSafely |
|---|---|---|
| 正在执行的回调 | 不会被强制中断 | 不会被强制中断 |
| 等待中的普通已到期消息 | 丢弃 | 在正常分派条件允许时继续处理 |
| 尚未到期的消息 | 丢弃 | 丢弃,不等到未来时间 |
| 后续提交 | 队列拒绝 | 队列拒绝 |
| 同步屏障 | 不替应用修复屏障 | 也不自动让被屏障阻塞的同步消息变得可分派 |
DeliQueue用退出哨兵保存截止时间:立即退出记为0,安全退出记为当前uptime;目标线程随后清理相应节点。
来源:MessageQueue.java · void quit(boolean safe)。以下为该函数内的连续原文摘录。
java
long ts = safe ? SystemClock.uptimeMillis() : 0;
if (mStack.pushQuitting(ts)) {
if (incrementMptrRefs()) {
try {
nativeWake(mPtr);
} finally {
decrementMptrRefs();
}
}
}
该片段保留Native指针引用计数的取得与finally释放。Legacy则在锁内设置mQuitting,并选择移除全部消息或未来消息,最后nativeWake,让正在等待的线程重新检查退出状态。
quitSafely不保证所有成功提交的任务都执行。 Future消息会被丢弃,与退出并发的提交也不能靠返回true获得业务完成保证。本基线的DeliQueue.quit还明确保留并发时间戳边界的TODO,应依据自己的完成回调或任务协议判断完成。
quit 模式怎样建立可比较的条件
当前任务先进入最多等待5秒的门闩,控制线程确认它已开始后,再提交已到期任务与未来任务,最后请求退出并释放门闩。超时或中断必须打印INVALID并将该轮判为无效;GATE_END的released表示是否确由门闩释放,不能只凭最终执行集合判断退出策略。
2026-10-21补齐门闩有效性检查后的示例尚未重跑设备实验,需按下述无超时判据重新验收。历史quit日志没有released字段,其QUIT → GATE_END顺序已核对,但不能代替修订版验收。
bash
"$chapter3_adb" -s "$chapter3_serial" shell am force-stop com.example.handlerdemo
"$chapter3_adb" -s "$chapter3_serial" shell am start -W \
-n com.example.handlerdemo/.MainActivity --es scenario quit
"$chapter3_adb" -s "$chapter3_serial" logcat -v threadtime -s 'HandlerDemo:I' '*:S'
先分别核对每组GATE → QUIT requested=true、afterAccepted=false → GATE_END released=true;due的POST须accepted=true,且没有INVALID或ERROR。之后才检查safe_due在其QUIT后执行,immediate_due、safe_future、immediate_future均不执行,两组STOPPED alive=false。独立线程的日志可以交错,不要求两组全局固定先后。控制线程发现门闩失效会中止,提前中止仍请求线程退出;检查之后才发生的超时也必须凭INVALID排除。
主Looper不允许退出。应用自己的线程需要明确结束策略:移除不再需要的工作、取消监听、关闭持有资源,再请求退出。需要等待线程结束时,在合适的工作线程join,避免主线程等待一个仍需主线程协作的任务造成死锁。
先看2026-10-20历史日志中safe这一组(PID=2832;控制线程TID=2852、worker TID=2851)。这份历史代码尚未增加released字段,不能当作当前门闩修订版的设备验收:
text
uptime=374008 tid=2851 GATE safe
uptime=374008 tid=2852 POST safe_due due=374008 accepted=true
uptime=374008 tid=2852 POST safe_future due=379008 accepted=true
uptime=374013 tid=2852 QUIT safe requested=true afterAccepted=false
uptime=374013 tid=2851 GATE_END safe
uptime=374013 tid=2851 RUN safe_due due=374008 late=5
uptime=374014 tid=2852 STOPPED safe alive=false
POST两条都accepted=true,但只有safe_due执行;QUIT的requested=true表示HandlerThread此次退出请求返回true,afterAccepted=false表示紧接着试投的新任务被拒绝,不是说退出前提交的future已经运行。GATE_END在QUIT之后,说明这次先请求退出,再让当前回调返回;STOPPED alive=false则由控制线程join后读取,确认当时该worker已停止。
同轮immediate组的GATE_END后没有RUN immediate_due,两组future都未执行。这是为什么"队列接受"不能等同"以后一定执行"的直接例子;缺少RUN还要结合整轮日志、退出顺序与STOPPED,不能只截一段日志便认定任务已丢弃。
移除消息不能取消已经开始的回调
removeCallbacks要传原来那个Runnable对象,重新创建一个内容相同的lambda并不是同一引用。removeCallbacksAndMessages(null)会移除这个Handler的待处理工作,不是把所有共用Looper的Handler一起清空。
已经开始执行的任务需要自行检查取消状态或使用协作式取消。MessageQueue、Message、Runnable、Handler.Callback、obj等都可能持有Activity或其他对象引用;超长延迟、忘记停止线程和忘记移除监听会延长它们的生命周期。静态Handler只能改变一部分引用关系,并不能替代整体的取消与清理。
清理必须同时阻止后续投递
只调用一次removeCallbacksAndMessages不能阻止其他线程随后重新post。示例通过生命周期锁把"检查destroyed并投递"与"设置destroyed并移除"串行化;所有面向主线程的业务投递都经过postToMain。已接受回调还会在执行前检查状态,回调执行与onDestroy同属主线程,二者不会同时运行。
先关闭新投递入口,再移除待处理任务;正在执行的工作允许自行返回,资源清理排在资源所属线程上。 fd模式把取消监听、关闭pipe的清理任务投到同一worker,再请求quitSafely,避免主线程直接关闭一个正在被worker使用的fd。这里的顺序依赖示例对该线程退出的统一管理;扩展程序时还需处理清理任务提交失败等情况。
用下面的实验放大"worker已开始注册,但尚未向主线程投递写入"的窗口:
bash
"$chapter3_adb" -s "$chapter3_serial" shell am force-stop com.example.handlerdemo
"$chapter3_adb" -s "$chapter3_serial" shell am start -W \
-n com.example.handlerdemo/.MainActivity --es scenario fd \
--ei fd_registration_pause_ms 1200 --ei finish_after_ms 300
"$chapter3_adb" -s "$chapter3_serial" logcat -v threadtime -s 'HandlerDemo:I' '*:S'
这两个extra只用于实验:worker在注册后暂停,Activity先自动finish。finish请求与onDestroy之间可能存在退出动画和生命周期调度延迟,必须按实际日志确认顺序。有效的一轮应先出现FD_REGISTERED,再出现DESTROY,之后FD_WRITE_SCHEDULED accepted=false、FD_CLEANED,且没有FD_WRITE / FD_WRITE_EXCEPTION。若设备调度没有形成这个顺序,不能把该轮当作竞态窗口已经覆盖。比较注册前退出时,把fd_registration_pause_ms换成fd_registration_delay_ms,让注册任务延迟入队;比较等待写入或读完后的退出时,去掉注册暂停,并调整finish_after_ms。各窗口都以FD_REGISTERED、FD_READ、DESTROY的实际顺序判断,不能只凭传入的毫秒数认定覆盖。
在2026-10-20形成该窗口的一轮中,PID=3060,main TID=3060、worker TID=3077;从原日志保留四个事件:
text
uptime=388684 tid=3077 FD_REGISTERED
uptime=389086 tid=3060 DESTROY
uptime=389885 tid=3077 FD_WRITE_SCHEDULED accepted=false
uptime=389885 tid=3077 FD_CLEANED
DESTROY说明onDestroy已设置destroyed、移除待处理工作并请求收尾;之后worker尝试安排延迟写入,postToMain发现destroyed而返回false,所以FD_WRITE_SCHEDULED accepted=false。这里拒绝的是应用生命周期入口,不是Handler队列因为quit才拒绝提交。 随后worker执行fdCleanup,移除监听并关闭pipe,才打印FD_CLEANED。
这和quit实验的afterAccepted=false虽然都是false,发生位置与原因不同。3.3完整MainActivity中postToMain的状态检查和onDestroy的相同lifecycleLock,正是阅读这个窗口时应对照的两段;资源清理本体在fileDescriptor的fdCleanup里。
quit 怎样接到 Native 对象释放
quit是退出请求,最终由目标线程的next发现退出并执行dispose,然后返回null,使loopOnce返回false、loop正常结束。dispose清除Java持有的Native地址:
来源:MessageQueue.java · private void dispose()。以下为该函数内的连续原文摘录。
java
private void dispose() {
if (mPtr != 0) {
nativeDestroy(mPtr);
mPtr = 0;
}
}
JNI的nativeDestroy释放nativeInit时取得的强引用:
来源:android_os_MessageQueue.cpp · static void android_os_MessageQueue_nativeDestroy(。以下为该函数内的连续原文摘录。
cpp
static void android_os_MessageQueue_nativeDestroy(JNIEnv* env, jclass clazz, jlong ptr) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->decStrong(env);
}
mPtr置零表示Java MessageQueue不再通过这个地址访问NativeMessageQueue。 NativeMessageQueue内部持有的sp<Looper>会随其析构释放,但Native Looper还有另一条持有关系:线程局部变量。
来源:Looper.cpp · void Looper::setForThread(const sp<Looper>& looper)。以下为该函数内的连续原文摘录。
cpp
void Looper::setForThread(const sp<Looper>& looper) {
gThreadLocalLooper = looper;
}
本文件的gThreadLocalLooper声明为thread_local sp<Looper>。通常在线程退出、线程局部强引用释放后,Native Looper才有机会失去最后一个强引用;若还有其他持有者,还要等它们释放。其mEpollFd、mWakeEventFd是unique_fd成员,随对象销毁关闭。因此不能把quit、nativeDestroy、线程退出与fd关闭当成同一个时刻。Native Looper字段声明 
图展示示例正常退出的生命周期;Java对象何时被GC、其他Native持有者何时释放,是另外的持有关系,不能单凭loop返回判断。
回调抛出异常后还会继续循环吗
普通Message的业务回调若抛出未捕获异常,loopOnce的异常分支会记录observer通知并继续抛出:
来源:Looper.java · private static boolean loopOnce(。以下为教学删节,注释标明省略位置;只展示本段讨论的分支。
java
// 笔记删节:只展示 catch 分支,省略其后的 finally。
catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
}
Looper不会自动吞掉业务异常并处理下一条消息。 异常会沿loopOnce、loop、HandlerThread.run向外传播;Error也不会被这个catch(Exception) 吞掉。线程若因此终止,队列中尚未执行的工作就失去消费者。
在普通Android应用的默认未捕获异常处理下,即使异常发生在工作线程,也会报告崩溃并终止应用进程;不能承诺只结束这个HandlerThread。业务上可以恢复的错误应在明确的任务边界处理,回调完成与失败通过自己的协议通知,不用post的true充当成功结果。RuntimeInit默认异常处理
用症状定位问题
3.12 用日志和 Perfetto 验证机制
本节的设备数据与APK身份对应2026-10-19、2026-10-20的历史实验版本;2026-10-21的quit门闩修订未重跑设备实验。下文将操作预期与历史观察分开,不把旧日志当作修订版的运行结果。
先按可观察判据运行四组实验
3.3、3.10、3.11已给出normal、fd、quit的运行指令。block的命令只需改变scenario:
bash
"$chapter3_adb" -s "$chapter3_serial" shell am force-stop com.example.handlerdemo
"$chapter3_adb" -s "$chapter3_serial" shell am start -W \
-n com.example.handlerdemo/.MainActivity --es scenario block
"$chapter3_adb" -s "$chapter3_serial" logcat -v threadtime -s 'HandlerDemo:I' '*:S'
重点看RUN deadline的late字段,再看MAIN remains responsive的TID。若主线程日志也晚很多,先检查设备负载和启动阶段,不把每一轮波动都归因于目标消息队列。
在可调试设备上对照 DeliQueue
这一步只用于本基线的可调试示例和支持相应变更的系统。先观察默认normal日志中的QUEUE实现名称,再开启该包的兼容性变更,重新启动:
bash
"$chapter3_adb" -s "$chapter3_serial" shell am compat enable \
421623328 com.example.handlerdemo
"$chapter3_adb" -s "$chapter3_serial" shell am force-stop com.example.handlerdemo
"$chapter3_adb" -s "$chapter3_serial" shell am start -W \
-n com.example.handlerdemo/.MainActivity --es scenario normal
"$chapter3_adb" -s "$chapter3_serial" logcat -v threadtime -s 'HandlerDemo:I' '*:S'
变更编号来自本版MessageQueue的USE_NEW_MESSAGEQUEUE。预期支持该分支的设备在dump中显示DeliQueue,normal的分派与顺序仍成立。命令被拒绝、变更不存在或日志未切换时,保留原结果并检查版本,不把"命令写了enable"当成分支确实变化。
完成后恢复该包的默认兼容性策略:
bash
"$chapter3_adb" -s "$chapter3_serial" shell am compat reset \
421623328 com.example.handlerdemo
"$chapter3_adb" -s "$chapter3_serial" shell am force-stop com.example.handlerdemo
reset移除本次覆盖,重新由系统默认策略决定;它不是强行永久禁用DeliQueue。不要修改共享设备上的全局flag来完成这个包级实验。
采集有自定义区间的 Trace
示例用公开Trace.beginSection标记long、deadline、earlier、future与fdCallback。配套配置开启应用Trace与sched事件,采集十秒。下面通过标准输入传配置,避免Perfetto进程无法读取 /data/local/tmp下配置文件的权限问题。
bash
chapter3_trace=/data/misc/perfetto-traces/chapter3-handler.perfetto-trace
"$chapter3_adb" -s "$chapter3_serial" shell perfetto --txt -c - \
-o "$chapter3_trace" \
< "$chapter3_notes/示例/追踪配置/第三章Handler.pbtxt" \
> "$chapter3_run/perfetto.log" 2>&1 &
chapter3_trace_pid=$!
sleep 1
"$chapter3_adb" -s "$chapter3_serial" shell am force-stop com.example.handlerdemo
"$chapter3_adb" -s "$chapter3_serial" shell am start -W \
-n com.example.handlerdemo/.MainActivity --es scenario block
wait "$chapter3_trace_pid"
"$chapter3_adb" -s "$chapter3_serial" pull "$chapter3_trace" \
"$chapter3_run/chapter3-handler.perfetto-trace"
预期采集命令成功退出并得到非空Trace。失败时先读perfetto.log,检查设备是否提供相应数据源、应用是否为可调试版本,以及远端输出目录是否允许使用。配置中的应用名必须与manifest包名一致。Perfetto命令行采集说明r3
用Perfetto UI打开文件,找到进程与chapter3-block线程:
- 先看Chapter3/long的起止区间,确认deadline没有插进尚未结束的long中。
- 再看同一线程的调度轨道,区分回调区间、实际在CPU上Running、处于等待和等待CPU的时间。
- 对照日志中的due与late,解释"到期"和"开始执行"的区别。
Trace区间持续1100 ms不等于占用CPU 1100 ms。 本示例的慢回调用sleep,区间内大部分时间可以是等待;消息循环被占住与CPU忙碌是不同维度。
如需命令行查询,工程的Perfetto工具可解析相同文件,首次使用可能下载官方预编译解析器:
bash
python3 "$chapter3_top/external/perfetto/tools/trace_processor" \
"$chapter3_run/chapter3-handler.perfetto-trace" \
-q "$chapter3_notes/示例/追踪配置/提取消息区间.sql"
怎样验证等待中的线程被这次提交唤醒
仅看到earlier比future先执行,不能证明提交时worker正在睡眠。复用上面的采集步骤,把scenario改为normal,并把远端与本地Trace文件名都改为chapter3-normal.perfetto-trace。示例在主线程投递earlier的外围标记Chapter3/sendEarlier,便于缩小关联范围。
打开这份Trace后,依次检查:worker在提交前是否处于Sleeping;提交区间内是否有主线程唤醒该worker的sched_waking;worker随后何时获得CPU、何时进入Chapter3/earlier。没有记录到这些事件时,保留"本轮不足以确认"的结论,不能用消息顺序代替等待证据。
配套SQL同时检查线程、进程、唤醒者、提交区间和前一段睡眠状态:
bash
python3 "$chapter3_top/external/perfetto/tools/trace_processor" \
"$chapter3_run/chapter3-normal.perfetto-trace" \
-q "$chapter3_notes/示例/追踪配置/提取唤醒调度.sql"
2026-10-20的补充采集中,主线程TID=3206、worker TID=3222;worker唤醒前连续睡眠389.764 ms,提交区间内由主线程唤醒,等待CPU约23.028 μs,唤醒到进入earlier回调约107.246 μs。查询结果与复现记录
这里关联的是等待、唤醒者与执行时机。 调度事件没有直接展示eventfd_write或ep_poll_callback的函数栈;这些具体函数仍依据固定源码解释,不把调度相关性扩大成逐函数实测。
本轮设备观察与证据
2026-10-19使用独立userdata的SDK Emulator执行示例。默认分支、DeliQueue对照、延迟、退出和fd回调的具体日志与Trace分析保存在第三章实测记录。下面只摘出该轮支撑理解的结论。2026-10-20修订示例后的回归另见补充实测,包含四种退出窗口与DeliQueue对照:
| 场景 | 本轮实际观察 |
|---|---|
| 默认队列 | dump显示Legacy;normal的PID为3954,主线程TID=3954,worker TID=3970 |
| normal顺序 | earlier比future先执行;两者分别比自己的due晚3 ms、1 ms;结果随后回到主线程 |
| 分派优先级 | RUNNABLE独立执行;what=10被Callback消费;what=20继续进入handleMessage |
| block | deadline比due晚907 ms;主线程在long尚未结束时打印响应日志 |
| DeliQueue对照 | 开启包级变更后dump显示DeliQueue;分派、earlier顺序与主线程回传仍成立;block的deadline晚902 ms;已reset恢复 |
| quit对照 | 只有safe_due执行;两组未来任务与immediate_due均未执行;退出后提交被拒绝,两个工作线程都停止 |
| fd回调 | PID=4098,监听回调在TID=4115读取1字节、数值42;写入日志在主线程 |
| Trace | PID=4009、TID=4026;long区间1106.260 ms,deadline区间0.021 ms,后者在long结束之后 |
这些结果验证本次示例在该设备上的可观察行为。用户空间日志和应用Trace没有逐条证明eventfd_write、ep_poll_callback等Kernel函数的实际执行;该部分依据匹配版本源码阅读。屏障注入、多发送者高压力、深度睡眠和异常退出等场景不在本轮设备实验范围。 
图根据本轮Trace的slice与thread_state查询重绘,不是Perfetto UI截图;横轴以long区间开始为0,底部数字是同一区间内的调度状态累计值。deadline的区间很短,图中用竖线标出位置,精确耗时见实测记录。SVG版本
完成后可停止应用,保留实验材料;不再需要示例时卸载:
bash
"$chapter3_adb" -s "$chapter3_serial" shell am force-stop com.example.handlerdemo
"$chapter3_adb" -s "$chapter3_serial" uninstall com.example.handlerdemo
3.13 回顾 自测与源码速查
用一条消息重新串起来
发送线程通过Handler选择目标Looper,把Runnable放进Message.callback,设定target与uptime时间。选定的MessageQueue实现发布消息并协调等待状态,必要时由nativeWake写eventfd。Kernel把可读事件交给epoll,唤醒等待任务,调度器给目标线程执行机会。Native pollOnce返回后,Java next再选择可执行消息;Looper根据target进入Handler分派,最后执行Runnable或handleMessage。
目标线程一次处理一个回调。长任务、屏障、退出、调度和休眠都可能改变可观察时间。文件描述符监听还有独立的回调入口,它可在Native pollOnce内通过JNI进入Java;消息内容和唤醒通知分别由不同结构承载。理解这几条边界,比背出一串函数名更能解释实际问题。