笔者之前现已写过一篇关于 Java 层 Handler 机制的文章,从运用开发的角度动身,详细介绍了 Handler 机制的规划背景,以及怎样自己怎样手写一套 Handler

本篇文章咱们将深入 Native 层,一起来探求 Looper#loop() 为什么不会卡死主线程背面的原因

以下,enjoy:

Android组件系列:再谈Handler机制(Native篇)

一、开篇

从 Android 2.3 开端,Google 把 Handler 的堵塞/唤醒计划从 Object#wait() / notify(),改成了用 Linux epoll 来完结

原因是 Native 层也引入了一套音讯管理机制,用于供给给 C/C++ 开发者运用,而现有的堵塞/唤醒计划是为 Java 层预备的,只支撑 Java

Native 希望能够像 Java 相同:main 线程在没有音讯时进入堵塞状况,有到期音讯需求履行时,main 线程能及时醒过来处理。怎样办?有两种挑选

要么,持续运用 Object#wait() / notify( ),Native 向音讯行列增加新音讯时,告诉 Java 层自己需求什么时分被唤醒

要么,在 Native 层从头完结一套堵塞/唤醒计划,弃用 Object#wait() / notify() ,Java 经过 jni 调用 Native 进入堵塞态

结局咱们都知道了,Google 挑选了后者

其实假如仅仅将 Java 层的堵塞/唤醒移植到 Native 层,倒也不用祭出 epoll 这个大杀器 ,Native 调用 pthread_cond_wait 也能到达相同的效果

挑选 epoll 的另一个原因是, Native 层支撑监听 自定义 Fd比如 Input 事情便是经过 epoll 监听 socketfd 来完结将事情转发到 APP 进程的),而一旦有监听多个流事情的需求,那就只能运用 Linux I/O 多路复用技术

了解 I/O多路复用之epoll

说了这么多,那到底什么是 epoll

epoll 全称 eventpoll,是 Linux I/O 多路复用的其间一个完结,除了 epoll 外,还有 selectpoll ,咱们这只评论 epoll

要了解 epoll ,咱们首要需求了解什么是 "流"

在 Linux 中,任何能够进行 I/O 操作的目标都能够看做是流,一个 文件socketpipe,咱们都能够把他们看作流

接着咱们来评论流的 I/O 操作,经过调用 read() ,咱们能够从流中读出数据;经过 write() ,咱们能够往流 写入数据

现在假定一个情形,咱们需求从流中读数据,可是流中还没有数据

int socketfd = socket();
connect(socketfd,serverAddr);
int n = send(socketfd,'在吗');
n = recv(socketfd); //等候承受服务器端 发过来的信息
...//处理服务器回来的数据

一个典型的比如为,客户端要从 socket 中读数据,可是服务器还没有把数据传回来,这时分该怎样办?

  • 堵塞: 线程堵塞到 recv() 办法,直到读到数据后再持续向下履行
  • 非堵塞: recv() 办法没读到数据立刻回来 -1 ,用户线程依照固定间隔轮询 recv() 办法,直到有数据回来

好,现在咱们有了堵塞非堵塞两种解决计划,接着咱们一起建议100个网络恳求,看看这两种计划各自会怎样处理

先说堵塞形式,在堵塞形式下,一个线程一次只能处理一个流的 I/O 事情,想要一起处理多个流,只能运用 多线程 + 堵塞 I/O 的计划。可是,每个 socket 对应一个线程会形成很大的资源占用尤其是关于长衔接来说,线程资源一向不会开释,假如后边陆续有很多衔接的话,很快就会把机器的内存跑完

非堵塞形式下,咱们发现 单线程能够一起处理多个流了只需不断的把所有流自始至终的访问一遍,就能够得知哪些流有数据回来值大于-1),但这样的做法效率也不高,由于假如所有的流都没有数据,那么只会白白糟蹋 CPU

