Handler后传篇一: 为什么Looper中的Loop()方法不能导致主线程卡死?

本文深入探讨了Android中Looper的loop()方法为何不会导致主线程卡死,分析了两个关键原因:耗时操作后的触屏事件未及时分发及loop()方法自身具备的事件分发机制。

关于 Handler 的问题已经是一个老生常谈的问题, 网上有很多优秀的文章讲解 Handler, 之所以还要拿出来讲这个问题, 是因为我发现, 在一些细节上面, 很多人还都似懂非懂, 面试的时候大家都能说出来一些东西, 但是又说不到点子上, 比如今天要说的这个问题: 为什么Looper 中的 loop()方法不能导致主线程卡死??

先普及下 Android 消息机制 的基础知识: 

Android 的消息机制涉及了四个类:

  1. Handler: 消息的发送者和处理着
  2. Message: 消息的载体
  3. MessageQueue: 消息队列
  4. Looper: 消息循环体

其中每一条线程只有一个消息队列MessageQueue, 消息的入队是通过 MessageQueue 中的 enqueueMessage() 方法完成的, 消息的出队是通过Looper 中的loop()方法完成的.

Android 是单线程模型, UI的更新只能在主线程中执行, 在开发过程中, 不能在主线程中执行耗时的操作, 避免造成卡顿, 甚至导致ANR. 

这里面, 我故意把执行耗时这四个字突出, 我想大家在面试的时候说个这个问题, 但是造成界面卡顿甚至ANR的原因真的是执行耗时操作本省造成的吗??

现在我们来写个例子, 我们定义一个 button, 在 button 的 onClick 事件中写一个死循环来模拟耗时操作, 代码很简单, 例子如下: 

@Override
public void onClick(View v) {

    if (v.getId() == R.id.coordination) {
        while (true) {
            Log.i(TAG, "onClick: 耗时测试");
        }
    }
}复制代码

注意, 这里我们运行程序, 然后点击按钮以后, 接下来不做任何操作

运行程序以后, 你会发现, 我们的程序会已知打印 log, 并不会出现ANR的情况...

按照我们以往的想法, 如果我们在主线程中执行了耗时的操作, 这里还是一个死循环, 那么肯定会造成ANR的情况, 那为什么我们的程序现在还在打印 log, 并没有出现我们所想的ANR呢??

接下来让我们继续,  如果这时候你用手指去触摸屏幕, 比如再次点击按钮或者点击我们的返回键, 你会发现5s 以后就出现了ANR....

其实前面的这个例子, 已经很好的说明了我们的问题. 之所以运行死循环不会导致ANR, 而在自循环以后触摸屏幕却出发了ANR, 原因就是因为耗时操作本身并不会导致主线程卡死, 导致主线程卡死的真正原因是耗时操作之后的触屏操作, 没有在规定的时间内被分发。其实这也是我们标题索要讨论的Looper 中的 loop()方法不会导致主线程卡死的原因之一。

看过 Looper 源码的都知道, 在 loop() 方法中也是有死循环的: 

for (;;) {
    //省略
}复制代码

前面我们说过, 死循环并不是导致主线程卡多的真正原因, 真正的原因是死循环后面的事件没有得到分发, 那 loop()方法里面也是一个死循环, 为什么这个死循环后面的事件没有出现问题呢??

熟悉Android 消息机制的都知道, Looper 中的 loop()方法, 他的作用就是从消息队列MessageQueue 中不断地取消息, 然后将事件分发出去:

for (;;) {
    /**
     * 通过 MessageQueue.next() 方法不断获取消息队列中的消息
     */
    Message msg = queue.next(); // might block
    if (msg == null) {//如果没有消息就会阻塞在这里
        // No message indicates that the message queue is quitting.
        return;
    }

    // This must be in a local variable, in case a UI event sets the logger
    Printer logging = me.mLogging;
    if (logging != null) {
        logging.println(">>>>> Dispatching to " + msg.target + " " +
                msg.callback + ": " + msg.what);
    }
    /**
     * 取出消息以后调用 handler 的 dispatchMessage() 方法来处理消息
     */
    msg.target.dispatchMessage(msg);

    if (logging != null) {
        logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
    }

    // Make sure that during the course of dispatching the
    // identity of the thread wasn't corrupted.
    final long newIdent = Binder.clearCallingIdentity();
    if (ident != newIdent) {
        Log.wtf(TAG, "Thread identity changed from 0x"
                + Long.toHexString(ident) + " to 0x"
                + Long.toHexString(newIdent) + " while dispatching to "
                + msg.target.getClass().getName() + " "
                + msg.callback + " what=" + msg.what);
    }

    msg.recycleUnchecked();
}复制代码

