Android 消息处理

Looper循环中,如果messageQueue没有消失,还会一直循环下去吗

这个问题涉及linuex里面的pipe(管道)和epoll机制,

先给出答案:不会一直循环下去,阻塞起来

首先说下pipe

pipe:中文意思是管道,使用I/O流操作,实现跨进程通信,管道的一端的读,另一端写,标准的生产者消费者模式

下面说下5种I/O模型 参考大话 Select、Poll、Epoll https://cloud.tencent.com/developer/article/1005481

[1] blocking IO - 阻塞IO [2] nonblocking IO - 非阻塞IO [3] IO multiplexing - IO多路复用 [4] signal driven IO - 信号驱动IO [5] asynchronous IO - 异步IO

其中前面4种IO都可以归类为synchronous IO - 同步IO,在介绍select、poll、epoll之前,首先介绍一下这几种IO模型,signal driven IO平时用的比较少,这里就不介绍了。

1. IO - 同步、异步、阻塞、非阻塞

下面以network IO中的read读操作为切入点,来讲述同步(synchronous) IO和异步(asynchronous) IO、阻塞(blocking) IO和非阻塞(non-blocking)IO的异同。一般情况下,一次网络IO读操作会涉及两个系统对象:(1) 用户进程(线程)Process;(2)内核对象kernel,两个处理阶段:

[1] Waiting for the data to be ready - 等待数据准备好 [2] Copying the data from the kernel to the process - 将数据从内核空间的buffer拷贝到用户空间进程的buffer

IO模型的异同点就是区分在这两个系统对象、两个处理阶段的不同上。

1.1 同步IO 之 Blocking IO

Android 消息处理_第1张图片
image

官方描述: 如上图所示,用户进程process在Blocking IO读recvfrom操作的两个阶段都是等待的。在数据没准备好的时候,process原地等待kernel准备数据。kernel准备好数据后,process继续等待kernel将数据copy到自己的buffer。在kernel完成数据的copy后process才会从recvfrom系统调用中返回。

本人描述: 用户进程向内核对象请求数据,如果内核对象数据没准备好,则用户进程一直等下去,直到内核将数据准备好,并且复制到用户进程

1.2 同步IO 之 NonBlocking IO

Android 消息处理_第2张图片
image

官方描述: 从图中可以看出,process在NonBlocking IO读recvfrom操作的第一个阶段是不会block等待的,如果kernel数据还没准备好,那么recvfrom会立刻返回一个EWOULDBLOCK错误。当kernel准备好数据后,进入处理的第二阶段的时候,process会等待kernel将数据copy到自己的buffer,在kernel完成数据的copy后process才会从recvfrom系统调用中返回。

本人描述:实际上就是用户进程不断轮寻,看内核进程的数据是否准备好,当然第二阶段(kernel将数据copy到自己的buffer)用户进程是需要等待的

1.3 同步IO 之 IO multiplexing

Android 消息处理_第3张图片
image

IO多路复用,就是我们熟知的select、poll、epoll模型。从图上可见,在IO多路复用的时候,process在两个处理阶段都是block住等待的。初看好像IO多路复用没什么用,其实select、poll、epoll的优势在于可以以较少的代价来同时监听处理多个IO。

1.4 异步IO

Android 消息处理_第4张图片
image

从上图看出,异步IO要求process在recvfrom操作的两个处理阶段上都不能等待,也就是process调用recvfrom后立刻返回,kernel自行去准备好数据并将数据从kernel的buffer中copy到process的buffer在通知process读操作完成了,然后process在去处理。遗憾的是,linux的网络IO中是不存在异步IO的,linux的网络IO处理的第二阶段总是阻塞等待数据copy完成的。真正意义上的网络异步IO是Windows下的IOCP(IO完成端口)模型。

Android 消息处理_第5张图片
image