发现问题了吗?只有堵塞非堵塞这两种计划时,一旦有监听多个流事情的需求,用户程序只能挑选,要么糟蹋线程资源(堵塞型 I/O要么糟蹋 CPU 资源(非堵塞型 I/O,没有其他更高效的计划

而且,这个问题在用户程序端是无解的,必须让内核创立某种机制,把这些流的监听事情接管曩昔,由于任何事情都必须经过内核读取转发,内核总是能在第一时刻知晓事情产生

这种能够让用户程序具有 “一起监听多个流读写事情” 的机制,就被称为 I/O 多路复用!

然后咱们来看 epoll 供给的三个函数:

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • epoll_create() 用于创立一个 epoll

  • epoll_ctl() 用来履行 fd“增删改” 操作,终究一个参数 event 是告诉内核 需求监听什么事情。仍是以网络恳求举例, socketfd 监听的便是 可读事情,一旦接收到服务器回来的数据,监听 socketfd 的目标将会收到 回调告诉,表明 socket 中有数据能够读了

  • epoll_wait()运用户线程堵塞 的办法,它的第二个参数 events 承受的是一个 调集目标,假如有多个事情一起产生,events 目标能够从内核得到产生的事情的调集

了解 Linux eventfd

了解了 epoll 咱们再来看 Linux eventfdeventfd 是专门用来传递事情的 fd ,它供给的功用也十分简略:累计计数

int efd = eventfd();
write(efd, 1);//写入数字1
write(efd, 2);//再写入数字2
int res = read(efd);
printf(res);//输出值为 3

经过 write() 函数,咱们能够向 eventfd 中写入一个 int 类型的值,而且,只需没有产生 操作,eventfd 中保存的值将会一向累加

经过 read() 函数能够将 eventfd 保存的值读了出来,而且,在没有新的值参加之前,再次调用 read() 办法会产生堵塞,直到有人从头向 eventfd 写入值

eventfd 完结的是计数的功用,只需 eventfd 计数不为 0 ,那么表明 fd 是可读的。再结合 epoll 的特性,咱们能够十分轻松的创立出 生产者/顾客模型

epoll + eventfd 作为顾客大部分时分处于堵塞休眠状况,而一旦有恳求入队(eventfd 被写入值),顾客就立刻唤醒处理,Handler 机制的底层逻辑便是运用 epoll + eventfd

好,有了 epolleventfd 基础,接下来咱们开端正式进入 Handler 机制的 Native 国际

二、进入Native Handler

绝大多数 Android 工程师都或多或少的了解过 Handler 机制,所以关于 Handler 的根本运用和完结的原理咱们就不过多赘述了,直奔主题

咱们来重点关注 MessageQueue 类中的几个 jni 办法:nativeInit()nativePollOnce()nativeWake()

它们别离对应了 Native 音讯行列中的 初始化音讯行列音讯的循环与堵塞 以及 音讯的分送与唤醒 这三大环节

/frameworks/base/core/java/android/os/MessageQueue.java
class MessageQueue {
    private native static long nativeInit();
    private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
    private native static void nativeWake(long ptr);
}

音讯行列的初始化

先来看第一步,音讯行列的初始化流程

Java MessageQueue 构造函数中会调用 nativeInit() 办法,同步在 Native 层也会创立一个音讯行列 NativeMessageQueue 目标,用于保存 Native 开发者发送的音讯

/frameworks/base/core/java/android/os/MessageQueue.java
MessageQueue(boolean quitAllowed) {
    mQuitAllowed = quitAllowed;
    mPtr = nativeInit();
}

看代码,在 NativeMessageQueue 的构造函数中,触发创立 Looper 目标Native 层的

/frameworks/base/core/jni/android_os_MessageQueue.cpp
class android_os_MessageQueue {
    void android_os_MessageQueue_nativeInit() {
        NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
    }
    NativeMessageQueue() {
        mLooper = Looper::getForThread();
        if (mLooper == NULL) {
            mLooper = new Looper(false);
            Looper::setForThread(mLooper);
        }
    }
}

Native 创立 Looper 目标的处理逻辑和 Java 相同:先去 线程部分存储区 获取 Looper 目标,假如为空,创立一个新的 Looper 目标并保存到 线程部分存储区

咱们持续,接着来看 Native Looper 初始化流程

/system/core/libutils/Looper.cpp
class looper {
    Looper::Looper() {
        int mWakeEventFd = eventfd();
        rebuildEpollLocked();
    }
    void rebuildEpollLocked(){
        int mEpollFd = epoll_create();//哎,这儿十分重要,在 Looper 初始化时创立了 epoll 目标
        epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);//把用于唤醒音讯行列的eventfd 增加到 epoll 池
    }
}

要害的当地来了!!!

Looper 的构造函数首要创立了 eventfd 目标 :mWakeEventFd,它的效果便是用来监听 MessageQueue 是否有新音讯参加,这个目标十分重要,一定要记住它!

随后调用的 rebuildEpollLocked() 办法中,又创立了 epoll 目标:mEpollFd,并将刚刚申请的 mWakeEventFd 注册到 epoll

到这一步,Handler 机制最依赖的两大核心目标 mEpollFdmWakeEventFd ,悉数都初始化成功!

咱们来整理一下 音讯行列的初始化 过程:

  1. Java 层初始化音讯行列时,同步调用 nativeInit() 办法,在 native 层创立了一个 NativeMessageQueue 目标
  2. Native 层的音讯行列被创立的一起,也会创立一个 Native Looper 目标
  3. Native Looper 构造函数中,调用 eventfd() 生成 mWakeEventFd,它是后续用于唤醒音讯行列的核心
  4. 终究调用 rebuildEpollLocked() 办法,初始化了一个 epoll 实例 mEpollFd ,然后将 mWakeEventFd 注册到 epoll

至此,Native 层的音讯行列初始化完结,Looper 目标持有 mEpollFdmWakeEventFd 两大金刚

音讯的循环与堵塞

Java 和 Native 的音讯行列都创立完今后,整个线程就会堵塞到 Looper#loop() 办法中,在 Java 层的的调用链大致是这样的:

Looper#loop()
    -> MessageQueue#next()
        -> MessageQueue#nativePollOnce()
}

