查看原文
其他

嗯?Android 消息机制还能难住我?告辞!

灯不利多 鸿洋 2021-10-12

本文作者


作者:灯不利多

链接:

https://juejin.cn/post/6983598752837664781

本文由作者授权发布。


1Android 消息机制概述


Android 消息机制


Android 消息机制是由 Handler、Looper 和 MessageQueue 三者合作完成的,消息机制可以分为消息机制初始化、消息轮询、消息发送和消息处理 4 个过程来理解,消息机制是基于 Linux 的事件轮询机制 epoll 和用来通知事件的 文件描述符 eventfd 来实现的 。


消息机制初始化过程是从消息轮询器 Looper 的 prepare() 方法开始的,当线程调用 Looper 的 prepare() 方法时,prepare() 方法会调用 Looper 的构造函数创建一个 Looper ,并放到线程私有变量 ThreadLocal 中。Looper 的构造函数中会创建一个消息队列 MessageQueue ,而消息队列的构造方法会调用 nativeInit() JNI 方法初始化 Native 层的消息队列,在 Native 层消息队列的构造方法中,会调用 Native 层 Looper 的构造函数初始化 Native 层的 Looper ,而在 Native 层 Looper 的构造函数中会调用 rebuildEpollLocked() 方法,在 rebuildEpollLocked() 方法中会调用 epoll_create1() 系统调用创建一个 epoll 实例,然后再调用 epoll_ctl() 系统调用给 epoll 实例添加一个唤醒事件文件描述符,到这里消息机制的初始化就完成了。


epoll 、select 和 poll 都是 Linux 中的一种 I/O 多路复用机制, poll 和 select 在每次调用时,都必须遍历所有被监视的文件描述符,文件描述符列表越大,性能就越差。而 epoll 则把监听注册从监听中分离了出来,这样就不需要每次调用时都遍历文件描述符列表了。创建 epoll 实例时,Linux 会创建一个 evnetpoll 结构体,这个结构体中有 rbr 和 rdlist 两个成员,rbr 是红黑树的根节点,epoll 会用红黑树存储所有需要监控的事件 ,rdlist 则是存放着要通过 epoll_wait() 返回给用户的事件。


唤醒事件文件描述符是一个 eventfd 对象,是 Linux 中的一个用来通知事件的文件描述符,与 pipe 相比,pipe 只能在进程/线程间使用,而 eventfd 是广播式的通知,可以多对多。eventfd 的结构体 eventfd_ctx 中有 wqh 和 count 两个成员,wqh 是一个等待队列的头结点,类型为 __wait_queue_head ,是一个自带自旋锁的双向链表的节点,而 count 则是一个计数器。


消息轮询过程是从 Looper 的 loop() 方法开始的,当线程调用 Looper 的 loop() 方法后,loop() 方法中会调用 MessageQueue 的 next() 方法获取下一条要处理的消息,next() 方法中会通过 nativePollOnce() JNI 方法调检查当前消息队列中是否有新的消息要处理,nativePollOnce() 方法会调用 NativeMessageQueue 的 pollOnce() 方法,NativeMessageQueue 的 pollOnce() 方法会调用 Native 层 Looper 的 pollOnce() 方法, Native 层 Looper 的 pollOnce() 方法中会把 timeout 参数传到 epoll_wait() 系统调用中,epoll_wait() 调用后会等待事件的产生,当 MessageQueue 中没有更多消息时,传到 epoll_wait() 中的 timeout 的值就是 -1 ,这时线程会一直被阻塞,直到有新的消息进来,这就是为什么 Looper 的死循环不会导致 CPU 飙高,因为主线程处于阻塞状态。当调用完 nativePollOnce() 方法后,MessageQueue 就会看下当前消息是不是同步屏障,是的话就找出并返回异步消息给 Looper ,不是的话则找出下一条到了发送时间的返回非异步消息。