最终调用的是 msg.target.dispatchMessage(msg) 将我们的事件分发出去, 所以不会造成卡顿或者ANR.

对于第一个原因, 我相信大家看那个对应的例子, 一定能看明白怎么回事, 但是对于第二个原因,该如何去验证呢?? 

想象一下, 我们自己写的那个例子, 造成ANR是因为死循环后面的事件没有在规定的事件内分发出去, 而 loop()中的死循环没有造成ANR, 是因为 loop()中的作用就是用来分发事件的, 那么如果我们让自己写的死循环拥有 loop()方法中同样的功能, 也就是让我们写的死循环也拥有事件分发这个功能, 如果没有造成死循环, 那岂不是就验证了第二点原因?? 接下来我将我们的代码改造一下, 我们首先通过一个 Handler 将我们的死循环发送到主线程的消息队列中, 然后将 loop() 方法中的部分代码 copy 过来, 让我们的死循环拥有分发的功能: 

new Handler(Looper.getMainLooper()).post(new Runnable() {
    @Override
    public void run() {
        try {
            Looper mainLooper = Looper.getMainLooper();
            final Looper me = mainLooper;
            final MessageQueue queue;
            Field fieldQueue = me.getClass().getDeclaredField("mQueue");
            fieldQueue.setAccessible(true);
            queue = (MessageQueue) fieldQueue.get(me);
            Method methodNext = queue.getClass().getDeclaredMethod("next");
            methodNext.setAccessible(true);
            Binder.clearCallingIdentity();
            for (; ; ) {
                Message msg = (Message) methodNext.invoke(queue);
                if (msg == null) {
                    return;
                }
                msg.getTarget().dispatchMessage(msg);
                msg.recycle();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
});复制代码

运行代码后你会发现, 我们自己写的死循环也不会造成ANR了!! 这也验证了我们的第二个原因

到目前为止, 关于为什么 Looper 中的 loop() 方法不会造成主线程阻塞的原因就分析完了, 主要有两点原因: 

  1. 耗时操作本身并不会导致主线程卡死, 导致主线程卡死的真正原因是耗时操作之后的触屏操作, 没有在规定的时间内被分发。
  2. Looper 中的 loop()方法, 他的作用就是从消息队列MessageQueue 中不断地取消息, 然后将事件分发出去。

后记: 

关于这个问题, 我上 google 搜了一下, 发现网上有很多博主说原因是因为 linux 内核的 eoll 模型, native 层会通过读写文件的方式来通知我们的主线程, 如果有事件就唤醒主线程, 如果没有就让主线程睡眠。

其实我个人的并不同意这个观点, 这个有点所答非所谓, 如果说没有事件让主线程休眠是不会造成主线程卡死的原因, 那么有事件的时候, 在忙碌的时候不也是在死循环吗??那位什么忙碌的时候没有卡死呢?? 我个人认为 epoll 模型通过读写文件通知主线程的作用, 应该是起到了节约资源的作用, 当没有消息就让主线程休眠, 这样可以节约 cpu 资源, 而并不是不会导致主线程卡死的原因。


转载于:https://juejin.im/post/5c5694b951882562e5441e71

<think>好的,用户想知道在循环或loop函数中避免长时间阻塞代码的原因和最佳实践。首先,我需要回忆下事件循环的基本概念。根据引用[1],事件循环在JavaScript中处理异步任务,保持应用响应。如果长时间阻塞,事件循环会被卡住,导致界面无响应。 然后引用[2]提到Android的主线程Looper处理所有事件,如果阻塞,会影响Activity生命周期,导致ANR。这说明阻塞代码会阻碍事件处理,影响用户体验。 引用[3]解释阻塞和非阻塞的区别。阻塞操作会挂起线程,而非阻塞允许继续执行。在事件循环中使用阻塞代码会导致整个线程卡住,无法处理其他任务。 接下来要分析原因:1. 保持响应性,避免UI冻结。2. 防止任务堆积,避免延迟。3. 避免系统强制关闭应用(如ANR)。4. 提高资源利用率,让其他任务有机会执行。 最佳实践方面,引用[1]的代码示例展示了将耗时操作分块,用setTimeout让出控制权,这样事件循环可以处理其他任务。类似地,在Android中应该使用Handler或协程来异步处理。 还需要提到分解任务、使用异步API、定时释放控制权等方法。引用[4]中的代码显示主线程Looper.loop()之前创建Binder线程,可能涉及线程分工,避免主线程阻塞。 最后要总结,避免阻塞是为了保证事件循环的高效运行,确保应用的流畅性和稳定性。需要结合不同平台的具体实践,如JavaScript的Promise、Android的Handler等。</think>在事件循环(Event Loop)或消息循环(Loop)中避免长时间阻塞代码,是保证系统响应性和稳定性的核心原则。以下是具体原因和最佳实践: --- ### **原因分析** 1. **响应性保障** 事件循环的核心任务是调度和执行事件/消息。若某段代码长时间阻塞(例如复杂计算、同步I/O),会导致后续事件无法及时处理,造成界面冻结或操作无响应。例如: - 在JavaScript中,阻塞主线程导致浏览器页面卡死[^1]; - 在Android中,主线程阻塞超过5秒会触发ANR(Application Not Responding)错误[^2]。 2. **任务堆积风险** 阻塞期间新到达的任务会被积压在队列中。例如,Android的`MessageQueue`若无法及时处理消息,可能导致内存泄漏或逻辑延迟执行[^4]。 3. **系统资源浪费** 阻塞的线程无法释放CPU资源,其他任务需等待,降低系统吞吐量(尤其在单线程环境如浏览器主线程或Android主线程)[^3]。 4. **事件驱动架构的依赖** 多数现代框架(如Node.js、Android)依赖非阻塞事件循环。阻塞代码会破坏其设计前提,导致调度失效[^1][^2]。 --- ### **最佳实践** 1. **分解耗时任务** 将长任务拆分为小块,通过分时执行让出控制权。例如JavaScript中的示例: ```javascript function processNext(array, index) { if (index < array.length) { // 处理单个元素 setTimeout(() => processNext(array, index + 1), 0); } } ``` 这种方式允许事件循环在每次`setTimeout`后处理其他任务[^1]。 2. **使用异步API** - 在JavaScript中,用`Promise`、`async/await`处理I/O操作; - 在Android中,通过`Handler`、`Looper`或协程(如Kotlin的`Coroutine`)将耗时操作移至子线程[^2]。 3. **避免同步阻塞调用** 例如: - 文件读写使用异步接口(如Node.js的`fs.readFile`); - 网络请求使用非阻塞模式(如Android的`OkHttp`回调)。 4. **定时释放控制权** 在循环中插入`yield`点(如`setTimeout(fn, 0)`或`Looper`的消息分片),确保事件循环能处理高优先级任务[^1]。 --- ### **技术实现对比** | 场景 | 阻塞代码的影响 | 非阻塞解决方案 | |--------------------|------------------------------------|--------------------------------| | 浏览器主线程 | 页面卡死,无法响应用户交互 | Web Workers或任务分片[^1] | | Android主线程 | ANR弹窗,应用可能被强制关闭 | `AsyncTask`或`HandlerThread`[^2] | | Node.js事件循环 | 服务无法处理新请求,QPS下降 | 使用`cluster`模块或异步I/O | --- ### **总结** 事件循环的本质是**通过非阻塞调度最大化资源利用率**。阻塞代码会破坏这机制,导致级联故障。开发者需通过任务拆分、异步编程和线程分工(如Android的Binder线程[^4])来规避这问题,从而保证系统的流畅性和可靠性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值