Looper线程绑定
本文接着消息机制总览、Handler构造阅读。前文已经说明 Handler 会保存一个 Looper;这里反过来追踪这个 Looper 是怎样绑定到线程的。核心问题不是“ThreadLocal 是什么”,而是四个动作如何配合:prepare() 创建当前线程的 Looper,myLooper() 查询绑定结果,prepareMainLooper() 额外登记进程级主 Looper,MessageQueue 再据此决定是否允许退出。
1. 绑定约束
源码文件:frameworks/base/core/java/android/os/Looper.java
相关字段:sThreadLocal、sMainLooper
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static Looper sMainLooper; // guarded by Looper.class这两个字段解决的是不同范围的问题。sThreadLocal 的读取范围是“当前线程”,所以两个线程可以各自保存一个 Looper;sMainLooper 是进程级静态引用,所有线程查询到的是同一个主 Looper。主引用的读写都放在 synchronized (Looper.class) 中,不能用它替代线程绑定查询。
2. 准备流程
源码文件:frameworks/base/core/java/android/os/Looper.java
相关函数:prepare()、prepare(boolean)、Looper(boolean)
public static void prepare() {
prepare(true);
}
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));
}
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}调用线程先读自己的 ThreadLocal。已有值时立即失败,因此“每线程一个 Looper”是显式的运行时约束,而不是调用者自觉遵守的约定。没有值时,构造函数把队列和当前 Thread 一起保存,再把新对象写回当前线程的 ThreadLocal。prepare() 不能为另一个线程预先绑定 Looper;要绑定哪个线程,取决于哪条线程执行了这段代码。
3. 主 Looper
源码文件:frameworks/base/core/java/android/os/Looper.java
相关函数:prepareMainLooper()、getMainLooper()
@Deprecated
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
public static Looper getMainLooper() {
synchronized (Looper.class) {
return sMainLooper;
}
}主 Looper 先走普通的线程绑定,再把同一个对象登记到静态字段。prepare(false) 的 false 不是“不能处理消息”,而是把退出权限传给队列。重复登记主 Looper 即使发生在不同线程,也会因为共享的 sMainLooper 被拒绝。该方法已标记为废弃,源码注释说明应用主 Looper 通常由 Android 环境创建。
应用进程的入口可以直接看到这条顺序:
源码文件:frameworks/base/core/java/android/app/ActivityThread.java
相关函数:ActivityThread.main()
Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");这里的关键不是 ActivityThread 的全部初始化,而是它在同一线程上先建立主 Looper,再把该线程交给持续循环。loop() 意外返回后抛异常,说明正常应用主线程不会把退出当作普通完成路径。
4. 查询入口
源码文件:frameworks/base/core/java/android/os/Looper.java
相关函数:myLooper()、myQueue()、getThread()、getQueue()
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
public static @NonNull MessageQueue myQueue() {
return myLooper().mQueue;
}
public @NonNull Thread getThread() {
return mThread;
}
public @NonNull MessageQueue getQueue() {
return mQueue;
}myLooper() 未准备时返回 null,适合显式判断;myQueue() 没有同样的空值保护,未准备线程调用它会在取 mQueue 时失败。getThread() 是对象保存的 owner,而不是调用查询方法的线程;因此把一个 Looper 传给别的线程后,仍可通过它观察原绑定线程,但不能据此改变 ThreadLocal 归属。
5. 退出权限
源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
相关函数:构造函数、quit(boolean)
MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
mLooperThread = Thread.currentThread();
}
void quit(boolean safe) {
if (!mQuitAllowed) {
throw new IllegalStateException("Main thread not allowed to quit.");
}
synchronized (this) {
if (mQuitting) {
return;
}
mQuitting = true;
if (safe) {
removeAllFutureMessagesLocked();
} else {
removeAllMessagesLocked();
}
nativeWake(mPtr);
}
}普通 prepare() 创建的队列允许 quit() 或 quitSafely();主队列由 prepareMainLooper() 传入 false,所以退出调用在清理前就抛出异常。对允许退出的队列,重复退出返回,不会再次清理;safe 只影响保留到期消息的清理策略,不能把主队列变成可退出队列。
6. 测试替换
源码文件:frameworks/base/core/java/android/os/Looper.java
相关函数:setMainLooperForTest()、clearMainLooperForTest()
public static void setMainLooperForTest(@NonNull Looper looper) {
synchronized (Looper.class) {
sMainLooper = Objects.requireNonNull(looper);
}
}
public static void clearMainLooperForTest() {
synchronized (Looper.class) {
sMainLooper = null;
}
}这两个隐藏 API 只替换进程级 sMainLooper,不会写入调用线程的 sThreadLocal。因此测试可以让 getMainLooper() 指向受控对象,但“当前线程是谁的 Looper”仍由该线程自己的绑定决定。传入 null 会被 requireNonNull 拒绝;清理后主 Looper 又变为未定义状态。
测试框架通过 TestableLooper 持有目标 Looper,并显式处理队列中的消息:
源码文件:frameworks/base/tests/testables/src/android/testing/TestableLooper.java
相关函数:构造函数、runWithLooper()
public TestableLooper(Looper l) throws Exception {
this(acquireLooperManager(l), l);
}
public void runWithLooper(RunnableWithException runnable) throws Exception {
new Handler(getLooper()).post(() -> {
try {
runnable.run();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
processAllMessages();
}它不是重新实现 sThreadLocal,而是把 Runnable 投递到目标 Looper,再主动处理消息。这样测试可以验证线程归属和执行顺序,同时避免等待真实时钟或真实主循环。
7. 失败分支
可以把失败点按发生时机分成三类:
| 时机 | 输入 | 结果 |
|---|---|---|
| 准备前 | 同一线程再次 prepare() | 抛 RuntimeException,旧 Looper 保留 |
| 主 Looper 登记 | sMainLooper 已非空 | 抛 IllegalStateException |
| 查询前 | 未 prepare() 就调用 myQueue() | 空引用访问失败,不能得到队列 |
| 退出 | 主队列调用 quit() | 抛 IllegalStateException,不进入清理 |
这些分支不会互相替代:重复 prepare() 检查的是当前线程,重复主登记检查的是进程级静态字段;一个线程可以没有自己的 Looper,却仍然能够读取非空的 getMainLooper()。
8. 动手验证
先在源码树定位入口:
rg -n "prepare\(|prepareMainLooper|getMainLooper|myQueue|quit\(boolean" \
frameworks/base/core/java/android/os/Looper.java \
frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java再用下面的最小实验观察绑定和失败边界:
Thread worker = new Thread(() -> {
Looper.prepare();
Looper first = Looper.myLooper();
System.out.println(first.getThread() == Thread.currentThread());
try {
Looper.prepare();
} catch (RuntimeException expected) {
System.out.println(expected.getMessage());
}
first.quitSafely();
});
worker.start();
worker.join();预期现象是第一行输出 true,第二次准备打印“Only one Looper may be created per thread”,随后普通队列允许安全退出。这个实验没有覆盖主线程初始化、native poll 的阻塞时序或测试框架的全部规则;要看主 Looper 的线程关系,可继续阅读 ActivityThread.main() 和 TestableLooperTest.testMainLooperAnnotation()。
9. 边界说明
ThreadLocal 在这里承担的是 Looper 到当前线程的索引,不等于消息循环本身;真正的消息存储和退出清理在 MessageQueue。getMainLooper() 也不保证调用者就在主线程,判断执行线程应比较 Looper.myLooper() 与目标 Looper。最后,setMainLooperForTest() 改的是全局查询结果,不能用来把任意工作线程自动变成主线程。
