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

5,988 阅读6分钟

关于 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 资源, 而并不是不会导致主线程卡死的原因。