Handler 运行流程源码解析(八):为什么主线程的 Looper 死循环不会卡死应用(ANR)

文章内容

主线程最终进入 Looper.loop(),正常运行时这个方法不会返回。既然线程一直停留在循环中,Activity 生命周期、点击事件和界面刷新为什么还能执行?

关键在于:这些工作本来就由事件循环驱动。 主线程不需要先退出 loop() 才能执行回调;回调就在循环处理事件的过程中被调用。空闲时,循环又会进入可唤醒的阻塞等待,避免持续空转。

一、先区分无限循环、忙等、死锁和 ANR

几个词经常被混用,但它们描述的不是同一件事:

概念 含义
无限循环 没有按普通条件自然结束的循环结构
忙等 不断运行代码检查条件,持续消耗 CPU
死锁 执行方之间形成无法自行解除的等待依赖
界面卡顿或无响应 用户感知到交互、绘制没有及时完成
ANR 系统监测的某项响应或组件执行超过期限

for (;;) 只能说明控制结构会持续重复,不能单独证明忙等或死锁。Looper 在每轮中处理工作或等待事件;业务代码如果在回调中写了一个永不返回的循环,才会让它无法继续处理后续工作。

ANR 也不是“主线程方法几秒没返回”的统一检查。主线程的 loop() 本来就长期不返回,系统关心的是需要响应的具体工作是否按时完成。

二、主线程的工作就在循环里面

Android 应用进程启动后,会进入 ActivityThread.main()。它准备主 Looper、建立应用线程与系统的联系,随后开始循环。省略初始化细节后,顺序如下:

java
// ActivityThread.main() 的关键顺序;startSeq 来自启动参数。Looper.prepareMainLooper();ActivityThread thread = new ActivityThread();thread.attach(false, startSeq);Looper.loop();throw new RuntimeException("Main thread loop unexpectedly exited");

ActivityThread 是应用进程主线程的管理类,不是通过创建这个对象才新建了主线程。上面的代码已经在主线程上执行。实现见 AOSP ActivityThread.main。

框架接收到生命周期调度请求后,会把相关工作安排到主线程执行;应用自己也可以通过绑定主 Looper 的 Handler 投递任务。

