为什么Looper中的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 资源, 而并不是不会导致主线程卡死的原因。

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

![在这里插入图片描述](https://img-blog.csdnimg.cn/20190123143200826.png)
  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值