Skip to content

Looper线程绑定

从 Looper.prepare 追踪线程绑定、主 Looper 注册、队列退出权限与测试替换。

基于android-17.0.0_r1
AndroidLooperThreadLocalMessageQueue源码阅读

Looper线程绑定 ​

本文接着消息机制总览、Handler构造阅读。前文已经说明 Handler 会保存一个 Looper;这里反过来追踪这个 Looper 是怎样绑定到线程的。核心问题不是“ThreadLocal 是什么”,而是四个动作如何配合:prepare() 创建当前线程的 Looper,myLooper() 查询绑定结果,prepareMainLooper() 额外登记进程级主 Looper,MessageQueue 再据此决定是否允许退出。

1. 绑定约束 ​

源码文件:frameworks/base/core/java/android/os/Looper.java

相关字段:sThreadLocal、sMainLooper

java
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)

java
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()

java
@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()

java
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()

java
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)

java
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()

java
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()

java
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. 动手验证 ​

先在源码树定位入口:

bash
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

再用下面的最小实验观察绑定和失败边界:

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() 改的是全局查询结果,不能用来把任意工作线程自动变成主线程。