美文网首页Android技术知识Android开发
为什么Looper中的Loop()方法不能导致主线程卡死?

为什么Looper中的Loop()方法不能导致主线程卡死?

作者: Android开发架构 | 来源:发表于2019-02-18 15:33 被阅读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 资源, 而并不是不会导致主线程卡死的原因。

    免费获取安卓开发架构的资料(包括Fultter、高级UI、性能优化、架构师课程、 NDK、Kotlin、混合式开发(ReactNative+Weex)和一线互联网公司关于android面试的题目汇总可以加:936332305 / 链接:点击链接加入【安卓开发架构】:https://jq.qq.com/?_wv=1027&k=515xp64

    在这里插入图片描述

    相关文章

      网友评论

        本文标题:为什么Looper中的Loop()方法不能导致主线程卡死?

        本文链接:https://www.haomeiwen.com/subject/twwaeqtx.html