MessageQueue 终究一步调用的 nativePollOnce() 是一个 jni 办法,具体完结在 Native 层

咱们接着往下跟,看看 Native 中做了些什么

/frameworks/base/core/jni/android_os_MessageQueue.cpp
class android_os_MessageQueue {
    //jni办法,转到 NativeMessageQueue#pollOnce()
    void android_os_MessageQueue_nativePollOnce(){
        nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
    }
    class NativeMessageQueue : MessageQueue {
        /转到 Looper#pollOnce() 办法
        void pollOnce(){
            mLooper->pollOnce(timeoutMillis);
        }
    }
}

nativePollOnce() 承受到恳求后,顺手转发到 NativeMessageQueue 的 pollOnce() 办法

NativeMessageQueue#pollOnce() 中什么都没做,仅仅又把恳求转发给了 Looper#pollOnce()

看来首要的逻辑都在 Looper 中,咱们接着往下看

//system/core/libutils/Looper.cpp
class looper {
    int pollOnce(int timeoutMillis){
        int result = 0;
        for (;;) {
            if (result != 0) {
                return result;
            }
            result = pollInner(timeoutMillis);//超时
        }
    }
    int pollInner(int timeoutMillis){
        int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);//调用 epoll_wait() 等候事情的产生
    }
}

看到了吗?线程堵塞和唤醒的履行逻辑都在这!

pollOnce() 会不断的轮询 pollInner() 办法,查看它的的回来值 result

这儿的 result 类型是在 Looper.h 文件中声明的枚举类,一共有4种结果:

  • -1 表明在 “超时时刻到期” 之前运用 wake() 唤醒了轮询,通常是有需求立刻履行的新音讯参加了行列
  • -2 表明多个事情一起产生,有可能是新音讯参加,也有可能是监听的 自定义 fd 产生了 I/O 事情
  • -3 表明设定的超时时刻到期了
  • -4 表明过错,不知道哪里会用到

音讯行列中没音讯,或许 设定的超时时刻没到期,再或许 自定义 fd 没有事情产生,都会导致线程堵塞到 pollInner() 办法调用

pollInner() 中,则是运用了 epoll_wait() 体系调用等候事情的产生

本末节标题是 音讯的循环与堵塞 ,现在线程现已堵塞到 pollInner() ,咱们能够来整理下产生堵塞的前后逻辑:

音讯行列在初始化成功今后,Java 层的 Looper#loop() 会开端无限轮询,不断的获取下一条音讯。假如音讯行列为空,调用 epoll_wait 使线程进入到堵塞态,让出 CPU 调度

从 Java 到 Native 整个调用流程大致是这样的:

Looper#loop()
    -> MessageQueue#next()
        -> MessageQueue#nativePollOnce()
            -> NativeMessageQueue#pollOnce() //注意,进入 Native 层
                -> Looper#pollOnce()
                    -> Looper#pollInner()
                        -> epoll_wait()

音讯的发送/唤醒机制

好,现在的音讯行列里边是空的,而且经过上一末节的剖析后,咱们发现用户线程堵塞到了 native 层的 Looper#pollInner() 办法,咱们来向音讯行列发送一条音讯唤醒它

