Handler epoll机制(Native)

前言

从 Android 2.3 开始,Google 把 Handler 的阻塞/唤醒方案从 Object#wait() / notify(),改成了用 Linux epoll 来实现

原因是 Native 层也引入了一套消息管理机制,用于提供给 C/C++ 开发者使用,而现有的阻塞/唤醒方案是为 Java 层准备的,只支持 Java

简述

一、 I/O多路复用: epoll

epoll 提供的三个函数:

//  用于创建一个 epoll 池
int epoll_create(int size);
// 用来执行 fd 的 “增删改” 操作,最后一个参数 event 是告诉内核 需要监听什么事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 使用户线程阻塞的方法,它的第二个参数 events 接受的是一个集合对象,
// 如果有多个事件同时发生,events 对象可以从内核得到发生的事件的集合
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

二、 Linux的eventfd

eventfd 是专门用来传递事件的 fd ,它提供的功能也非常简单:累计计数

理解write() 函数与read() 函数:

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 计数不为 0 ,那么表示 fd 是可读的。再结合 epoll 的特性,我们可以非常轻松的创建出 生产者/消费者模型


Handler 机制的底层逻辑就是利用 epoll + eventfd,其中消费者大部分时候处于阻塞休眠状态,而一旦有请求入队(eventfd 被写入值),消费者就立刻唤醒处理,

流程

一、java层中MessageQueue 类中的几个 jni 方法:

nativeInit()、nativePollOnce() 和 nativeWake()

/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() 方法

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

在native层会创建一个消息队列 NativeMessageQueue 对象,用于保存 Native 开发者发送的消息,并且在NativeMessageQueue 构造函数中会创建一个native层的Looper对象

/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 初始化流程

/system/core/libutils/Looper.cpp
class looper {

    Looper::Looper() {
       // 重头戏:mWakeEventFd 是用来监听 MessageQueue 是否有新消息加入
        int mWakeEventFd = eventfd();
        rebuildEpollLocked();
    }

    void rebuildEpollLocked(){
        // 重头戏:在 Looper 初始化时创建了 epoll 对象
        int mEpollFd = epoll_create();
        // 把用于唤醒消息队列的eventfd 添加到 epoll 池
        epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);
    }

}

消息队列初始化总结:
重点:mWakeEventFd 与 mEpollFd
1、Looper 的构造函数首先创建了 eventfd 对象 :mWakeEventFd,作用是用来监听 MessageQueue 是否有新消息加入
2、 Looper 初始化时创建了 epoll 对象:mEpollFd
3、把mWakeEventFd 添加到epoll池中

三、消息循环与阻塞

Java层MessageQueue的next方法中会调用 nativePollOnce() 方法。
链路如下:
Looper#loop() -> MessageQueue#next() -> MessageQueue#nativePollOnce()

在native层的nativePollOnce()调用NativeMessageQueue中的pollOnce()方法,最终会进入Looper的poollPnce()方法

/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);
        }
    }
}

接着我们看native中Looper里的pollOnce()方法

//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() 等待事件的产生
    }
}

消息循环与阻塞总结:
整体链路如下:
Looper#loop() -> MessageQueue#next() -> MessageQueue#nativePollOnce()
->NativeMessageQueue#pollOnce() -> Looper#pollOnce()-> Looper#pollInner() -> epoll_wait()
消息队列在初始化成功以后,Java 层的 Looper#loop() 会开始无限轮询,不停的获取下一条消息。如果消息队列为空,调用 epoll_wait 使线程进入到阻塞态,让出 CPU 调度

四、消息的发送/唤醒机制

Java层MessageQueue的enqueueMessage方法中会调用 nativewake() 方法。
链路如下:
Handler #enqueueMessage() -> MessageQueue#enqueueMessage() -> MessageQueue#nativeWake()

Java 和 Native 都各自维护了一套消息队列,所以他们发送消息的入口也不一样
1、Java 开发使用 Handler#sendMessage() / post()
2、C/C++ 开发使用 Looper#sendMessage()

先看Java层:
Handler 发送消息时,不管调用的是 sendMessage 还是 post,最后都是调用到 MessageQueue#enqueueMessage() 方法将消息入列,入列的顺序是按照执行时间先后排序

注:如果我们发送的消息需要马上被执行,那么将 needWake 变量置为 true,接着使用 nativeWake() 唤醒线程

/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);
        }
    }

}

在native层的nativeWake()调用NativeMessageQueue中的wake()方法,最终会进入Looper的wake()方法

/frameworks/base/core/jni/android_os_MessageQueue.cpp
class android_os_MessageQueue {

    // jni方法,转到 NativeMessageQueue#wake()
    void android_os_MessageQueue_nativeWake(){
        nativeMessageQueue->wake();
    }
    class NativeMessageQueue : MessageQueue {

        // 转到 Looper#wake() 方法
        void wake(){
            mLooper->wake();
        }
    }
}

接着我们看native中Looper里的wake()方法

/system/core/libutils/Looper.cpp
class looper {

    void Looper::wake() {
        int inc = 1;
        write(mWakeEventFd, &inc);
    }
}

总结java层唤醒:
链路如下:
Handler #enqueueMessage() -> MessageQueue#enqueueMessage() -> MessageQueue#nativeWake()->NativeMessageQueue#wake() -> Looper#wake() -> write()

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

    void Looper::wake() {
        int inc = 1;
        write(mWakeEventFd, &inc);
    }
}

总结唤醒:
java与native发消息的方式 、消息类型 、 送达的消息队列 都不相同,但是当需要唤醒线程时,Java 和 Native 都会执行到 Looper#wake() 方法

线程如何被唤醒?为什么这样的操作会唤醒?
1、write() 一行方法调用,向 mWakeEventFd 写入了一个 1则会唤醒。
2、mWakeEventFd 被写入值后,状态会从 不可读 变成 可读,内核监听到 fd 的可读写状态发生变化,会将事件从内核返回给 epoll_wait() 方法调用
而 epoll_wait() 方法一旦返回,阻塞状态将会被取消,线程继续向下执行

总结

重点: java层的looper、messageQueue在c++层均有对应的类,然后通过将looper初始化时创建的 eventFd 返回的 wakeEventFd,注册到 epoll_create创建的 epoll对象里,然后通过epoll_ctl去在wakeEventFd上添加一个 ADD类型的事件监听,最后上层 调用nativePollOnce的时候,最终会调用 epoll 的 epoll_wait 通过监听fd的方式来形成阻塞。来消息后,去往wakeEventFd去写一个“1”值,此后epoll_wait监听到值了,就阻塞解除继续执行了,继续取message执行去了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容