消息发送过程一般是从 Handler 的 sendMessage() 方法开始的,当我们调用 Handler 的 sendMessage() sendEmptyMessage() 等方法时,Handler 会调用 MessageQueue 的 enqueueMessage() 方法把消息加入到消息队列中。消息 Message 并不是真正的队列结构,而是链表结构。MessageQueue 的enqueueMessage() 方法首先会判断消息的延时时间是否晚于当前链表中最后一个结点的发送时间,是的话则把该消息作为链表的最后一个结点。然后 enqueueMessage() 方法会判断是否需要唤醒消息轮询线程,是的话则通过 nativeWake() JNI 方法调用 NativeMessageQueue 的 wake() 方法。NativeMessageQueue 的 wake() 方法又会调用 Native 层 Looper 的 wake() 方法,在 Native 层 Looper 的 wake() 方法中,会通过 write() 系统调用写入一个 W 字符到唤醒事件文件描述符中,这时监听这个唤醒事件文件描述符的消息轮询线程就会被唤醒。


消息处理过程也是从 Looper 的 loop() 方法开始的,当 Looper 的 loop() 方法从 MessageQueue 的 next()中获取到消息时,就会调用 Message 的 target 的 dispatchMessage() 的方法,Message 的 target 就是发送消息时用的 Handler ,Handler 的 dispatchMessage() 方法首先会判断 Message 是否设置了 callback 回调 ,比如用 post() 方法发送消息时,传入 post() 方法中的 Runnable 就是 Message 的 callback 回调,如果 Message 没有设置 callback ,则 dispatchMessage() 方法会调用 Handler 的 handleMessage() 方法,到这里消息处理过程就结束了。


另外在使用消息 Message 的时候,建议使用 Message 的 obtain() 方法复用全局消息池中的消息。


2消息机制初始化流程


消息机制初始化流程就是 Handler、Looper 和 MessageQueue 三者的初始化流程,Handler 的初始化流程比较简单,而 Looper 的初始化流程则是从 prepare() 方法开始的,当 Looper 的 prepare() 方法被调用后,Looper 会创建一个消息队列 MessageQueue ,在 MessageQueue 的构造方法中会调用 nativeInit() JNI 方法初始化 Native 层的消息队列,在 NativeMessageQueue 的构造方法中会创建 Native 层的 Looper 实例,而在 Native 层的 Looper 的构造函数中,则会把唤醒事件的文件描述符和监控请求的文件描述符添加到 epoll 的兴趣列表中。


消息机制初始化流程


1 Handler 初始化流程


Handler 的初始化过程比较简单,这个过程中比较特别的两个点分别是不能在没有调用 Looper.prepare() 的线程创建 Handler以及异步 Handler。


Handler 中有好几个构造函数,其中不传 Looper 的构造函数在高版本的 SDK 中已经被声明为弃用了,也就是我们要创建主线程消息处理器的话,就要把 Looper.getMainLooper() 传到 Handler 的构造函数中。


Handler 的构造函数有一个比较特别的一个 async 参数,async 为 true 时表示该 Handler 是一个异步消息处理器,使用这个 Handler 发送的消息会是异步消息,但是这个构造函数没有开放给我们使用,是系统组件自己用的。


HandlerCode


2 Looper 初始化流程


之所以我们能在 Activity 中直接用 Handler 给主线程发消息 ,是因为 ActivityThread 的主函数 main() 中初始化了一个主线程专用的 Looper ,也正是这个 Looper 一直在轮询主线程要处理的消息。


ActivityThread


Looper 的 prepareMainLooper() 方法会调用 prepare() 方法创建一个新的 Looper , prepare() 是一个公共静态方法,如果我们也要开一个新的线程执行一个任务,这个任务也需要放在死循环中执行并等待消息,而我们又不想浪费 CPU 资源的话,就可以通过 Looper.prepare() 来创建线程的 Looper ,也可以直接使用 Android SDK 中 的 HandlerThread ,HandlerThread 内部也维护了一个 Looper.prepare() 方法会把创建好的 Looper 会放在线程局部变量 ThreadLocal 中。


prepare() 方法可以传入一个 quitAllowed 参数,这个参数默认为 true ,用于指定是否允许退出,假如 quitAllowed 为 false 的话,那在 MessageQueue 的 quit() 方法被调用时就会抛出一个非法状态异常。


Looper


Looper 的构造函数中创建了 MessageQueue ,下面来看下 MessageQueue 的初始化流程。


3 MessageQueue 初始化流程


在 MessageQueue 的构造函数中调用了一个 JNI 方法 nativeInit() ,并且把初始化后的 NativeMessageQueue 的指针保存在 mPtr 中,发送消息的时候要用这个指针来唤醒消息轮询线程。


