Android:Handler消息机制(四)——为什么主线程不会因Looper.loop()里的死循环卡死

本文深入探讨Android主线程如何在Looper.loop()死循环中保持运行,而不消耗大量CPU资源。通过分析主线程的生命周期和消息处理机制,解释了主线程如何在等待Message时进入阻塞状态,以及为何长时间运行的任务会导致ANR,而不是死循环。

这个问题需要通过三方面来讲:

1.为什么主线程不会因为Looper.loop()里的死循环卡死? 

2.为什么主线程一直在死循环却不会占用大量CPU消耗?

3.那究竟是什么导致主线程卡死?

 

一、为什么主线程不会因为Looper.loop()里的死循环卡死? 

 

首先理解“线程进入死循环”这个问题, 就是在循环体内具有一段可执行的子程序,由于for(; ;)的调度导致这段子程序持续不断的在执行, 也就是持续的占用CPU资源, 从而导致当前线程的循环体外的子程序无法执行, 导致线程卡死的状态。

 

对于主线程既然是一段可执行的代码,也需要维持自己的执行周期,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程绝不希望会被运行一段时间就自动退出,所以为了何保证程序能一直存活,就需要一个死循环来保证主线程一直处于运行状态,让程序处于运行阶段。 所以这个死循环可以看作是一种“假卡死”状态, 倘若这个循环体结束了意味着主线程该停止运行了, 那么app可以退出了。

 

那既然是死循环又如何去处理其他事务呢?会通过创建新线程的方式来完成,这里简单介绍一下,ActivityThread实际上并非线程,它不像HandlerThread类,ActivityThread并没有真正继承Thread类,只是往往运行在主线程,给人以线程的感觉,其实承载ActivityThread的主线程就是由Zygote fork而创建的进程,以下就是代码ActivityThread.main():

    public static void main(String[] args) {
        ....
        //初始化Looper,创建Looper和MessageQueue对象,用于处理主线程的消息
        Looper.prepareMainLooper();

        //创建ActivityThread对象,并绑定到AMS
        ActivityThread thread = new ActivityThread(); 

        //建立Binder通道,创建新线程,一般的应用程序都不是系统应用,因此设置为false,在这里面会绑定到AMS
        thread.attach(false);

        if (sMainThreadHandler == null) {
       		sMainThreadHandler = thread.getHandler();
    	}
    	if (false) {
            Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "ActivityThread"));
    	}

        Looper.loop(); //开启循环,消息循环运行
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

在创建ActivityThread的时候,会在这里面进行内部的两个重要变量的初始化,就是后续的mAppThread Binder实例以及一个H Handler实例。ActivityThread内的所有message就是来自其他线程send过来的, 例如binder线程的消息也是通过handler方式传递给ActivityThread的queue。

 

ActivityThread对应的Handler是一个内部类H继承于Handler,任何一个与生命周期相关的消息, 都会把target设置为H, 这样才能够确保在for(; ;)中取出的每一个message都能够通过message.target.sendMessage(msg)的方式通知H类的handleMessage()方法,里边通过handler消息机制来进行启动Activity、处理Activity生命周期等方法。Activity的生命周期函数都是在主线程的Looper.loop()循环中,被ActivityThread内部的Handler.handleMessage()调用,当收到不同Message时则采用相应措施,在H.handleMessage(msg)方法中,根据接收到不同的msg,进而调用不同的生命周期处理函数:

比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终会通过反射机制,创建Activity实例,然后再执行Activity.onCreate()等方法;

比如收到msg=H.PAUSE_ACTIVITY,则调用ActivityThread.handlePauseActivity()方法,最终会执行Activity.onPause()等方法。

 

而当main方法里调用了thread.attach(false),就便会创建一个Binder线程,,在内部进行 AMS(ActivityManagerSevice)和mAppThread Binder进程通讯者的绑定,即是AMS的attachApplication()的调用,在这个函数里面将会进行第一次跨进程通讯,AMS运行在系统进程,而我们的APP是另外一个进程。此时在AMS里面会调用上面绑定的mAppThread binder对象的bindApplication(...)方法,触发BIND_APPLICATION消息,该消息由 H 的sendMessage(...)来进行发送,此时消息会在 Looper 里面的 loop() 进行处理,最后会在 H.handleMessage(...) 处理, 然后进入到对应的函数里面进行 Context 的初始化,Application开始初始化,并且调用它的 onCreate,等其他操作。就是说该Binder线程通过Handler将Message发送给主线程。

 

接下来在AMS的attachApplicationLocked(...)函数里面,在触发了第一次进程通讯后,代码会继续运行,在里面会进行第二次进程通讯,首先是Activity的栈管理者之一ActivityStackSupervisor调用它的attachApplicationLocked(...),并在里面调用 realStartActivityLocked(...),就会正式发起第二次IPC,触发 LAUNCH_ACTIVITY 消息,还是通过 H 发送和处理,最后在处理处调用 performLaunchActivity(...),在这里面会根据一直传下来的信息,new 一个 Actiivity,然后会调用它的onCreate()和onStart()。