前面咱们说了,Java 和 Native 都各自维护了一套音讯行列,所以他们发送音讯的进口也不相同

Java 开发运用 Handler#sendMessage() / post(),C/C++ 开发运用 Looper#sendMessage()

咱们先来看 Java

/frameworks/base/core/java/android/os/Handler.java
class Handler {
    boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        return queue.enqueueMessage(msg, uptimeMillis);
    }
}
/frameworks/base/core/java/android/os/MessageQueue.java
class MessageQueue {
    boolean enqueueMessage(Message msg, long when) {
        //...依照到期时刻将音讯刺进音讯行列
        if (needWake) {
            nativeWake(mPtr);
        }
    }
}

在运用 Handler 发送音讯时,不论调用的是 sendMessage 仍是 post,终究都是调用到 MessageQueue#enqueueMessage() 办法将音讯入列,入列的次序是依照履行时刻先后排序

假如咱们发送的音讯需求立刻被履行,那么将 needWake 变量置为 true,接着运用 nativeWake() 唤醒线程

注:nativeWake() 办法也是 jni 调用,经过层层转发终究调用到 Native Looper 中的 wake() 办法,整个转发过程的调用链明晰而且十分简略,这儿就不展开剖析了

Java 发送音讯的办法聊完了,然后咱们看 Native 层怎样发送音讯

/system/core/libutils/Looper.cpp
class looper {
    void Looper::sendMessageAtTime(uptime, handler,message) {
        int i = 0;
        int messageCount = mMessageEnvelopes.size();
        while (i < messageCount && uptime >= mMessageEnvelopes.itemAt(i).uptime) {
            i += 1;
        }
        mMessageEnvelopes.insertAt(messageEnvelope(uptime, handler, message), i, 1);
        // Wake the poll loop only when we enqueue a new message at the head.
        if (i == 0) {
            wake();
        }
    }
}

看上面的代码,Native 层经过 sendMessageAtTime() 办法向音讯行列发送音讯,增加音讯的处理逻辑和 Java 处理逻辑相似:

依照时刻的先后次序增加到 mMessageEnvelopes 调集中,履行时刻离得最近的音讯被放在前面,假如发现需求唤醒线程,则调用 wake() 办法

好,Java 和 Native 发送音讯的办法都介绍完了

咱们发现,尽管它俩 发音讯的办法 、音讯类型 、 送达的音讯行列 都不相同,可是,当需求唤醒线程时Java 和 Native 都会履行到 Looper#wake() 办法

之前咱们说 “Handler 机制的底层是 epoll + eventfd

读者朋友无妨大胆猜一下,这儿的线程是怎样被唤醒的?

/system/core/libutils/Looper.cpp
class looper {
    void Looper::wake() {
        int inc = 1;
        write(mWakeEventFd, &inc);
    }
}