很多时候,我们比较容易混淆non-blocking IO和asynchronous IO,认为是一样的。但是通过上图,几种IO模型的比较,会发现non-blocking IO和asynchronous IO的区别还是很明显的,non-blocking IO仅仅要求处理的第一阶段不block即可,而asynchronous IO要求两个阶段都不能block住。

select与epoll

阻塞I/O模式下,一个线程只能处理一个流的I/O事件。如果想要同时处理多个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。

我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了,但这样的做法显然不好,因为如果所有的流都没有数据,那么只会白白浪费CPU。这里要补充一点,阻塞模式下,内核对于I/O事件的处理是阻塞或者唤醒,而非阻塞模式下则把I/O事件交给其他对象(后文介绍的select以及epoll)处理甚至直接忽略。

为了避免CPU空转,可以引进了一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不过两者的本质是一样的)。这个代理比较厉害,可以同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流(于是我们可以把“忙”字去掉了)

于是,如果没有I/O事件产生,我们的程序就会阻塞在select处。但是依然有个问题,我们从select那里仅仅知道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。

但是使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,没一次无差别轮询时间就越长。再次

说了这么多,终于能好好解释epoll了

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

epoll与select/poll的区别

 select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪,能够通知程序进行相应的操作。

 select的本质是采用32个整数的32位,即32*32= 1024来标识,fd值为1-1024。当fd的值超过1024限制时,就必须修改FD_SETSIZE的大小。这个时候就可以标识32*max值范围的fd。

 poll与select不同,通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只需要被初始化一次。

 epoll还是poll的一种优化,返回后不需要对所有的fd进行遍历,在内核中维持了fd的列表。select和poll是将这个内核列表维持在用户态,然后传递到内核中。与poll/select不同,epoll不再是一个单独的系统调用,而是由epoll_create/epoll_ctl/epoll_wait三个系统调用组成,后面将会看到这样做的好处。epoll在2.6以后的内核才支持。

select/poll的几大缺点:

1、每次调用select/poll,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

2、同时每次调用select/poll都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

3、针对select支持的文件描述符数量太小了,默认是1024

为什么epoll相比select/poll更高效

 传统的poll函数相当于每次调用都重起炉灶,从用户空间完整读入ufds,完成后再次完全拷贝到用户空间,另外每次poll都需要对所有设备做至少做一次加入和删除等待队列操作,这些都是低效的原因。

 epoll的解决方案中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。select, poll和epoll都是使用waitqueue调用callback函数去wakeup你的异步等待线程的,如果设置了timeout的话就起一个hrtimer,select和poll的callback函数并没有做什么事情,但epoll的waitqueue callback函数把当前的有效fd加到ready list,然后唤醒异步等待进程,所以epoll函数返回的就是这个ready list, ready list中包含所有有效的fd,这样一来kernel不用去遍历所有的fd,用户空间程序也不用遍历所有的fd,而只是遍历返回有效fd链表。

epoll的调用

1. int epoll_create(int size);

创建一个epoll的句柄,size用来告诉内核需要监听的数目一共有多大。当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close() 关闭,否则可能导致fd被耗尽。

2.int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll的事件注册函数,第一个参数是 epoll_create() 的返回值,第二个参数表示动作,使用如下三个宏来表示:

EPOLL_CTL_ADD //注册新的fd到epfd中;

EPOLL_CTL_MOD //修改已经注册的fd的监听事件;

EPOLL_CTL_DEL //从epfd中删除一个fd;

第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event 结构如下:

typedef union epoll_data

{

void *ptr;

int fd;

__uint32_t u32;

__uint64_t u64;

} epoll_data_t;

struct epoll_event {

__uint32_t events; /* Epoll events */

epoll_data_t data; /* User data variable */

};

events 可以是以下几个宏的集合:

EPOLLIN //表示对应的文件描述符可以读(包括对端SOCKET正常关闭);

EPOLLOUT //表示对应的文件描述符可以写;

EPOLLPRI //表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

