为什么主线程的 Looper.loop() 不会阻塞 UI 线程?

5
今天我阅读了一些关于Handler和Looper如何协作的博客和源代码。
根据我的学习,我们可以通过使用ThreadLocal技巧,在每个线程上只有一个Looper。通常情况下,Handler在主线程中初始化,否则您必须手动启动或者说,准备单独的线程中的Looper,然后循环它。
class LooperThread extends Thread {
    public Handler mHandler;

    public void run() {
        Looper.prepare();

        mHandler = new Handler() {
            public void handleMessage(Message msg) {
                // process incoming messages here
            }
        };

        Looper.loop();
    }
}

我真正困惑的是主线程中的 loop()。当我在 Looper 的源代码中读到它时,它是一个无限循环,用于处理消息队列,然后将消息分派给回调进行处理。
根据这个https://dev59.com/9W435IYBdhLWcg3w0Tnc#5193981,Handler 和它的 Looper 运行在同一线程中。
如果主线程上有一个无限循环,那么它不会阻塞整个 UI 系统吗?
我知道我一定很傻才会错过什么。但对于某人揭示其中的秘密,这将是可爱的。
public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;

    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    for (;;) {
        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);
        }

        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();
    }
}
2个回答

7
实际上,主线程中的Looper是允许绘图的。当视图无效时,消息被传递到主线程的Looper,告诉它请求绘制。当Looper处理该消息时,实际的绘制发生。其他阻塞UI线程的活动之所以会阻塞绘制,是因为它阻止了Looper处理绘制消息。
这基本上是任何基于事件的系统中的绘图方式,从Windows到Mac再到Android。
为什么不立即绘制而是发送消息呢?性能原因。绘图很慢。如果您对一个事件做出多个更改,您不希望为每个更改重新绘制屏幕。用这种方法,您可以将处理单个事件的所有重绘操作集中在一起进行1次重绘。例如,如果您设置了一个视图的文本和另一个视图的图像,则它们都将同时重绘,并且只重绘1次。

我认为你没有仔细阅读我的问题。我的意思是,如果主线程上有 for(;;){ } 循环,那么如何发送消息以绘制用户界面呢? - Ryan Hoo
1
我做了,我不认为你理解我的答案。其中一个消息发送给循环器引起了绘制。绘制是发送给该循环器的一条特殊消息。 - Gabe Sechan
3
换句话说,你所认为的主线程实际上是一个名为Looper的处理消息并根据其调用你的代码的东西。这是一种称为事件循环或消息循环的技术,在事件驱动编程中很常见。这里有Android的详细解释 http://mattias.niklewski.com/2012/09/android_event_loop.html - Gabe Sechan
谢谢@Gabe Sechan,那篇博客真的解释得很清楚。我以前总是用多线程模型来解决这个问题。比如事件必须从另一个线程发送,主线程等待事件处理。太愚蠢了! - Ryan Hoo
@LiuWenbin_NO。我不确定ListView的滚动过程,因为我认为它更与绘图系统相关。但是对于谁特别发送消息给AndroidThread.H的问题,这是关于android IPC Binder设计的一个话题,ActivityManagerServiceActivityThreadApplicationThread之间存在关系(类名不确定,抱歉我的记忆不好)。您可以在一些博客或直接查看源代码中了解更多详细信息。 - Jian Guo
显示剩余4条评论

2
这个问题是一个微妙的陷阱。为什么无限循环不会阻塞UI线程?因为所有UI线程的行为都始于msg.next。
如果没有消息,这意味着不需要更新。我们所有的代码都只是回调函数,比如Application onCreate,Activity onCreate,BroadcastReceiver onReceive。
所有的更新回调都是由消息引起的,而这些消息来自系统服务,比如ActivityManagerService、InputManagerService、WindowMangerService。如果你需要更新UI,安卓服务会通过IPC向循环发送一条消息。
因此,无限循环就是无限更新。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接