答案十分简略,write() 一行办法调用,向 mWakeEventFd 写入了一个 1(小提示,mWakeEventFd 的类型是 eventfd

为什么 mWakeEventFd 写入了一个 1,线程就能够被唤醒呢???

mWakeEventFd 被写入值后,状况会从 不可读 变成 可读,内核监听到 fd可读写状况产生变化,会将事情从内核回来给 epoll_wait() 办法调用

epoll_wait() 办法一旦回来,堵塞态将会被撤销,线程持续向下履行

好,咱们来总结一下 音讯的发送与唤醒 中几个要害的过程:

  1. Java 层发送音讯,调用 MessageQueue#enqueueMessage() 办法,假如音讯需求立刻履行,那么调用 nativeWake() 履行唤醒
  2. Native 层发送音讯,调用 Looper#sentMessageAtTime() 办法,处理逻辑与 Java 相似,假如需求唤醒线程,调用 Looper#wake()
  3. Looper#wake() 唤醒办法很简略,向 mWakeEventFd 写入 1
  4. 初始化行列 时为 mWakeEventFd 注册了 epoll 监听,所以一旦有来自于 mWakeEventFd 的新内容, epoll_wait() 堵塞调用就会回来,这儿就现已起到了唤醒行列的效果

呼~ 到这儿 音讯的发送与唤醒 的流程根本上完毕了,接下来是 Handler 的重头戏:线程唤醒后的音讯分发处理

唤醒后音讯的分发处理

线程在没有音讯需求处理时会堵塞在 Looper 中的 pollInner() 办法调用,线程唤醒今后同样也是在 pollInner() 办法中持续履行

线程醒来今后,先判别自己为什么醒过来,再依据唤醒类型履行不同的逻辑

pollInner() 办法稍微有点长,大致能够分为5步来看,过程我作了标记,咱们一点点来捋

/system/core/libutils/Looper.cpp
class looper {
    int pollInner(int timeoutMillis){
        int result = POLL_WAKE;
        // step 1,epoll_wait 办法回来
        int eventCount = epoll_wait(mEpollFd, eventItems, timeoutMillis); 
        if (eventCount == 0) { // 事情数量为0表明,到达设定的超时时刻
            result = POLL_TIMEOUT;
        }
        for (int i = 0; i < eventCount; i++) {
            if (eventItems[i] == mWakeEventFd) {
                // step 2 ,清空 eventfd,使之从头变为可读监听的 fd
                awoken();
            } else {
                // step 3 ,保存自定义fd触发的事情调集
                mResponses.push(eventItems[i]);
            }
        }
        // step 4 ,履行 native 音讯分发
        while (mMessageEnvelopes.size() != 0) {
            if (messageEnvelope.uptime <= now) { // 查看音讯是否到期
                messageEnvelope.handler->handleMessage(message);
            }
        }
        // step 5 ,履行 自定义 fd 回调
        for (size_t i = 0; i < mResponses.size(); i++) {
            response.request.callback->handleEvent(fd, events, data);
        }
        return result;
    }
    void awoken() {
        read(mWakeEventFd) ;// 从头变成可读事情
    }
}

step 1 : epoll_wait 办法回来阐明有事情产生,回来值 eventCount 是产生事情的数量。假如为0,表明到达设定的超时时刻,下面的判别逻辑都不会走,不为0,那么咱们开端遍历内核回来的事情调集 eventItems,依据类型履行不同的逻辑

step 2 : 假如事情类型是音讯行列的 eventfd ,阐明有人向音讯行列提交了需求立刻履行的音讯,咱们只需把音讯行列的 eventfd 数据读出来,使他从头变成能够触发 可读事情fd,然后等候办法完毕就行了

step 3 : 事情不是音讯行列的 eventfd ,阐明有其他当地注册了监听 fd,那么,咱们将产生的事情保存到 mResponses 调集中,待会需求对这个事情做出呼应,告诉注册目标

step 4 : 遍历 Native 的音讯调集 mMessageEnvelopes,查看每个音讯的到期时刻,假如音讯到期了,交给 handler 履行分发,分发逻辑参阅 Java Handler

step 5 : 遍历 mResponses 调集,把其他当地注册的 自定义 fd 消费掉,呼应它们的回调办法

唤醒后履行的逻辑仍是十分复杂的,咱们总结一下:

用户线程被唤醒后,优先分发 Native 层的音讯,紧接着,告诉 自定义 fd 产生的事情(假如有的话),终究 pollInner() 办法完毕,回来到 Java 层 Looper#loop() 办法履行到 Java 层的音讯分发。只有当 Java Handler 履行完音讯分发,一次 loop() 循环才算是完结

再之后,由于 Looper#loop() 是死循环,所以会立刻再一次进入循环,持续调用 next() 办法获取音讯、堵塞到 pollInner() 、从 pollInner() 唤醒履行分发,履行完毕接着进入下一次循环,无尽的轮回

main 线程的一生都将重复这一流程,直到 APP 进程完毕运行..

三、结语

以上便是 Handler Native 篇的悉数内容,首要介绍了 Java MessageQueue 中几个要害的 jni 办法在底层是怎样完结的

将悉数的代码逻辑剖析完今后,咱们会发现 Native Handler 的完结不算复杂,要害的堵塞与唤醒部分是凭借了 Linux 体系 epoll 机制来完结的

所以,咱们只需了解了 epoll 机制,再对照源码看看 Looper#pollInner() 中的内部逻辑,就能理解整个 Handler 机制是怎样一回事了

本篇文章到这儿就完毕了,希望能对我们有帮助

全文完

四、参阅资料

  • Scalable Event Multiplexing: epoll vs. kqueue
  • epoll 或许 kqueue 的原理是什么?- 知乎 – 蓝形参的答复
  • Android 音讯机制Native层音讯机制 – 吴迪
  • Linux 网络编程的5种IO模型:堵塞IO与非堵塞IO