EPOLLERR //表示对应的文件描述符发生错误;

EPOLLHUP //表示对应的文件描述符被挂断;

EPOLLET //将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。

EPOLLONESHOT//只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。

示例代码

int pipe_fd[2];

int pipe_fd1[2];

/**

  • 调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端和一个写端,

  • 然后通过fd参数传出给用户程序两个文件描述符,fd[0]指向管道的读端,fd[1]指向管道的写端(

  • 很好记,就像0是标准输入1是标准输出一样)。

  • 所以管道在用户程序看起来就像一个打开的文件,

  • 通过read(fd[0])或者write(fd[1])向这个文件读写数据其实是在读写内核缓冲区。

  • pipe函数调用成功返回0,调用失败返回-1。

*/

if ((ret = pipe(pipe_fd)) < 0) {

cout << "create pipe fail:" << ret << ",errno:" << errno << endl;

return -1;

}

if ((ret = pipe(pipe_fd1)) < 0) {

cout << "create pipe1 fail:" << ret << ",errno:" << errno << endl;

return -1;

}

struct epoll_event ev, ev1; //事件临时变量

// ev.data.fd = pipe_fd[0]; //设置监听文件描述符

ev.events = EPOLLET | EPOLLIN; //设置要处理的事件类型

ev1.events = EPOLLET | EPOLLIN; //设置要处理的事件类型

int epfd = epoll_create(MAXEVENTS);

ret = epoll_ctl(epfd, EPOLL_CTL_ADD, pipe_fd[0], &ev);

ret = epoll_ctl(epfd, EPOLL_CTL_ADD, pipe_fd1[0], &ev1);

这里面实际上监听了两个I/O管道(pipe_fd和pipe_fd1)

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

参数events用来从内核得到事件的集合,maxevents 告之内核这个events有多大,这个 maxevents 的值不能大于创建 epoll_create() 时的size,参数 timeout 是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

EPOLL事件有两种模型 Level Triggered (LT) 和 Edge Triggered (ET):

LT(level triggered,水平触发模式)是缺省的工作方式,并且同时支持 block 和 non-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。

ET(edge-triggered,边缘触发模式)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,等到下次有新的数据进来的时候才会再次出发就绪事件。

举个例

struct epoll_event ev //事件临时变量

ev.data.fd = pipe_fd[0]; //设置监听文件描述符

ev.events =EPOLLET|EPOLLIN; //设置要处理的事件类型

int epfd = epoll_create(MAXEVENTS);

ret = epoll_ctl(epfd, EPOLL_CTL_ADD, pipe_fd[0], &ev);

for (;;) {

sleep(2);

int count = epoll_wait(epfd, events, MAXEVENTS, -1);

printf("count is %d\n", count);

char r_buf[100];

// for (int i = 0; i < count; i++) {

// if ((events[i].data.fd == pipe_fd[0])

// && (events[i].events & EPOLLIN)) {

// int r_num = read(fd[i], r_buf, 100);

// printf(

// "read num is %d bytes data from the pipe,value is %d \n",

// r_num, atoi(r_buf));

// }

// }

}

这段代码是ET(edge-triggered,边缘触发模式), epoll_wait阻塞, 那么当管道另一端有写数据时,printf("count is %d\n", count)会执行一次;

如果把ET去掉,默认是LT(level triggered,水平触发模式),那么那么当管道另一端有写数据时,printf("count is %d\n", count)会一直执行下, 直到读取数据

即把for(int i = 0; i < count; i++)代码片段打开

思考,如果messagequeue里面没有message,那么looper会阻塞,相当于主线程阻塞

1.那么点击事件是怎么传入到主线程呢

首先上面说loop之所有会阻塞,是因为epoll机制,代码位于Loop.cpp中的pollOnce方法,所以要让主线程接触阻塞,必须要往管道里面write,那么当点击屏幕时