所以让ActivityThread进入”假卡死”状态, 一来不需要持续的占用CPU资源, 二来可以保证应用的不退出,ActivityThread的假卡死状态是最适合这种模式的交互方式的设备。

 

二、为什么主线程一直在死循环却不会占用大量CPU消耗?

 

到了这里,其实都会有个疑问,死循环会让主线程一直运行这样会不会特别消耗CPU资源呢?其实不然,如果当前主线程的MessageQueue没有消息时,,需要等待到下一条消息可用为止,也就是处于等待状态时,因为MessageQueue的阻塞机制,会让 主线程会处于阻塞状态, 这个过程涉及到MessageQueue 类中的两个方法: next()和 enqueueMessage(Message, long).

(1) next() :从队列中获取并返回下一个消息. 如果队列为空(无返回值), 则该方法将调用 native void nativePollOnce(long, int), 此操作为阻塞操作程序就会便阻塞在loop的queue.next()中的nativePollOnce()方里,该方法将一直阻塞直到添加新消息为止.

(2)enqueueMessage():将 Message 添加到队列时, 会调用该方法, 该方法不仅将消息插入队列, 而且还会调用native static void nativeWake(long).

 

如果当前线程阻塞的话就会释放掉CPU的占用, 进入阻塞状态.这个阻塞过程主要是通过nativePollOnce和nativeWake来完成的,nativePollOnce 和 nativeWake 的核心过程发生在 native 代码中.nativePollOnce 方法用于等待,而 nativeWake则是唤醒。通过 写入一个 IO 操作到描述符, epoll_wait 等待. 然后, 内核从等待状态中取出 epoll 等待线程, 并且该线程继续处理新消息.

 

nativePollOnce 这里就涉及到Linux pipe/epoll机制,该系统调用可以监视文件描述符中的 IO 事件. handler机制就是使用pipe来实现的。当主线程的MessageQueue没有消息时,就会阻塞在loop的queue.next()中的nativePollOnce()方法里,因为nativePollOnce() 在某个文件描述符上调用 epoll_wait,会让主线程在没有消息处理时就会阻塞在管道的读端。此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,nativeWake()会通过binder线程往主线程消息队列里添加消息,就会通过往pipe管道写端写入一个字节数据来唤醒主线程从管道读端返回,也就是说queue.next()会调用返回,

 nativePollOnce 大致等同于 Object.wait(), nativeWake 等同于 Object.notify(),只不过它们的实现完全不同: nativePollOnce使用 epoll, 而 Object.wait 使用 futex Linux 调用.

 

这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。所以主线程的死循环并不是一直运行,在大多数时候都是处于休眠状态,并不会消耗大量CPU资源。因为nativePollOnce()只是表明所有消息的处理已完成, 线程正在等待下一个消息。

 

三、那究竟是什么导致主线程卡死?

 

ANR众所周知就是UI线程做了耗时操作而导致线程进入卡死状态,然后有人都会以为是不是UI线程进入Loop死循环后,就会出现卡死,但上面已经解释过了,loop死循环并不是主线程卡死的原因。

首先UI耗时导致卡死前提是要有输入事件,当一个MessageQueue不是空的,并且Looper正常轮询,线程并没有阻塞,但是该事件执行时间过长(一般5秒),而且与此期间其他的事件(按键按下,屏幕点击等等)都没办法处理,在执行时间过长后在触摸屏幕,然后就会触发ANR异常了。

所以运行死循环并不会导致ANR,原因就是因为耗时操作本身并不会导致主线程卡死, 导致主线程卡死的真正原因是耗时操作之后的触屏操作, 没有在规定的时间内被分发和处理。所以真正会卡死主线程的操作是在进行回调方法onCreate/onStart/onResume等操作时间过长,就有可能发生ANR,而就像上面说的looper.loop()本身不会导致应用卡死。

ANR机制的目的还有一个是为了监测主线程的耗时操作,譬如密集CPU运算、大量IO、复杂界面布局等,因为这些都会降低应用程序的响应能力。所以从理念上也能理解,loop死循环只是简单地处理轻量的消息操作,和ANR并没有关系。

 