MessageQueue


在 nativeInit() 方法中调用了 NativeMessageQueue 的构造函数,在 NativeMessageQueue 的构造函数中创建了一个新的 Native 层的 Looper ,这个 Looper 跟 Java 层的 Looper 没有任何关系,只是在 Native 层实现了一套类似功能的逻辑。


NativeMessageQueue 的构造函数中创建完 Looper 后,会通过 setForThread() 方法把它设置给当前线程,这个操作类似于把 Looper 放到 ThreadLocal 中。


NativeMessageQueue


在 Native 层的 Looper 的构造函数中,创建了一个新的唤醒事件文件描述符(eventfd)并赋值给 mWakeEventFd 变量,这个变量是一个唤醒事件描述符,然后再调用 rebuildEpollLocked() 方法重建 epoll 实例,新的事件文件描述符的初始值为 0 ,标志为 EFD_NONBLOCK 和 EFD_CLOEXEC ,关于什么是文件描述符和这两个标志的作用在后面会讲到。


NativeLooper


rebuildEpollLocked() 方法的实现如下,关于什么是 epoll 后面会讲到,在 rebuildEpollLocked() 方法的最后会遍历请求列表,这个请求列表中的请求有很多地方会添加,比如输入分发器 InputDispatcher 的 registerInputChannel() 方法中也会添加一个请求到 Native 层 Looper 的请求列表中。


rebuildEpollLocked()


1.4 Unix/Linux 体系架构


由于 eventfd 和文件描述符都是 Linux 中的概念,所以下面来看一些 Linux 相关的知识。


Linux 体系架构


Linux 操作系统的体系架构分为用户态和内核态(用户空间和内核空间),内核本质上看是一种软件,控制着计算机的硬件资源,并提供上层应用程序运行的环境。


而用户态就是上层应用程序的活动空间,应用程序的执行,比如依托于内核提供的资源,包括 CPU 资源、存储资源、I/O 资源等,为了让上层应用能够访问这些资源,内核必须为上层应用提供访问的接口,也就是系统调用。


系统调用是受控的内核入口,借助这一机制,进程可以请求内核以自己的名义去执行某些动作,以 API 的形式,内核提供有一系列服务供程序访问,包括创建进程、执行 I/O 以及为进程间通信创建管道等。


5 文件描述符


Linux 继承了 UNIX 一切皆文件 的思想,在 Linux 中,所有执行 I/O 操作的系统调用都以文件描述符指代已打开的文件,包括管道(pipe)、FIFO、Socket、终端、设备和普通文件,文件描述符往往是数值很小的非负整数,获取文件描述符一般是通过系统调用 open() ,在参数中指定 I/O 操作目标文件的路径名。


通常由 shell 启动的进程会继承 3 个已打开的文件描述符:


描述符 0 :标准输入,指代为进程提供输入的文件。


描述符 1 :标准输出,指代供进程写入输出的文件。


描述符 2 :标准错误,指代进程写入错误消息或异常通告的文件。


文件描述符(File Descriptor) 是 Linux 中的一个索引值,系统在运行时有大量的文件操作,内核为了高效管理已被打开的文件会创建索引,用于指向被打开的文件,这个索引就是文件描述符。


6 事件文件描述符 eventfd


eventfd 可以用于线程或父子进程间通信,内核通过 eventfd 也可以向用户空间发送消息,其核心实现是在内核空间维护一个计数器,向用户空间暴露一个与之关联的匿名文件描述符,不同线程通过读写该文件描述符通知或等待对方,内核则通过写该文件描述符通知用户程序。


在 Linux 中,很多程序都是事件驱动的,也就是通过 select/poll/epoll 等系统调用在一组文件描述符上进行监听,当文件描述符的状态发生变化时,应用程序就调用对应的事件处理函数,有的时候需要的只是一个事件通知,没有对应具体的实体,这时就可以使用 eventfd 。


