【18】Android 线程间通信(三) - Handler
概述
接下来我们会从native层来分析一下,Handler做了什么,以及之前提到过的应用层的两个native的调用链。
nativeWake
最早接触这个方法还记得是什么时候吗?MessageQueue#enqueueMessage中,在这个方法的末尾,我们看到了它的身影,通过判断条件needWake是否为真,从而执行到这个方法中。
//MessageQueue.javamsg.markInUse();msg.when = when;Message p = mMessages;boolean needWake;if (p == null || when == 0 || when < p.when) {// New head, wake up the event queue if blocked.msg.next = p;mMessages = msg;needWake = mBlocked;} else {// Inserted within the middle of the queue. Usually we don't have to wake// up the event queue unless there is a barrier at the head of the queue// and the message is the earliest asynchronous message in the queue.needWake = mBlocked && p.target == null && msg.isAsynchronous();Message prev;for (;;) {prev = p;p = p.next;if (p == null || when < p.when) {break;}if (needWake && p.isAsynchronous()) {needWake = false;}}msg.next = p; // invariant: p == prev.nextprev.next = msg;}// We can assume mPtr != 0 because mQuitting is false.if (needWake) {nativeWake(mPtr);}
我们首先看看这个needWake的条件是什么,从上面源码可以看到,这个needWake是一个局部变量,在方法内定义的。当进入第一个if条件判断,会给needWake赋值mBlocked。mMessage是保存的头节点的Message消息,当头结点消息为空(没消息队列暂时没有消息),或者新消息延迟为0,又或新消息的延迟小于头节点消息的延迟,就进入条件体内部。而这个mBlocked在next方法中,会被赋值,当nativePollOnce未被阻塞,即当前有消息需要立即执行,mBlocked就为false。而当消息队列消息为空,IdleHandlers集合也为空,就会重新赋值给true。
进入else当中,也会给needWake赋值,满足mBlocked为true,且为头部为屏障消息,且新消息为异步消息,才会是true。
接着我们看一下这个mPtr,这个在MessageQueue中定义并且注解说明是专门为nativa code使用的。赋值是在MessageQueue创建的时候,通过nativeInit返回上来。这个先保留着,之后我们再慢慢分析。
MessageQueue.java@UnsupportedAppUsage@SuppressWarnings("unused")private long mPtr; // used by native code
而nativeWake对应的native的类 /frameworks/base/core/jni/android_os_MessageQueue.cpp
//android_os_MessageQueue.cpp
void NativeMessageQueue::wake() {mLooper->wake();
}static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);nativeMessageQueue->wake();
}
进而调用到了native层的Looper中,继续看看Looper做了什么。/system/core/libutils/Looper.cpp
//Looper.cpp
void Looper::wake() {
#if DEBUG_POLL_AND_WAKEALOGD("%p ~ wake", this);
#endifuint64_t inc = 1;ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd.get(), &inc, sizeof(uint64_t)));if (nWrite != sizeof(uint64_t)) {if (errno != EAGAIN) {LOG_ALWAYS_FATAL("Could not write wake signal to fd %d (returned %zd): %s",mWakeEventFd.get(), nWrite, strerror(errno));}}
}
这段代码执行到这里就执行完毕了。这段代码主要做了一个事情,就是通过write方法,向mWakeEventFd写入inc值。
fd 文件描述符。系统管理了一个fd列表,记录了各种不同的fd,mWakeEventFd是其中一个用于唤醒事件的fd。
这里我们一直跟踪下来,发现nativeWake走到native层,最后也只是在fd上写了一个值。而主要执行进来的手段,也是needWake判断条件为真,即:
1、消息队列没有消息的前提下,新消息的延迟when,小于头部消息。
2、消息队列没有消息的前提下,头部有一个同步屏障,且进来的新消息是一个异步消息。
nativePollOnce
这个方法接触的时候是在MessageQueue#next方法中,我们在文章一有补充说明到,nativePollOnce是一个阻塞方法,特定条件下会阻塞当前线程。
@UnsupportedAppUsageMessage next() {// Return here if the message loop has already quit and been disposed.// This can happen if the application tries to restart a looper after quit// which is not supported.final long ptr = mPtr;if (ptr == 0) {return null;}int pendingIdleHandlerCount = -1; // -1 only during first iterationint nextPollTimeoutMillis = 0;for (;;) {if (nextPollTimeoutMillis != 0) {Binder.flushPendingCommands();}nativePollOnce(ptr, nextPollTimeoutMillis);...}
next方法刚进入的时候,会初始化nextPollTimeoutMillis值为0,然后进入for循环,进入了nativePollOne,同nativeWake一样,会传入ptr这个值进去,同时把nextPollTimeoutMillis传入。
//android_os_MessageQueue.cpp
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,jlong ptr, jint timeoutMillis) {NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {...mLooper->pollOnce(timeoutMillis);...
}//Looper.cpp
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {int result = 0;for (;;) {...result = pollInner(timeoutMillis);}
}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++) {const SequenceNumber seq = eventItems[i].data.u64;uint32_t epollEvents = eventItems[i].events;if (seq == WAKE_EVENT_FD_SEQ) {if (epollEvents & EPOLLIN) {awoken();} else {ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);}} else {const auto& request_it = mRequests.find(seq);if (request_it != mRequests.end()) {const auto& request = request_it->second;int events = 0;if (epollEvents & EPOLLIN) events |= EVENT_INPUT;if (epollEvents & EPOLLOUT) events |= EVENT_OUTPUT;if (epollEvents & EPOLLERR) events |= EVENT_ERROR;if (epollEvents & EPOLLHUP) events |= EVENT_HANGUP;mResponses.push({.seq = seq, .events = events, .request = request});} else {ALOGW("Ignoring unexpected epoll events 0x%x for sequence number %" PRIu64" that is no longer registered.",epollEvents, seq);}}...}...
}
这里直接贴出了整个调用链,前面的调用我们都不关心,因为最终还是把timeoutMillis传到了Looper#pollOnceInner中,省略了很多细节,只保留了我们关注的东西。epoll_wait函数,等待文件描述符(fd)上的事件,eventItems存储了发生的事件,timeoutMills应用传下来的超时时间。
1、timeoutMillis 为 -1,epoll_wait 将无限期地等待事件
2、 timeoutMillis 为 0,epoll_wait 将立即返回,不会阻塞
3、一个整数,超时时间,时间到了,epoll_wait也会返回
然后通过for循环取出eventItems中,上一个事件发生后到此之间所有发生的事件,并且判断是否属于WAKE_EVENT_FD_SEQ,这个fd有点眼熟吧,就是nativeWake中写入的那个fd。然后执行awoken进行唤醒。
epoll Linux底层提供的一个消息监听的接口
1、通过写入fd事件,使得epoll_wait将被唤醒。
2、timeoutMillis时间达到
至此,nativePollOnce我们就分析清楚了,当有事件发生,或者延时事件到达,就会取出期间所有发生的事件,判断事件类型是否属于wake事件,并唤醒应用层的nativePollOnce,让逻辑继续执行。
Ptr
这个东西不知道看到这里是否还记得它是什么?就是上面我们提到的在调用这两个native方法都会传入的数据类型,我们到现在还没有搞懂这个东西是做什么用的。只记得在MessageQueue初始化的过程会通过nativeInit返回这个值上来,并且后续将这个值传递下去。
//android_os_MessageQueue.cpp
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();if (!nativeMessageQueue) {jniThrowRuntimeException(env, "Unable to allocate native queue");return 0;}nativeMessageQueue->incStrong(env);return reinterpret_cast<jlong>(nativeMessageQueue);
}
这里的nativeMessageQueue是一个指针,将指针类型转换为一个长整数类型返回回去。我们知道一个MessageQueue对应一个Looper,即对应一个线程。只有在创建MessageQueue的时候,会执行初始化,native层也会维护一个nativeMassageQueue的对象和应用层的MessageQueue一一对应。这样上层触发唤醒,或者阻塞的时候,下层native可以通过这个ptr拿到之前创建的NativeMessageQueue进行wake或者pollonce的操作了。
总结
1、nativePollOnce会阻塞线程
2、MessageQueue和NativeMessageQueue一一对应
3、nativeWake会写fd触发epoll_wait的监听
4、只有fd属于wake的类型才会唤醒线程
5、nativeWake唤醒的条件比较苛刻(从这里看)
这样一来,Handler的所有内容就基本讲完了,可能还涉及一些其他的内容大家可以自行补充一下,这里就不在多提了。这次Handler的相关内容整理下来,我自己的收获也是蛮多的。
相关文章:
【18】Android 线程间通信(三) - Handler
概述 接下来我们会从native层来分析一下,Handler做了什么,以及之前提到过的应用层的两个native的调用链。 nativeWake 最早接触这个方法还记得是什么时候吗?MessageQueue#enqueueMessage中,在这个方法的末尾,我们看…...