java
Handler mainHandler = new Handler(Looper.getMainLooper());mainHandler.post(() -> {    // 由主线程的消息分发过程执行。    updateUi();});

因此,loop() 是执行这些工作的入口,而不是挡在这些工作前面的一段业务代码。当前回调返回,控制权回到事件循环,主线程才能继续处理下一项工作。

应用启动即创建主线程消息循环

这里还要避免把所有事件都说成 Java Message。输入事件涉及 InputChannel 和 native Looper 监听的文件描述符回调,不一定先转换成 Java 消息;生命周期调度、应用 Handler 和输入分发也不是全部经过同一个 ActivityThread.H。它们共享的关键约束是:需要在主线程执行的部分,要等待主线程获得执行机会。

三、循环不空转,因为 next() 可以等待

Android 14 将单轮消息处理放在 Looper.loopOnce() 中,外层 loop() 持续调用它。合并后的正常控制流可以简化为:

java
// 正常分发路径示意,省略日志、身份校验和异常处理。for (;;) {    Message msg = queue.next();    if (msg == null) {        return;    }    msg.target.dispatchMessage(msg);    msg.recycleUnchecked();}

queue.next() 不会在队列为空时不断返回一个“没有消息”的结果,让外层疯狂重试。它在内部等待可执行消息;有消息才返回给分发流程。null 表示退出路径,不是普通空闲状态。可对照 AOSP Looper。

这带来两种行为:

  • 队列中有可执行消息时,主线程连续处理;
  • 暂时没有可执行工作时,它进入等待。不能预先断言每一轮都阻塞,或任何应用的主线程“大部分时间”都空闲,实际比例取决于负载。

主线程消息循环状态机

四、nativePollOnce 等待什么,谁让它醒来

MessageQueue.next() 内部会调用:

java
nativePollOnce(ptr, nextPollTimeoutMillis);

这是一次 JNI 调用,ptr 是 native 层 NativeMessageQueue 对象的地址。JNI 桥接层只是转发,没有额外逻辑(frameworks/base/core/jni/android_os_MessageQueue.cpp):

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 记下当前 JNI 环境后,把等待动作真正交给 native 层的 Looper;返回后再清理引用、把 native 侧攒下的异常抛回 Java:

cpp
void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {    mPollEnv = env;    mPollObj = pollObj;    mLooper->pollOnce(timeoutMillis);    mPollObj = NULL;    mPollEnv = NULL;     if (mExceptionObj) {        env->Throw(mExceptionObj);        env->DeleteLocalRef(mExceptionObj);        mExceptionObj = NULL;    }}

真正的等待发生在 Looper::pollOnce(system/core/libutils/Looper.cpp)。它是个循环:先看有没有已经就绪、可以直接返回给调用方的响应;没有就调 pollInner 去阻塞,等 pollInner 返回后再检查一轮:

cpp
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {    int result = 0;    for (;;) {        while (mResponseIndex < mResponses.size()) {            const Response& response = mResponses.itemAt(mResponseIndex++);            int ident = response.request.ident;            if (ident >= 0) {                if (outFd != nullptr) *outFd = response.request.fd;                if (outEvents != nullptr) *outEvents = response.events;                if (outData != nullptr) *outData = response.request.data;                return ident;            }        }         if (result != 0) {            if (outFd != nullptr) *outFd = 0;            if (outEvents != nullptr) *outEvents = 0;            if (outData != nullptr) *outData = nullptr;            return result;        }         result = pollInner(timeoutMillis);    }}

pollInner 里才是真正阻塞线程的地方——epoll_wait(省略了原文件中的调试日志和事件分发部分):

cpp
int Looper::pollInner(int timeoutMillis) {    // 按下一条延迟消息的到期时间收紧 timeoutMillis    if (timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) {        nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);        int messageTimeoutMillis = toMillisecondTimeoutDelay(now, mNextMessageUptime);        if (messageTimeoutMillis >= 0                && (timeoutMillis < 0 || messageTimeoutMillis < timeoutMillis)) {            timeoutMillis = messageTimeoutMillis;        }    }     int result = POLL_WAKE;    mResponses.clear();    mResponseIndex = 0;     // 即将进入空闲等待    mPolling = true;     struct epoll_event eventItems[EPOLL_MAX_EVENTS];    int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);     // 不再空闲    mPolling = false;    mLock.lock();    // ……后续处理 epoll 事件、到期消息回调、fd 事件回调,最终解锁并返回 result    mLock.unlock();    return result;}

超时为 0 时不阻塞;为正数时,最多等待指定毫秒数;为 -1 时,没有预设超时,等待事件唤醒。普通延迟消息尚未到期时,队列会计算剩余时间;没有可确定的下一条执行消息时,则可能进入无限期等待。这三种取值最终都体现为传给 epoll_wait 的 timeoutMillis:0 立即返回、正数是普通超时、-1 让内核一直挂起线程直到某个已注册的 fd 就绪或被显式唤醒。

这一步在 MessageQueue 的 Java 同步块之外。因此,消费者阻塞时,其他线程仍可进入队列锁、添加消息,并在需要时发出唤醒信号。

唤醒不是“每条消息都对应一次”。例如,队列已经在等待十秒后执行的消息,此时插入一条一秒后执行的消息,就需要让消费者重新计算等待时间;若新增的是更晚的消息,通常没有必要提前唤醒。完整逻辑还会考虑同步屏障和异步消息。

native 层使用 epoll 监听事件,本文版本通过 eventfd 发出显式唤醒信号。文件描述符事件、等待超时和显式唤醒都可能让轮询继续运行。因此,一次唤醒不等于恰好消费一条 Java Message,也不保证某条业务消息立即执行。相关实现见 AOSP native Looper。

五、可唤醒等待为什么不同于业务阻塞

从“线程没有继续往下走”的表象看,nativePollOnce() 和回调里的 Thread.sleep() 很像,但它们对事件处理的影响不同。

空闲轮询已经接入 Looper 的唤醒机制。新事件到来后,线程可以被唤醒、检查事件并继续分发。业务回调里的休眠、锁等待或长时间 I/O,却不会因为队列多了一条消息就自动结束。

java
mainHandler.post(() -> {    doSlowWork();    // 这段工作返回前,主线程不能正常进入下一轮消息分发。});

即使把 doSlowWork() 放进 post(),只要 Handler 绑定的是主 Looper,它仍然占用主线程。post() 改变了执行的安排方式,不会自动把任务变成后台工作。

还可能出现真正的死锁:主线程等待工作线程结束,而工作线程又向主线程投递回调并等待回调完成。此时双方互相等待,主线程再高效的轮询机制也无法解开这个业务依赖。

主线程睡眠与卡死(ANR)对比

六、ANR 监测对象分析

广播通过 goAsync() 将工作转移后,若后台任务未及时调用 PendingResult.finish(),即使主线程已经空闲,也仍可能发生广播 ANR。ANR 的类型、超时条件和相关线程需要分别检查;输入分发超时常见默认值为 5 秒,其他类型和设备实现不能直接套用同一个数字。见 Android 官方 ANR 排查文档。

同样,在 ANR 堆栈中看到主线程停在 nativePollOnce,并不能证明从未卡住。采样可能晚于实际阻塞,也可能需要查主线程之外的任务。应结合报告的 ANR 原因、发生时间和相关线程分析,而不是把一个栈帧当作结论。

Looper 的日志和慢分发诊断能够帮助观察耗时,但不等同于系统对各类 ANR 的超时判定。

七、观察消息循环

调试时可以在应用主线程设置消息日志:

java
Looper.getMainLooper().setMessageLogging(        new LogPrinter(Log.DEBUG, "MainLooper"));

随后投递一个轻量任务:

java
Handler mainHandler = new Handler(Looper.getMainLooper());mainHandler.post(() ->        Log.d("MainLooper", "running on " + Thread.currentThread().getName()));

日志能显示 Java 消息分发的开始和结束,帮助确认回调运行在主线程。它并不覆盖所有 native 输入回调,也不能独自定位所有耗时来源。日志本身会增加开销,观察结束后恢复:

java
Looper.getMainLooper().setMessageLogging(null);

进一步分析时,可以结合 Perfetto 查看主线程是在运行、等待 CPU、阻塞在某项调用,还是空闲轮询。单次线程栈只是一个时间点;分析卡顿时,需要把栈与时间线、任务耗时联系起来。

如果想验证“循环不返回仍然能处理消息”,在独立 HandlerThread 上投递几次轻量任务即可。测试结束后调用 quitSafely() 并在后台确认线程结束,不必人为冻结主线程或要求每次实验都弹出 ANR 对话框。

八、为什么主线程不用像工作线程一样主动退出

工作线程不再使用时,可以请求退出 Looper;主线程则由框架创建为不可退出的消息队列。对主 Looper 调用 quit() 或 quitSafely() 会抛出 IllegalStateException,不能用这种方式结束一个页面。

页面销毁时应处理的是自己的任务、监听和引用关系。主线程仍然可能需要服务同一进程中的其他组件,它的事件循环不属于某个 Activity。

回答开头的问题时,可以沿着执行方式说明:主线程进入 Looper 后,以事件循环驱动回调;可执行工作到来时处理,空闲时通过 native 轮询等待。这个循环本身既不构成忙等,也不构成死锁。应用能否持续响应,取决于主线程是否能及时把控制权交还给事件循环,以及系统监测的工作是否按时完成。


本文由 AgentWorkFlow 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Handler 运行流程源码解析(八):为什么主线程的 Looper 死循环不会卡死应用(ANR) · Yinchen 的博客