文章内容
本文根据视频《为何主线程可以new Handler,子线程中如何new Handler》整理。
一、为什么换一个线程,new Handler 就报错?
在主线程的 Activity.onCreate() 中,下面这行代码通常可以正常执行:
Handler handler = new Handler();把它放到一个普通子线程里,就可能得到这样的异常:
new Thread(() -> { Handler handler = new Handler();}).start();java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()关键在于无参构造会查找当前线程的 Looper。Android 框架已经为应用主线程准备好了 Looper,而普通 Thread 启动时不会自动创建它。
这里要分清两个条件:创建 Handler 需要有可绑定的 Looper;通过消息队列执行回调,还需要这个 Looper 进入消息循环。 只调用 prepare(),Handler 就能创建,但消息不会因此自动得到处理。
二、先明确 Handler、Looper 和线程的关系

Handler 机制可以沿着一条消息的路径理解:
- 调用
Handler.sendMessage()或post(),将消息加入 Handler 关联的队列。 - Looper 在自己的线程上从队列中取出可执行的消息。
- Looper 调用
msg.target.dispatchMessage(msg),把消息交回目标 Handler。 - Handler 根据消息内容和回调配置,执行 Runnable、
Handler.Callback或handleMessage()。
这几个对象的职责不同:
| 对象 | 职责 |
|---|---|
| Message | 携带消息数据、目标 Handler,以及调度时间等信息 |
| MessageQueue | 保存待处理消息,决定何时取出可执行的消息 |
| Looper | 在所属线程中持续取消息、分发消息 |
| Handler | 提供消息投递入口,并决定消息交给哪个回调处理 |
在本文版本中,MessageQueue 使用链表组织消息,普通定时消息主要按 when 排序;队首插入、同步屏障等机制会影响实际取出顺序,不能把它简单理解成严格的先进先出队列。
一个线程最多准备一个 Looper,每个 Looper 持有一条 MessageQueue。多个 Handler 可以绑定同一个 Looper,共享队列和执行线程。Handler 不会因为实例数量增加而创建更多线程。

通过消息队列分发时,决定回调线程的是 Handler 绑定的 Looper,而不是创建 Handler 或调用 sendMessage() 的线程。入队可以发生在其他线程,出队和回调则由 Looper 所属线程完成。
三、Handler 与 Looper 绑定
无参构造最终会走到下面这个构造方法。省略无关检查后,核心逻辑如下:
public Handler(Callback callback, boolean async) { mLooper = Looper.myLooper(); if (mLooper == null) { throw new RuntimeException( "Can't create handler inside thread that has not called Looper.prepare()"); } mQueue = mLooper.mQueue; mCallback = callback; mAsynchronous = async;}它先通过 Looper.myLooper() 取得当前线程的 Looper,再保存该 Looper 和它的队列。这里没有创建线程、创建队列或启动循环的操作。
因此,失败的直接原因是 myLooper() 返回了 null。这个限制适用于隐式使用当前线程 Looper 的构造方式,包括无参构造和只传 Callback 的构造,并非只有无参构造才会遇到。
显式传入 Looper 则走另一条路径:
public Handler(Looper looper, Callback callback, boolean async) { mLooper = looper; mQueue = looper.mQueue; mCallback = callback; mAsynchronous = async;}这时 Handler 使用传入对象的队列,不再查询创建线程的 ThreadLocal。例如,在普通子线程中执行 new Handler(Looper.getMainLooper()),创建的是一个绑定应用主线程的 Handler,子线程自身不需要调用 prepare()。
对应源码:Android 8.1 Handler.java。
四、prepare:为当前线程创建 Looper