静态路由技术
一、路由的概念 路由是指指导IP报文发送的路径信息。 二、路由表的结构 1、Destination/Mask:IP报文的接收方的IP地址及其子网掩码; 2、proto:协议(Static:静态路由协议,Direct:表示直连路由) 3、pref:优先级(数值和优先级成反比) 4、cost:路由开销(从源到目的…...

SpringBoot缓存注解使用
背景 除了 RedisTemplate 外, 自Spring3.1开始,Spring自带了对缓存的支持。我们可以直接使用Spring缓存技术将某些数据放入本机的缓存中;Spring缓存技术也可以搭配其他缓存中间件(如Redis等)进行使用,将某些数据写入到缓存中间件…...
@RequestBody接收到的参数中如何限制List的长度?
在Spring MVC中,你可以使用Valid注解和自定义的验证注解来限制List的长度,防止DOS攻击。具体步骤如下: 创建自定义注解:首先,创建一个自定义注解来验证List的长度。 import javax.validation.Constraint; import jav…...
Linux C语言 54-目录操作
Linux C语言 54-目录操作 本节关键字:Linux、C语言、目录操作、遍历目录 相关C库函数:opendir、readdir、closedir 遍历目录 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <dirent.h> #include <…...

Java实战中如何使用多线程(线程池)及其为什么使用?
这个话题在入行之前就想过很多次,很多8古文或者你搜索的结果都是告诉你什么提高高并发或者是一些很高大上的话,既没有案例也没有什么公式去证明,但是面试中总是被问到,也没有实战经历,所以面试时一问到多线程的东西就无…...

kafka集群搭建-使用zookeeper
1.环境准备: 使用如下3台主机搭建zookeeper集群,由于默认的9092客户端连接端口不在本次使用的云服务器开放端口范围内,故端口改为了8093。 172.2.1.69:8093 172.2.1.70:8093 172.2.1.71:8093 2.下载地址 去官网下载,或者使用如…...

【python】Numpy运行报错分析:IndexError与形状不匹配问题
✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,…...
你有多自律就有多自由
当你失去对时间的控制权,生活也就失去了平衡。 真正对自己有要求的人,都是高度自律的人。 追求自己想要的生活,任何时候开始都不会晚,关键在于你能够坚持下去,以高度自律的精神,日复一日、年复一年的坚持下…...
Codeforces Round 959 (Div. 1 + Div. 2 ABCDEFG 题) 文字讲解+视频讲解
Problem A. Diverse Game Statement 给定 n m n\times m nm 的矩形 a a a, a a a 中的每一个数均在 1 ∼ n m 1\sim nm 1∼nm 之间且互不相同。求出 n m n\times m nm 的矩形 b b b, b b b 中的每一个数均在 1 ∼ n m 1\sim nm 1∼nm 之间且互…...

WSL2 Centos7 Docker服务启动失败怎么办?
wsl 安装的CentOS7镜像,安装了Docker之后,发现用systemctl start docker 无法将docker启动起来。 解决办法 1、编辑文件 vim /usr/lib/systemd/system/docker.service将13行注释掉,然后在下面新增14行的内容。然后保存退出。 2、再次验证 可以发现,我们已经可以正常通过s…...
分布式锁-redisson锁重试和WatchDog机制
抢锁过程中,获得当前线程,通过tryAcquire进行抢锁,该抢锁逻辑和之前逻辑相同。 1、先判断当前这把锁是否存在,如果不存在,插入一把锁,返回null 2、判断当前这把锁是否是属于当前线程,如果是&a…...

ESP8266模块(2)
实例1 查看附近的WiFi 步骤1:进入AT指令模式 使用USB转串口适配器将ESP8266模块连接到电脑。打开串口终端软件,并设置正确的串口和波特率(通常为115200)。输入以下命令并按回车确认: AT如果模块响应OK,…...

Docker安装笔记
1. Mac安装Docker 1.1 Docker安装包下载 1.1.1 阿里云 对于10.10.3以下的用户 推荐使用 对于10.10.3以上的用户 推荐使用 1.1.2 官网下载 系统和芯片选择适合自己的安装包 1.2 镜像加速 【推荐】阿里镜像 登陆后,左侧菜单选中镜像加速器就可以看到你的专属地…...

《昇思25天学习打卡营第21天|Pix2Pix实现图像转换》
Pix2Pix 是一种图像转换模型,使用条件生成对抗网络(Conditional Generative Adversarial Networks,cGANs)实现图像到图像的转换。它主要由生成器(Generator)和判别器(Discriminator)…...

Python和MATLAB网络尺度结构和幂律度大型图生成式模型算法
🎯要点 🎯算法随机图模型数学概率 | 🎯图预期度序列数学定义 | 🎯生成具有任意指数的大型幂律网络,数学计算幂律指数和平均度 | 🎯随机图分析中巨型连接分量数学理论和推论 | 🎯生成式多层网络…...

在jsPsych中使用Vue
jspsych 介绍 jsPsych是一个非常好用的心理学实验插件,可以用来构建心理学实验。具体的就不多介绍了,大家可以去看官网:https://www.jspsych.org/latest/ 但是大家在使用时就会发现,这个插件只能使用js绘制界面,或者…...

机器学习·概率论基础
概率论 概率基础 这部分太简单,直接略过 条件概率 独立性 独立事件A和B的交集如下 非独立事件 非独立事件A和B的交集如下 贝叶斯定理 先验 事件 后验 在概率论和统计学中,先验概率和后验概率是贝叶斯统计的核心概念 简单来说后验概率就是结合了先验概…...

c生万物系列(面向对象:封装)
本系列博客主要介绍c语言的一些屠龙技,里面包含了笔者本人的一些奇思妙想。 该系列博客笔者只是用作记录。如果你偶然找到了这篇博客,但是发现不知所云,请不要过多投入时间,可能笔者本人那时候也看不懂了。 笔者决定用c语言模仿…...

当当网数据采集:Scrapy框架的异步处理能力
在互联网数据采集领域,Scrapy框架以其强大的异步处理能力而著称。Scrapy利用了Python的异步网络请求库,如twisted,来实现高效的并发数据采集。本文将深入探讨Scrapy框架的异步处理能力,并展示如何在当当网数据采集项目中应用这一能…...

Python实现prophet 理论及参数优化
文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候,写过一篇简单实现,后期随着对该模型的深入研究,本次记录涉及到prophet 的公式以及参数调优,从公式可以更直观…...
OkHttp 中实现断点续传 demo
在 OkHttp 中实现断点续传主要通过以下步骤完成,核心是利用 HTTP 协议的 Range 请求头指定下载范围: 实现原理 Range 请求头:向服务器请求文件的特定字节范围(如 Range: bytes1024-) 本地文件记录:保存已…...

ardupilot 开发环境eclipse 中import 缺少C++
目录 文章目录 目录摘要1.修复过程摘要 本节主要解决ardupilot 开发环境eclipse 中import 缺少C++,无法导入ardupilot代码,会引起查看不方便的问题。如下图所示 1.修复过程 0.安装ubuntu 软件中自带的eclipse 1.打开eclipse—Help—install new software 2.在 Work with中…...

基于Springboot+Vue的办公管理系统
角色: 管理员、员工 技术: 后端: SpringBoot, Vue2, MySQL, Mybatis-Plus 前端: Vue2, Element-UI, Axios, Echarts, Vue-Router 核心功能: 该办公管理系统是一个综合性的企业内部管理平台,旨在提升企业运营效率和员工管理水…...
WebRTC从入门到实践 - 零基础教程
WebRTC从入门到实践 - 零基础教程 目录 WebRTC简介 基础概念 工作原理 开发环境搭建 基础实践 三个实战案例 常见问题解答 1. WebRTC简介 1.1 什么是WebRTC? WebRTC(Web Real-Time Communication)是一个支持网页浏览器进行实时语音…...

【Linux】自动化构建-Make/Makefile
前言 上文我们讲到了Linux中的编译器gcc/g 【Linux】编译器gcc/g及其库的详细介绍-CSDN博客 本来我们将一个对于编译来说很重要的工具:make/makfile 1.背景 在一个工程中源文件不计其数,其按类型、功能、模块分别放在若干个目录中,mak…...
Kubernetes 网络模型深度解析:Pod IP 与 Service 的负载均衡机制,Service到底是什么?
Pod IP 的本质与特性 Pod IP 的定位 纯端点地址:Pod IP 是分配给 Pod 网络命名空间的真实 IP 地址(如 10.244.1.2)无特殊名称:在 Kubernetes 中,它通常被称为 “Pod IP” 或 “容器 IP”生命周期:与 Pod …...

Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement
Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement 1. LAB环境2. L2公告策略2.1 部署Death Star2.2 访问服务2.3 部署L2公告策略2.4 服务宣告 3. 可视化 ARP 流量3.1 部署新服务3.2 准备可视化3.3 再次请求 4. 自动IPAM4.1 IPAM Pool4.2 …...

数据分析六部曲?
引言 上一章我们说到了数据分析六部曲,何谓六部曲呢? 其实啊,数据分析没那么难,只要掌握了下面这六个步骤,也就是数据分析六部曲,就算你是个啥都不懂的小白,也能慢慢上手做数据分析啦。 第一…...

今日行情明日机会——20250609
上证指数放量上涨,接近3400点,个股涨多跌少。 深证放量上涨,但有个小上影线,相对上证走势更弱。 2025年6月9日涨停股主要行业方向分析(基于最新图片数据) 1. 医药(11家涨停) 代表标…...