<think>我们正在讨论的问题是:为什么Looper.loop()中的死循环不会导致主线程ANR? 核心要点: 1. ANR(Application Not Responding)是指应用程序未响应,通常发生在主线程被阻塞超过一定时间(如5秒)。 2. Looper.loop()确实是一个死循环,它不断地从消息队列中取出消息并处理。 3. 关键在于,这个循环本身并不是导致ANR的原因,相反,它是主线程能够持续处理消息(包括用户输入、生命周期事件等)的基础。 原因分析: 1. 消息循环的设计:主线程Looper循环在等待消息时会进入休眠状态(通过epoll机制),此时不会消耗CPU资源。当有新消息到来时,它会被唤醒并处理消息。 2. ANR的产生实际上是由于某个消息处理时间过长(超过了规定时间)导致的,而不是因为循环本身。 3. 主线程消息队列中包含了各种消息,例如:Activity生命周期回调、用户输入事件、界面绘制事件等。这些事件都是由Looper循环来分发的。 4. 如果某个消息处理时间过长,那么后续的消息(包括用户输入事件)就无法及时处理,从而触发ANR。 因此,我们可以得出结论:Looper死循环不会导致ANR,因为它本身不会阻塞主线程(在无消息时休眠),而ANR是由于处理单个消息超时引起的。 参考引用: [^1]: 提到ANR发生的种情况,并指出Looper的阻塞机制(nativePollOnce)在无消息时会让线程休眠。 [^2]: 解释了主线程Looper.loop()阻塞的意义:如果没有这个循环,主线程会立即退出。 [^3]: 指出Activity的生命周期也是由Looper循环中的消息触发的,因此循环是必要的。 [^4]: 同样强调Looper循环是为了维持应用运行。 [^5]: 详细描述了消息循环的阻塞机制(queue.next()可能会阻塞)以及如何唤醒。 回答结构: 1. 简要说明ANR的定义和触发条件。 2. 解析Looper.loop()的工作机制,重点说明无消息时休眠的特性。 3. 对比ANR产生的原因(消息处理超时)与循环本身的关系。 4. 总结:循环是必要的,且设计上不会导致ANR;ANR的真正原因是消息处理超时。 最后,生成相关问题。 </think>### Looper.loop() 死循环为何不会导致主线程 ANR #### 一、ANR 的本质与触发条件 ANR(Application Not Responding)发生在主线程**无法及时处理关键消息**时,核心触发条件是: 1. **超时阻塞** - Activity:5 秒内未响应输入事件 [^2] - Service:20 秒内未完成生命周期回调 - Broadcast:10 秒内未结束 `onReceive()` 2. **消息饥饿** 关键消息(如点击响应、生命周期回调)因前序任务阻塞而无法执行 [^2][^3] #### 二、Looper.loop() 的非阻塞机制死循环**不会导致 ANR** 的关键在于智能的阻塞-唤醒设计: ```java // 消息循环核心逻辑 public static void loop() { while (true) { // 1. 智能阻塞:无消息时休眠(不消耗CPU) Message msg = queue.next(); // 调用 nativePollOnce() // 2. 消息分发:执行注册的Handler回调 msg.target.dispatchMessage(msg); } } ``` ##### 阻塞优化原理 1. **休眠等待机制** - `queue.next()` 底层通过 `nativePollOnce()` 调用 Linux `epoll` 系统调用 [^1][^5] - 当消息队列空时,主线程进入 **WAITING 状态**(释放 CPU 资源) - 唤醒条件:新消息入队、定时任务到期、输入事件到达等 [^1][^5] 2. **高效唤醒机制** ```mermaid graph LR A[输入事件/生命周期回调] --> B(系统触发唤醒) B --> C[epoll_wait 返回] C --> D[Looper 处理消息] D --> E{是否关键消息?} E -->|是| F[立即执行] E -->|否| G[加入队列排队] ``` #### 三、与 ANR 的本质区别 | 特性 | Looper.loop() | ANR 触发场景 | |--------------------|-----------------------------------|--------------------------| | **线程状态** | 无消息时休眠(WAITING) | 持续 RUNNABLE 但无响应 | | **CPU 占用** | 休眠时 0% 占用 | 持续高占用或死锁 | | **消息处理** | 按优先级处理(输入事件优先)[^3] | 关键消息被阻塞超时 | | **系统设计意图** | 维持消息泵的必需机制 [^4] | 应用逻辑缺陷导致 | #### 、关键矛盾解析 1. **为什么看似阻塞却不 ANR?** ANR 计时器在**消息开始处理时启动**,而非在循环等待期间计时: - 点击事件入队 → Looper 唤醒 → `dispatchMessage()` 开始执行 → ANR 计时启动 - 若 `handleMessage()` 5 秒未完成 → 触发 ANR [^2][^5] 2. **生命周期如何执行?** Activity 的生命周期(如 `onCreate()`, `onResume()`)本质是 **Looper 分发的消息**: ```java // 简化源码:ActivityThread 处理生命周期 class H extends Handler { public void handleMessage(Message msg) { switch (msg.what) { case RESUME_ACTIVITY: handleResumeActivity(); // 执行 onResume() break; } } } ``` 循环本身是生命周期执行的**载体而非阻碍**[^3](引用[3]证实此观点)。 #### 五、实验验证 可通过以下代码模拟不同场景: ```java // 场景1:主线程休眠5秒(不会触发ANR) handler.postDelayed(() -> { Thread.sleep(5000); // 不会ANR,因在消息处理中计时 }, 0); // 场景2:主线程死循环(立即触发ANR) handler.post(() -> { while (true) {} // 阻塞消息队列导致输入事件超时 }); ``` > **总结**: > Looper死循环通过 `epoll` 实现了**无消息时休眠**,在消息处理时**按优先级调度**,其设计正是避免 ANR 的基石。ANR 的根源永远是**主线程同步阻塞**(如耗时计算、死锁),而非消息循环本身 [^1][^2][^3][^5]。 --- ### 相关问题
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值