与管道(pipe)相比,管道是半双工的传统 IPC 方式,两个线程就需要两个 pipe 文件,而 eventfd 只要打开一个文件,而文件描述符又是非常宝贵的资源,linux 的默认值也只有 1024 个。eventfd 非常节省内存,可以说就是一个计数器,是自旋锁 + 唤醒队列来实现的,而管道一来一回在用户空间有多达 4 次的复制,内核还要为每个 pipe 至少分配 4K 的虚拟内存页,就算传输的数据长度为 0 也一样。这就是为什么只需要通知机制的时候优先考虑使用 eventfd 。


eventfd 提供了一种非标准的同步机制,eventfd() 系统调用会创建一个 eventfd 对象,该对象拥有一个相关的由内核维护的 8 字节无符号整数,它返回一个指向该对象的文件描述符,向这个文件描述符中写入一个整数会把该整数加到对象值上,当对象值为 0 时,对该文件描述符的 read() 操作将会被阻塞,如果对象的值不是 0 ,那么 read() 会返回该值,并将对象值重置为 0 。



struct eventfd_ctx {

    struct kref kref;
    wait_queue_head_t wqh;
    __u64 count;
    unsigned int flags;
    int id;
};


eventfd_ctx 结构体是 eventfd 实现的核心,其中 wqh、count 和 flags 的作用如下。


wqh 是等待队列头,所有阻塞在 eventfd 上的读进程挂在该等待队列上。


count 是 eventfd 计数器,当用户程序在一个 eventfd 上执行 write 系统调用时,内核会把该值加在计数器上,用户程序执行 read 系统调用后,内核会把该值清 0 ,当计数器为 0 时,内核会把 read 进程挂在等待队列头 wqh 指向的队列上。


有两种方式可以唤醒等待在 eventfd 上的进程,一个是用户态 write ,另一个是内核态的 eventfd_signal ,也就是 eventfd 不仅可以用于用户进程相互通信,还可以用作内核通知用户进程的手段。


在一个 eventfd 上执行 write 系统调用,会向 count 加上被写入的值,并唤醒等待队列中输入的元素,内核中的 eventfd_signal 函数也会增加 count 的值并唤醒等待队列中的元素。


flags 是决定用户 read 后内核的处理方式的标志,取值有EFD_SEMAPHORE、EFD_CLOEXEC和EFD_NONBLOCK三个。


EFD_SEMAPHORE表示把 eventfd 作为一个信号量来使用。


EFD_NONBLOCK 表示该文件描述符是非阻塞的,在调用文件描述符的 read() 方法时,有该标志的文件描述符会直接返回 -1 ,在调用文件描述符的 write() 方法时,如果写入的值的和大于 0xFFFFFFFFFFFFFFFE ,则直接返回 -1 ,否则就会一直阻塞直到执行 read() 操作。


EFD_CLOEXEC 表示子进程执行 exec 时会清理掉父进程的文件描述符。


3事件轮询 epoll


select、poll和epoll都是 I/O 多路复用模型,可以同时监控多个文件描述符,当某个文件描述符就绪,比如读就绪或写就绪时,则立刻通知对应程序进行读或写操作,select/poll/epoll 都是同步 I/O ,也就是读写是阻塞的。


1. epoll 简介


epoll 是 Linux 中的事件轮询(event poll)机制,是为了同时监听多个文件描述符的 I/O 读写事件而设计的,epoll API 的优点有能高效检查大量文件描述符、支持水平和边缘触发、避免复杂的信号处理流程和灵活性高四个。


当检查大量的文件描述符时,epoll 的性能延展性比 select() poll() 高很多。


epoll API 支持水平触发和边缘触发,而 select() poll() 只支持水平触发,信号驱动 I/O 则只支持边缘触发。


epoll 可以避免复杂的信号处理流程,比如信号队列溢出时的处理。


epoll 灵活性高,可以指定我们想检查的事件类型,比如检查套接字文件描述符的读就绪、写就绪或两者同时指定。


2. 水平触发与边缘触发


Linux 中的文件描述符准备就绪的通知有水平触发和边缘触发两种模式。


水平触发通知就是文件描述符上可以非阻塞地执行 I/O 调用,这时就认为它已经就绪。


边缘触发通知就是文件描述符自上次状态检查以来有了新的 I/O 活动,比如新的输入,这时就要触发通知。


3. epoll 实例


epoll API 的核心数据结构称为 epoll 实例,它与一个打开的文件描述符关联,这个文件描述符不是用来做 I/O 操作的,而是内核数据结构的句柄,这些内核数据结构实现了记录兴趣列表和维护就绪列表两个目的。