谁来往当前app的主线程的管道wrtite呢,看看调用栈

"main@4011" prio=5 runnable

java.lang.Thread.State: RUNNABLE

at android.os.MessageQueue.enqueueMessage(MessageQueue.java:534)

at android.os.Handler.enqueueMessage(Handler.java:631)

at android.os.Handler.sendMessageAtTime(Handler.java:600)

at android.os.Handler.sendMessageDelayed(Handler.java:570)

at android.os.Handler.postDelayed(Handler.java:398)

at android.view.View.postDelayed(View.java:13011)

at android.view.View.onTouchEvent(View.java:10370)

at android.widget.TextView.onTouchEvent(TextView.java:8300)

at android.view.View.dispatchTouchEvent(View.java:9300)

at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2553)

at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2197)

at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2553)

at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2197)

at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2553)

at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2197)

at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2553)

at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2197)

at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2553)

at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2197)

at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2553)

at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2197)

at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2553)

at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2197)

at com.android.internal.policy.PhoneWindow$DecorView.superDispatchTouchEvent(PhoneWindow.java:2403)

at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1737)

at android.app.Activity.dispatchTouchEvent(Activity.java:2771)

at com.android.internal.policy.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:2364)

at android.view.View.dispatchPointerEvent(View.java:9520)

at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4230)

at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4096)

at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3642)

at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3695)

at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3661)

at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:3787)

at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3669)

at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:3844)

at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3642)

at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3695)

at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3661)

at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3669)

at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3642)

at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:5922)

at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:5896)

at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:5857)

at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:6025)

at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:185)

at android.os.MessageQueue.nativePollOnce(MessageQueue.java:-1)

at android.os.MessageQueue.next(MessageQueue.java:323)

at android.os.Looper.loop(Looper.java:135)

at android.app.ActivityThread.main(ActivityThread.java:5417)

at java.lang.reflect.Method.invoke(Method.java:-1)

at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)

at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

这个dispatchInputEvent实际上是从native方法里面调用的,Looper.cpp 中pollInner方法中的handleEvent最终会调用dispatchInputEvent,这个里面没有

的调用实际上已经在app的主线程啦,所以write不是在这儿,这里只是想看下调用栈

真正write是在system_server进程中主要是InputTransport.cpp中的send方法,这里面的调用比较复杂,可以参考

Input系统—事件处理全过程 http://gityuan.com/2016/12/31/input-ipc/,这篇文章

思考2.广播事件怎么传入主线程

直接看调用栈

"Binder_3@4082" prio=5 runnable

java.lang.Thread.State: RUNNABLE

at android.os.MessageQueue.enqueueMessage(MessageQueue.java:534)

at android.os.Handler.enqueueMessage(Handler.java:631)

at android.os.Handler.sendMessageAtTime(Handler.java:600)

at android.os.Handler.sendMessageDelayed(Handler.java:570)

at android.os.Handler.sendMessage(Handler.java:507)

at android.app.ActivityThread.sendMessage(ActivityThread.java:2281)

at android.app.ActivityThread.sendMessage(ActivityThread.java:2258)

at android.app.ActivityThread.-wrap25(ActivityThread.java:-1)

at android.app.ActivityThread$ApplicationThread.scheduleReceiver(ActivityThread.java:696)

at android.app.ApplicationThreadNative.onTransact(ApplicationThreadNative.java:217)

at android.os.Binder.execTransact(Binder.java:453),

,这个就很清晰了,在binder线程调用enqueueMessage,enqueueMessage方法里面有nativewait即可唤醒主线程

总结:也就是说唤醒主线程有两种方式

1.外部进程通过管道唤醒

2.自己的进程通过bind线程唤醒

参考链接:

我读过的最好的epoll讲解:http://blog.51cto.com/yaocoder/888374

大话 Select、Poll、Epoll : https://cloud.tencent.com/developer/article/1005481

你可能感兴趣的:(Android 消息处理)