Looper.myLooper() 读取的是线程本地存储:
public static @Nullable Looper myLooper() { return sThreadLocal.get();}虽然 sThreadLocal 是静态字段,但不同线程通过它访问的是各自的值。主线程已经保存了 Looper,不代表子线程也能通过 myLooper() 取得它。
prepare() 负责建立这份绑定:
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));}Looper 构造时还会创建 MessageQueue,并记录当前线程:
private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread();}所以,谁执行 prepare(),新 Looper 就属于谁。要给子线程准备 Looper,必须在子线程的 run() 内调用;在 start() 之前由主线程调用,无法替子线程完成初始化,还会因主线程已存在 Looper 而触发重复准备异常。
同一线程重复调用 prepare() 会抛异常。退出 Looper 也不会清除这里的绑定,不能在同一个线程上重新 prepare() 来重启循环。
主线程的初始化入口多做了两件事:
public static void prepareMainLooper() { prepare(false); synchronized (Looper.class) { if (sMainLooper != null) { throw new IllegalStateException("The main Looper has already been prepared."); } sMainLooper = myLooper(); }}首先,prepare(false) 创建不允许主动退出的消息队列;其次,把当前 Looper 保存到静态字段 sMainLooper。getMainLooper() 通过同步访问读取这个字段,因此应用内其他线程也能取得主线程 Looper。
myLooper() 回答“当前线程绑定了谁”,getMainLooper() 回答“应用主线程绑定了谁”。这两个方法的区别,是理解跨线程创建 Handler 的关键。
五、loop:让消息真正开始执行
prepare() 完成初始化,loop() 才负责处理消息。下面是消息循环的简化片段,省略了日志、身份检查和消息回收等逻辑:
public static void loop() { final Looper me = myLooper(); if (me == null) { throw new RuntimeException( "No Looper; Looper.prepare() wasn't called on this thread."); } final MessageQueue queue = me.mQueue; for (;;) { Message msg = queue.next(); // 可能阻塞 if (msg == null) { return; } msg.target.dispatchMessage(msg); }}loop() 不会启动新线程。调用它的线程会留在这个循环里,不断取消息并执行回调;正常退出后,才会继续执行 loop() 后面的代码。未捕获的回调异常也可能使循环异常终止,不能把“调用过 loop”当成永久存活的保证。
dispatchMessage() 也不一定调用 handleMessage():通过 post() 提交的 Runnable 会走消息自身的回调;否则先尝试构造时传入的 Handler.Callback,未被处理的消息才交给 handleMessage()。
循环不会仅仅因为写成 for (;;) 就持续空转。当没有可执行消息时,MessageQueue.next() 会进入等待,消息入队或等待时间到达等事件可使其继续检查。准确说法是空闲等待不会忙等消耗 CPU;消息密集或回调耗时时,线程依然会消耗 CPU。
消息的执行时间也不是实时保证。即使延迟时间已到,只要前一个回调尚未返回,后面的消息仍需等待。
对应源码:Looper.java、MessageQueue.java。
六、主线程的Looper初始化

应用进程的 Java 层启动链最终进入 ActivityThread.main()。省略其他初始化步骤后,可以看到这个顺序:
public static void main(String[] args) { Looper.prepareMainLooper(); ActivityThread thread = new ActivityThread(); thread.attach(false); if (sMainThreadHandler == null) { sMainThreadHandler = thread.getHandler(); } Looper.loop(); throw new RuntimeException("Main thread loop unexpectedly exited");}框架先准备主线程 Looper,再创建 ActivityThread 并完成关联,最后进入消息循环。ActivityThread 内部的 Handler H 也就可以绑定到已经准备好的 Looper。
当正常启动流程执行到 Application.onCreate() 或 Activity.onCreate() 时,主线程 Looper 已经存在,所以这些回调里的无参 new Handler() 能够成功。应用无需也不应再手动初始化主线程 Looper。
H 负责处理应用进程中的一部分框架消息。在 Android 8.1 中,可以看到 LAUNCH_ACTIVITY、PAUSE_ACTIVITY、RESUME_ACTIVITY、BIND_APPLICATION 等分支。系统经 Binder 将请求送到应用进程,应用端的 ApplicationThread 再把相关工作转交主线程,最终进入对应的生命周期处理逻辑。
不要把 H 理解为主线程上唯一的 Handler:业务 Handler、界面相关组件的 Handler 也可以共享主线程队列。主线程上的事件处理还涉及 native 回调等路径,不能把所有主线程代码都归结为 H.handleMessage()。
这也解释了长任务为什么会阻塞界面:当前回调占用主线程时,后续依赖主线程的消息和界面工作无法及时推进。问题在于某一次处理迟迟不结束,而不是消息循环本身持续存在。
对应源码:Android 8.1 ActivityThread.java。生命周期调度的具体方法和消息类型应按目标 Android 版本核对。
七、子线程中使用 Handler

1. 让新线程自己处理消息
如果消息需要在这个子线程执行,就在该线程内依次准备 Looper、创建 Handler、启动循环。下面的独立示例会投递一条消息,在子线程打印日志后退出:
new Thread(() -> { Looper.prepare(); Looper workerLooper = Looper.myLooper(); Handler handler = new Handler(workerLooper) { @Override public void handleMessage(Message msg) { try { android.util.Log.d("HandlerDemo", "message=" + msg.what + ", thread=" + Thread.currentThread().getName()); } finally { workerLooper.quitSafely(); } } }; handler.sendEmptyMessage(1); Looper.loop();}, "manual-worker").start();示例使用 API 18 起提供的 quitSafely()。消息可以在 loop() 之前入队,等线程进入循环后再执行。这正好验证:创建与入队不依赖循环已经启动,分发才依赖它。
如果把 sendEmptyMessage() 放到 loop() 后面,线程就会先等待消息,无法执行后面的发送语句。若要把 Handler 暴露给其他线程,还需要通过锁、同步工具或其他安全发布方式,确保对方在初始化完成后取得它。
2. 使用 HandlerThread 管理准备过程
HandlerThread 在 run() 内封装了准备 Looper、发布 Looper 和启动循环的过程。下面示例把工作交给 worker 线程,执行完成后退出:
HandlerThread worker = new HandlerThread("worker");worker.start(); Handler handler = new Handler(worker.getLooper());handler.post(() -> { try { android.util.Log.d("HandlerDemo", "running on " + Thread.currentThread().getName()); } finally { worker.quitSafely(); }});正常启动后,getLooper() 会等待内部 Looper 准备完成。如果线程尚未启动或已经结束,该方法可以返回 null,因此调用顺序和线程生命周期都需要管理。
这里 Handler 可以在主线程创建,Runnable 仍然由 worker 执行。对于需要持续接收任务的线程,应由持有者在不再使用时退出,而不是每个任务完成后都退出。
参见 HandlerThread API 文档 和 Android 8.1 HandlerThread.java。
3. Handler Callback结果回调到主线程
如果子线程只是计算或加载数据,再把结果交给界面,它并不需要拥有自己的 Looper。显式绑定主线程即可:
// 可以在应用的普通子线程中执行。Handler mainHandler = new Handler(Looper.getMainLooper());mainHandler.post(() -> { // 这里运行在主线程,可在检查界面生命周期后更新 UI。 android.util.Log.d("HandlerDemo", "running on " + Thread.currentThread().getName());});需要哪条线程处理消息,就绑定那条线程的 Looper;只有希望当前子线程本身承担消息循环时,才需要为它准备 Looper。
八、退出消息循
普通 Looper.prepare() 创建的队列允许退出,主线程通过 prepare(false) 创建的队列则不允许退出。对主线程 Looper 调用 quit() 或 quitSafely() 会抛出异常。
对于可退出的工作线程,两种退出方式的差异如下:
| 方法 | 队列中待处理消息的命运 |
|---|---|
quit() |
丢弃待处理消息,结束循环 |
quitSafely() |
处理已到期且可分发的消息,丢弃尚未到期的延迟消息,再结束循环 |
退出请求不会强行中断正在执行的回调。回调需要先返回,循环才能继续推进到退出。同步屏障等机制也会影响消息是否可分发,因此“安全退出”不意味着保证所有已提交任务都执行。
队列开始退出后,新的投递会失败,sendMessage() 或 post() 返回 false。即使投递曾返回 true,也只代表当时成功入队,不能保证它不会因随后退出而被丢弃。
九、测试
可以在测试工程中分别验证以下情况;涉及未捕获异常的场景应单独运行:
| 操作 | 预期现象 |
|---|---|
普通子线程中直接无参 new Handler() |
因当前线程没有 Looper 而抛异常 |
子线程先 prepare(),再创建 Handler 并发送消息 |
构造、入队成功;尚未进入循环时不会分发 |
在上一种情况后调用 loop() |
消息开始由该子线程处理 |
子线程创建 Handler(Looper.getMainLooper()) 并 post() |
Runnable 在主线程执行 |
同一线程调用两次 prepare() |
第二次抛出异常 |
定位问题时,先看 Handler 构造时绑定的是哪个 Looper,再看那个 Looper 是否已经进入循环、是否正在被长任务阻塞或已经退出。沿着这条顺序排查,才能分清“没有队列可绑定”“消息还没开始处理”和“回调执行不及时”三类不同的问题。
本文由 AgentWorkFlow 生成