这些内核数据结构记录了进程中声明过的感兴趣的文件描述符列表,也就是兴趣列表(interest list)。


这些内核数据结构维护了处于 I/O 就绪状态的文件描述符列表,也就是就绪列表(ready list),ready list 中的成员是兴趣列表的子集。


4. epoll API 的 4 个系统调用


epoll API 由以下 4 个系统调用组成。


epoll_create() 创建一个 epoll 实例,返回代表该实例的文件描述符,有一个 size 参数,该参数指定了我们想通过 epoll 实例检查的文件描述符个数。


epoll_creaet1() 的作用与 epoll_create() 一样,但是去掉了无用的 size 参数,因为 size 参数在 Linux 2.6.8 后就被忽略了,而 epoll_create1() 把 size 参数换成了 flag 标志,该参数目前只支持 EPOLL_CLOEXEC 一个标志。


epoll_ctl() 操作与 epoll 实例相关联的列表,通过 epoll_ctl() ,我们可以增加新的描述符到列表中,把已有的文件描述符从该列表中移除,以及修改代表文件描述符上事件类型的掩码。


epoll_wait()用于获取 epoll 实例中处于就绪状态的文件描述符。


5. epoll_ctl()


epoll_ctl


epoll_ctl() 用于操作与 epoll 实例相关联的列表,成功返回 0 ,失败返回 -1,的 fd 参数指明了要修改兴趣列表中的哪一个文件描述符的设定,该参数可以是代表管道、FIFO、套接字等,甚至可以是另一个 epoll 实例的文件描述符。


op 参数用于指定要执行的操作,可以选择的值如下。


EPOLL_CTL_ADD 表示把描述符添加到 epoll 实例 epfd 的兴趣列表中。


EPOLL_CTL_MOD 表示修改描述符上设定的事件。


EPOLL_CTL_DEL 表示把文件描述符从 epfd 的兴趣列表中移除。


6. epoll_wait()


epoll_wait


epoll_wait() 方法用于获取 epoll 实例中处于就绪状态的文件描述符,其中参数 timeout 就是 MessageQueue 的 next() 方法中的 nextPollTimeoutMillis ,timeout 参数用于确定 epoll_wait() 的阻塞行为,阻塞行为有如下几种。


-1 :调用将一直阻塞,直到兴趣列表中的文件描述符有事件产生,或者直到捕捉到一个信号为止。


0 :执行一次非阻塞式检查,看兴趣列表中的文件描述符上产生了哪个事件。


大于 0 :调用将阻塞至 timeout 毫秒,直到文件描述符上有事件发生,或者捕捉到一个信号为止。


7. epoll 事件


下面是几个调用 epoll_ctl() 时可以在 ev.events 中指定的位掩码,以及由 epoll_wait() 返回的 evlist[].events 中的值。


EPOLLIN:可读取非高优先级的数据。


EPOLLPRI:可读取高优先级的数据。


EPOLLRDHUP:套接字对端关闭。


EPOLLOUT:普通数据可写。


EPOLLET:采用边缘触发事件通知。


EPOLLONESHOT:在完成事件通知后禁用检查。


EPOLLERR:在错误时发生。


EPOLLHUP:出现挂断。


4消息轮询过程


1. 消息轮询过程概述


消息循环过程主要是由 Looper 的 loop() 方法、MessageQueue 的 next() 方法、Native 层 Looper 的 pollOnce() 这三个方法组成。


消息轮询过程是从 Looper 的 loop() 方法开始的loop() 方法中有一个死循环,死循环中会调用 MessageQueue 的 next() 方法,获取到消息后,loop() 方法就会调用 Message 的 target 的 dispatchMessage() 方法分发消息,target 其实就是最初发送 Message 的 Handler 。loop() 方法最后会调用 recycleUnchecked() 方法回收处理完的消息。


在 MessageQueue 的 next() 方法中,首先会调用 nativePollOnce() JNI 方法检查队列中是否有新的消息要处理,没有时线程就会被阻塞。有的话就会尝试找出需要优先执行的异步线程,没有异步消息的话,就会判断消息是否到了要执行的时间,是的话就返回给 Looper 处理,否则重新计算消息的执行时间。


