Handler Looper MQ(eventfd & epol

作者: 北邙山之光 | 来源:发表于2021-03-05 09:37 被阅读0次
    eventfd_notify.png

    前言

    Handler、Looper、MessageQueue 是老生常谈的话题了,你可能会觉得没什么好讲的啊,网上文章也一堆。

    这里我有几个问题问一下,如果你都很清晰,那就完全不需要看此文。

    1.多个 Handler 可以往同一个 MQ 里发消息么?如果可以,多个 Handler 都会处理对应的消息么?为什么?

    2.Looper 会 单纯 的因为取不到消息而导致死循环退出么?

    3.Looper 是死循环,为什么主线程没有卡死(这个看 https://www.zhihu.com/question/34652589)

    4.是接着问题2的一系列问题。看过 MQ.next() 方法么?看过 nativePollOnce() 的代码?如果看过会发现调用流程是: nativePollOnce() -> pollOnce() -> pollInner()。但是 pollOnce() 是个死循环,nativePollOnce() 怎么跳出去的?

    5.Java 层和 Native 层的 MQ、Looper 有什么关系吗?有什么优先级么?

    6.如果主线程处于休眠状态,点击屏幕的时候又是怎么被唤醒的?点击事件是通过什么机制传递过来的?(IMS)

    问题解析

    Question 1

    Handler 有众多传递事件的方法

    诸如: sendMessage()、post() 等等

    不过这些方法最终都汇聚于一个方法 enqueueMessage()

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        // this 就是 Handler 对象
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }
    

    Handler 的构造函数也有多个

     public Handler(Looper looper, Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }
    public Handler(Callback callback, boolean async) {
        // 省略
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }
    
    

    通过以上可知,Handler 初始化要么当前线程已经存在 Looper,要么自己绑定一个 Looper。

    主线程的 Looper 在 AcitivityThread.main 方法(App 进程创建的时候)已经通过 Looper.prepareMainLooper() 创建完成了。

    那么,当然是可以在主线程创建多个 Handler 绑定到同一个 Looper(MQ 与 Looper 相对应)的,这些 Handler.enqueueMessage() 方法均是往同一个 Looper 所对应的同一个 MQ 里发送消息。

    但是可以看到 enqueueMessage() 时,每一个 Message 对象的 target 属性都被设置为了发送此消息的 Handler(见上方源码),所以谁发的 Message 谁自己处理,这当然也是合情合理的。

    答:
    多个 Handler 可以往同一个 MQ 发消息,但是谁发的消息谁处理,因为 Message 保留了 Handler 对象的引用。(当然 Looper 其实也是通过取得 Message 之后,用 Message.target 回调到 Handler.handleMessage() 的)

    Question 2

    Looper.loop() 众所周知是一个死循环,但是细看代码你会发现一些问题

    loop() 方法部分源码如下:

    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }
        // 省略
    }
    

    省略了很多代码,只看这个 queue.next() 方法,发现只要 msg == null 就 return 了,不就退出了?但是不会出现这种情况,后面说原因。

    Looper.quit()

    先看看 quit() 方法,其实它调用的是 MQ.quit() 方法,可以传一个 safe 参数判断是否管理剩余的消息。具体的代码如下:

    oid quit(boolean safe) {
        if (!mQuitAllowed) {
            throw new IllegalStateException("Main thread not allowed to quit.");
        }
    
        synchronized (this) {
            if (mQuitting) {
                return;
            }
            mQuitting = true;
    
            if (safe) {
                removeAllFutureMessagesLocked();
            } else {
                removeAllMessagesLocked();
            }
    
            // We can assume mPtr != 0 because mQuitting was previously false.
            nativeWake(mPtr);
        }
        }
    

    通过源码会发现 mQuitAllowed 这个值,如果是 false, 就是不让 Looper 退出直接抛出异常,且这个参数是 Looper 初始化时传入的。

    所以,其实即使我们想要 主动 调用 quit() 方法让 Looper 退出,也要看创建 Looper 时的 mQuitAllowed 参数。MainLooper 此处就是 false,所以没法调 quit 方法。

    queue.next() 为什么不会是 null

    先看代码:

    Message next() {
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }
    
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
    
            nativePollOnce(ptr, nextPollTimeoutMillis);
        }
        // 省略
    }
    

    mPtr 如果为 0 自然返回 null,但是正常情况下这个不可能的,因为 mPtr 是 MQ 初始化时调用的 nativeInit 返回的一个 NativeMQ 指针。

    后面的代码均包含在了 for 死循环中,且只会 return 实体 message,而 nativePollOnce 最终会调用到 epoll_wait() 而使得线程被挂起休眠,具体查看问题3。

    所以除非有什么系统故障,否则 Looper 要么因为取不到消息而休眠,要么就返回一个非空消息。

    答:
    除非有系统故障,否则不会 单纯 的因为取不到消息而导致死循环退出。这也合情合理,主 Looper 退出了,后续的事件又如何处理呢?

    Question 3

    这个查看知乎链接即可。

    https://www.zhihu.com/question/34652589)

    首先是因为 epoll_wait() 的情况下,主线程只是被挂起,并不耗费 CPU。其次,如果有事件到来,也会通过 epoll 机制唤醒主线程。再其次,binder 线程池的存在使得可以从其所在线程通过 Handler 发送消息到主线程或者其他方式,最终能够唤醒主线程。

    如何唤醒的呢?

    首先需要一个 文件描述符(fd,Linux 一切皆文件) 且支持 poll 操作。之后通过 epoll_ctl() 注册,epoll_wait() 等待事件到来。

    事件如何来?

    往 fd 里写入数据等等,根据 epoll_ctl 注册的消息类型会进行相应的唤醒。

    Looper 处理 Java 层 enqueueMessage 唤醒时,老版本使用了 pipe,新版本使用了 eventfd

    Question 4 & 5

    MessageQueue.next() 方法 Question 2 已经看过了。

    之后的 native 调用过程是 nativePollOnce() -> pollOnce() -> pollInner()

    伪代码大概如下(源码太长自己去看):

    // pollOnce
    for (;;) {
            while (mResponseIndex < mResponses.size()) {
            const Response& response = mResponses.itemAt(mResponseIndex++);
            int ident = response.request.ident;
            if (ident >= 0) {
                // 省略
                return ident;
            }
        }
    
        if (result != 0) {
            // 省略
            return result;
        }
    
        result = pollInner(timeoutMillis);
    }
    
    // pollInner
    int Looper::pollInner(int timeoutMillis) {
        // 省略
        struct epoll_event eventItems[EPOLL_MAX_EVENTS];
        int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
        for (int i = 0; i < eventCount; i++) {
            int fd = eventItems[i].data.fd;
            // 省略
            pushResponse(events, mRequests.valueAt(requestIndex));
        }
        // 省略
        for (size_t i = 0; i < mResponses.size(); i++) {
            Response& response = mResponses.editItemAt(i);
            if (response.request.ident == POLL_CALLBACK) {
                // 省略
                int callbackResult = response.request.callback->handleEvent(fd, events, data);
                if (callbackResult == 0) {
                    removeFd(fd, response.request.seq);
                }
                // 省略
            }
        }
        return result;
    }
    
    

    可以看到,一进入 pollOnce() 就是一个死循环。但是,是有几个 return 条件的,result 的值通过 pollInner() 返回,pollInner() 才真正调用了 epoll_wait() 使得线程挂起。

    当有事件来时,我们会首先读取监听的 event 事件(pushResponse),之后在循环所有的事件进行处理,而且只要有对应的事件发生,result 的返回值总不会是 0。

    result 有几种取值

    enum {
        /**
            * Result from Looper_pollOnce() and Looper_pollAll():
            * The poll was awoken using wake() before the timeout expired
            * and no callbacks were executed and no other file descriptors were ready.
            */
        POLL_WAKE = -1,
    
        /**
            * Result from Looper_pollOnce() and Looper_pollAll():
            * One or more callbacks were executed.
            */
        POLL_CALLBACK = -2,
    
        /**
            * Result from Looper_pollOnce() and Looper_pollAll():
            * The timeout expired.
            */
        POLL_TIMEOUT = -3,
    
        /**
            * Result from Looper_pollOnce() and Looper_pollAll():
            * An error occurred.
            */
        POLL_ERROR = -4,
    };
    

    所以,pollOnce() 的死循环 不会退不出去,也不会因此一直卡在 nativePollOnce() 方法中。当 pollOnce() 和 nativePollOnce() 返回以后,Java 层的 MQ.next() 方法继续执行,才取到了 MQ.java 中的 Message,进行后续的调用。

    答:
    pollOnce() 不会一直跳不出,只要有事件到来,都能跳出死循环。同时,Java 层和 Native 层的同样的一套机制可以互相引用,但是,是完全的两套东西,包括 MQ 也是各自管理各自的。但是 Native 的优先级更高,因为处理完了 Native 的事件以后,才开始处理 Java 层的事件。

    Question 6

    看过 InputManagerService 都知道,输入事件的传递是依赖 socket 通信的。那么它又是怎么唤醒主线程的呢?

    在 Native 层的 Looper 中有 addFd() 方法,socket 同样是一个 fd,Looper.addFd() 通过 epoll_ctl 注册 socket 监听。

    当有输入事件需要传递时,只需要往 socket 中写入数据,就可以唤醒主线程。

    依然是会回到 pollInner() 相关的代码

    response.request.callback->handleEvent(fd, events, data)

    这里其实已经开始到了主线程相关的事件分发部分,并且通过 Native 层直接调用 Java 层代码的方式调了回去,直到 ViewRootImpl.deliverInputEvent()

    详见:
    http://gityuan.com/2016/12/31/input-ipc/

    答:同样是通过 epoll 机制唤醒

    Worker Pool

    其实无论是 Android 还是其他的事件驱动的模型,原理都相似,下文就是介绍一个 Worker Pool 的设计。

    其实,我们的主线程可以看做是一个消费者,不断消费各种事件,而各种其他的线程(Android 中,如 InputManagerService 等等系统服务)都可以看做是生产者,不断的产生事件通知,交给主线程处理,从而让整个程序得以运转。

    https://www.yangyang.cloud/blog/2018/11/09/worker-pool-with-eventfd/

    总结

    其实了解这些对于实际开发作用并不大,而且整个过程依然有很多细节没有深究。

    但是,对于原理和模型的了解是十分必要的,了解了这些才能举一反三。

    引用

    https://www.yangyang.cloud/blog/2018/11/09/worker-pool-with-eventfd/

    相关文章

      网友评论

        本文标题:Handler Looper MQ(eventfd & epol

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