2. Looper.loop()


前面讲到了在 ActivityThread 的 main() 函数中会调用 Looper 的 loop() 方法让 Looper 开始轮询消息,loop() 方法中有一个死循环,死循环中会调用 MessageQueue 的 next() 方法获取下一条消息,获取到消息后,loop() 方法就会调用 Message 的 target 的 dispatchMessage() 方法,target 其实就是发送 Message 的 Handler 。最后就会调用 Message 的 recycleUnchecked() 方法回收处理完的消息。


loop()


3. MessageQueue.next()


在 MessageQueue 的 next() 方法中,首先会调用 nativePollOnce() JNI方法检查队列中是否有新的消息要处理,如果没有,那么当前线程就会在执行到 Native 层的 epoll_wait() 时阻塞。如果有消息,而且消息是同步屏障,那就会找出或等待需要优先执行的异步消息。调用完 nativePollOnce() 后,如果没有异步消息,就会判断当前消息是否到了要执行的时间,是的话则返回消息给 Looper 处理,不是的话就重新计算消息的执行时间(when)。在把消息返回给 Looper 后,下一次执行 nativePollOnce() 的 timeout 参数的值是默认的 0 ,所以进入 next() 方法时,如果没有消息要处理,next() 方法中还可以执行 IdleHandler。在处理完消息后,next() 方法最后会遍历 IdleHandler 数组,逐个调用 IdleHandler 的 queueIdle() 方法。


下图是 MessageQueue 中找出异步消息后的链表变化。


MessageQueue 异步消息处理机制


光看 next() 方法的代码的话会觉得有点绕。ViewRootImpl 的 scheduleTraversals() 方法在很多地方都会被调用,当 scheduleTraversals() 方法被调用时,ViewRootImpl 就会调用 MessageQueue 的 postSyncBarrier() 方法插入一个同步屏障到消息链表中,然后再调用 Choreographer 的 postCallback() 方法执行一个 View 遍历任务 ,然后再调用 MessageQueue 的 removeSyncBarrier() 方法移除同步屏障。


Choreographer 的 postCallback() 方法会调用 postCallbackDelayedInternal() 方法,postCallbackDelayedInternal() 方法会调用 scheduleFrameLocked() 方法,scheduleFrameLock() 方法会从消息池中获取一条消息,并调用 Message 的 setAsynchronous() 方法把这条消息的标志 flags 设为异步标志 FLAG_ASYNCHRONOUS,然后调用内部类 FrameHandler 的 sendMessageAtFrontOfQueue() 方法把异步消息添加到队列中。


scheduleFrameLocked()


下面是 MessageQueue next() 方法的具体实现代码。


MessageQueue.next()


IdleHandler 可以用来做一些在主线程空闲的时候才做的事情,通过 Looper.myQueue().addIdleHandler() 就能添加一个 IdleHandler 到 MessageQueue 中,比如下面这样。


addIdleHandler()


当 IdleHandler 的 queueIdle() 方法返回 false 时,那 MessageQueue 就会在执行完 queueIdle() 方法后把这个 IdleHandler 从数组中删除,下次不再执行。


4. Looper.pollOnce()(Native 层)


继续往下看。在 NativeMessageQueue 的 pollOnce() 方法中,会调用 Native 层的 Looper 的 pollOnce() 方法。


NativeMessageQueuePollOnce


在 Looper 的 pollOnce() 方法中,首先会遍历了响应列表,如果响应的标识符(identifier)ident 值大于等 0 ,则返回标识符,响应是在 pollInner() 方法中添加的。


NativeLooperPollOnce


5. Looper.pollInner() (Native 层)


pollInner() 方法中,首先会调用 epoll_wait() 获取可用事件,获取不到就阻塞当前线程,否则遍历可用事件数组 eventItems ,如果遍历到的事件的文件描述符是唤醒事件文件描述符 mWakeEventFd ,则调用 awoken()方法 唤醒当前线程。然后还会遍历响应数组和信封数组,这两个数组是在 Native 层消息机制里用的,和我们上层用的关系不大,这里就不展开讲了。


LooperPollInner


awoken() 方法的实现很简单,只是调用了 read() 方法把 mWakeEventFd 的数据读取出来,mWakeEventFd 是一个 eventfd ,eventfd 的特点就是在读的时候它的 counter 的值会重置为 0 。


awoken()


5消息发送机制




当我们用 Handler 的 sendMessage()  sendEmptyMessage() 和 post() 等方法发送消息时, 最终都会走到 Handler 的 enqueueMessage() 方法。Handler 的 enqueueMessage() 又会调用 MessageQueue 的 enqueueMessage() 方法。



MessageQueue 的 enqueueQueue() 方法的实现如下。enqueueMessage() 首先会判断,当没有更多消息、消息不是延时消息、消息的发送时间早于上一条消息这三个条件其中一个成立时,就会把当前消息作为链表的头节点,然后如果 IdleHandler 都执行完的话,就会调用 nativeWake() JNI 方法唤醒消息轮询线程。


如果把当前消息作为链表的头结点的条件不成立,就会遍历消息链表,当遍历到最后一个节点,或者发现了一条早于当前消息的发送时间的消息,就会结束遍历,然后把遍历结束的最后一个节点插入到链表中。如果在遍历链表的过程中发现了一条异步消息,就不会再调用 nativeWake() JNI 方法唤醒消息轮询线程。



nativeWake() 的实现如下,只是简单调用了 Native 层 Looper 的 wake() 方法。



Native 层 Looper 的 wake() 方法的实现如下,TEMP_FAILURE_RETRY 是一个用于重试,能返回 EINTR 的函数 ,write() 方法会向唤醒事件文件描述符写入一个 W 字符,这个操作唤醒被阻塞的消息循环线程 。



6消息处理过程


消息处理过程是从 Looper 的 loop() 方法开始的,当 Looper 从 MessageQueue 中获取下一条要处理的消息后,就会调用 Message 的 target 的 dispatchMessage() 方法,而 target 其实就是发送消息的 Handler 。



设置 Message 的 target 的地方就是在 Handler 的 enqueueMessage() 方法中。



在 Handler 的 dispatchMessage() 方法中,如果消息是通过 post() 方法发送,那么 post() 传入的 Runnable 就会作为 msg 的 callback 字段。如果 callback 字段不为空,dispatchMessage() 方法就会调用 callback 的 run() 方法 ,否则调用 Handler 的 callback 或 Handler 本身的 handleMessage() 方法,Handler 的 callback 指的是在创建 Handler 时传入构造函数的 Callback 。



7消息 Message


下面我们来看下 Message 的实现。Message 中的 what 是消息的标识符。而 arg1、arg2、obj 和 data 分别是可以放在消息中的整型数据、Object 类型数据和 Bundle 类型数据。when 则是消息的发送时间。


sPool 是全局消息池,最多能存放 50 条消息,一般建议用 Message 的 obtain() 方法复用消息池中的消息,而不是自己创建一个新消息。如果在创建完消息后,消息没有被使用,想回收消息占用的内存,可以调用 recycle() 方法回收消息占用的资源。如果消息在 Looper 的 loop() 方法中处理了的话,Looper 就会调用 recycleUnchecked() 方法回收 Message 。



参考资料

一文帮你搞懂 Android 文件描述符

https://book.douban.com/subject/27197018/

让事件飞 ——Linux eventfd 原理与实践

https://zhuanlan.zhihu.com/p/40572954

Linux探秘之用户态与内核态 - 猿大白 - 博客园

https://www.cnblogs.com/bakari/p/5520860.html

Linux/Unix 系统编程手册(上/下)

https://www.amazon.cn/dp/B075R5LCFY

图解 | 深入揭秘 epoll 是如何实现 IO 多路复用的!

https://mp.weixin.qq.com/s?__biz=MjM5Njg5NDgwNA==&mid=2247484905&idx=1&sn=a74ed5d7551c4fb80a8abe057405ea5e&chksm=a6e304d291948dc4fd7fe32498daaae715adb5f84ec761c31faf7a6310f4b595f95186647f12&scene=21#wechat_redirect

卡顿、ANR、死锁,线上如何监控?

https://juejin.cn/post/6973564044351373326




最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读


任性!我开发了一款自己用的天气预报app
关于 Bitmap 你要知道的一切
官方推荐 Flow 取代 LiveData,有必要吗?


点击 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存