深入理解 Android Handler
一、引言
Handler 在安卓中的地位是不言而喻的,几乎维系着整个安卓程序运行的生命周期,但是这么重要的一个东西,我们真的了解它吗?下面跟随着我的脚步,慢慢揭开Hanler的神秘面纱吧!
本文将介绍Handler 的运行机制、MessageQueue、Looper 的关系,ThreadLocal,以及Handler 导致的内存泄漏问题
二、Handler 系统组成概览
在 Handler 的源码中,主要涉及以下核心组件:
Message:封装消息的数据结构。MessageQueue:存储Message的队列,内部是单链表。Looper:负责循环读取MessageQueue并分发消息。Handler:对外提供sendMessage()、post()发送消息,并处理MessageQueue中的消息。
它们之间关系如下图所示:

三、Handler 的创建
当 Handler 被创建时,它会绑定当前线程的 Looper:
public Handler() {this(Looper.myLooper(), null, false);
}
public Handler(Looper looper) {this(looper, null, false);
}
最终调用:
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async,boolean shared) {mLooper = looper;mQueue = looper.mQueue;mCallback = callback;mAsynchronous = async;mIsShared = shared;
}
mLooper通过Looper.myLooper()获取当前线程的Looper。mQueue由Looper提供,确保所有Handler在同一个Looper线程内共享MessageQueue。
重点:主线程默认初始化
Looper,但子线程默认没有,需要手动Looper.prepare()。如果一定要在子线程中使用,推荐使用
HandlerThread,比于手动创建Looper,HandlerThread封装了Looper的创建和管理逻辑,代码更加简洁,也更易于维护。同时,HandlerThread有自己独立的消息队列,不会干扰主线程或其他线程的消息处理。
四、sendMessage() 如何发送消息
当我们调用 sendMessage() 时:
handler.sendMessage(msg);
实际上调用:
public boolean sendMessage(Message msg) {return sendMessageDelayed(msg, 0);
}
最终:
public boolean sendMessageDelayed(Message msg, long delayMillis) {return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
最终调用 enqueueMessage():
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {msg.target = this; // 绑定 Handlerreturn queue.enqueueMessage(msg, uptimeMillis);
}
@UnsupportedAppUsage
/*package*/ Handler target;
也就是说 Message 引用了 Handler,这也为内存泄漏埋下伏笔
五、MessageQueue 插入消息
boolean enqueueMessage(Message msg, long when) {synchronized (this) {// 插入 MessageQueueMessage 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;}}return true;
}
enqueueMessage 方法负责将消息按照时间顺序正确地插入到单链表结构的队列中,按 when 进行排序。
六、Looper 如何处理消息
Looper.loop() 读取消息
public static void loop() {for (;;) {Message msg = queue.next(); // 取出消息//...msg.target.dispatchMessage(msg); // 交给 Handler 处理}
}
MessageQueue.next()
Message next() {// 检查消息队列是否已销毁,若销毁则返回 nullif (mPtr == 0) return null;int nextPollTimeoutMillis = 0;for (;;) {// 若有超时时间,刷新 Binder 待处理命令if (nextPollTimeoutMillis != 0) Binder.flushPendingCommands();// 阻塞线程,等待新消息或超时nativePollOnce(mPtr, nextPollTimeoutMillis);synchronized (this) {final long now = SystemClock.uptimeMillis();Message msg = mMessages;// 若为屏障消息,找下一个异步消息if (msg != null && msg.target == null) {do { msg = msg.next; } while (msg != null && !msg.isAsynchronous());}if (msg != null) {// 若消息未到处理时间,计算超时时间if (now < msg.when) {nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);} else {// 若消息到处理时间,从队列移除并返回mMessages = msg.next;msg.next = null;msg.markInUse();return msg;}} else {// 若无消息,一直阻塞nextPollTimeoutMillis = -1;}// 若消息队列正在退出,释放资源并返回 nullif (mQuitting) {dispose();return null;}}}
}
nativePollOnce() 让当前线程进入阻塞状态,直到有新的消息到来或者超时
nativePollOnce() 的主要功能是:
- 线程阻塞:让当前线程进入等待状态,避免空转消耗CPU资源
- 事件唤醒:当有新消息到达或超时发生时,立即唤醒线程处理
- Native 层集成:与 Linux 的 epoll 机制对接,实现高效I/O多路复用
void nativePollOnce(long ptr, int timeoutMillis)
ptr:指向 Native Looper 对象的指针(C++对象地址)
timeoutMillis 的含义:
- 如果 timeoutMillis > 0
epoll_wait最多阻塞 timeoutMillis 毫秒,期间如果有事件发生,则提前返回。
- 如果 timeoutMillis == 0
epoll_wait立即返回(非阻塞)。
- 如果 timeoutMillis < 0
epoll_wait无限等待,直到有事件触发。
最终调用了 Linux epoll 机制 来监听消息事件。
七、nativePollOnce 方法调用流程
Java 层调用
// MessageQueue.java
private native void nativePollOnce(long ptr, int timeoutMillis);
JNI 本地方法,由 MessageQueue 调用,用于等待消息。
在 MessageQueue.next() 方法中:
// MessageQueue.java
nativePollOnce(mPtr, nextPollTimeoutMillis);
它的作用是:
- 如果
MessageQueue里有消息,立即返回。 - 如果没有消息,则阻塞,直到有新的消息到来或
timeoutMillis超时。
JNI 层调用
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,jlong ptr, jint timeoutMillis) {MessageQueue* mq = reinterpret_cast<MessageQueue*>(ptr);mq->pollOnce(timeoutMillis);
}
将 Java 传来的 mPtr 转换成 MessageQueue* 对象,并调用 pollOnce() 方法。
Native 层 pollOnce()
在 MessageQueue.cpp:
void MessageQueue::pollOnce(int timeoutMillis) {mLooper->pollOnce(timeoutMillis);
}
调用了 Looper::pollOnce(),进入 消息轮询 逻辑。
Looper 的 pollOnce()
在 Looper.cpp:
int Looper::pollOnce(int timeoutMillis) {return pollInner(timeoutMillis);
}
这里调用 pollInner(timeoutMillis),它的核心逻辑是 使用 epoll_wait() 监听事件。
epoll 监听消息事件
pollInner(timeoutMillis) 的核心逻辑:
int Looper::pollInner(int timeoutMillis) {struct epoll_event eventItems[EPOLL_MAX_EVENTS];int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);if (eventCount > 0) {for (int i = 0; i < eventCount; i++) {// 处理事件}}
}
其中:
mEpollFd是epoll文件描述符,用于监听多个文件描述符(FD)。epoll_wait()会 阻塞当前线程,直到:- 有 新消息可读
- 有 文件描述符事件触发
- 超时(
timeoutMillis毫秒后自动返回)
到这里,我们清楚了 nativePollOnce 的主要作用是等待新消息到达消息队列。当调用这个方法时,如果当前消息队列中没有需要立即处理的消息,线程会被阻塞,从而释放 CPU 资源,直到有新消息到来或者发生其他唤醒条件。
那么 epoll_wait() 如何监听消息?
epoll_wait() 监听哪些事件?
MessageQueue 的 pipe(管道):当 Handler 发送消息时,写入 pipe,触发 epoll 事件。
输入事件:当用户触摸屏幕或按键时,触发 epoll 事件。
文件描述符(FileDescriptor):例如 Binder 进程间通信(IPC)事件。
等等…
消息如何触发 epoll?
Handler.sendMessage()会向MessageQueue写入数据:
write(mWakeEventFd, "W", 1);
-
epoll_wait()监听到pipe有数据,返回。 -
Looper处理新消息,Java 层Handler开始执行handleMessage()。
epoll_wait阻塞等待wakeFd上的可读事件,当有数据写入wakeFd,epoll_wait返回,线程被唤醒,这里并不关心写入wakeFd的具体数据是什么,只关心可读事件的发生。
pipe 的作用
让 Handler.sendMessage() 触发 epoll 事件,立即唤醒 Looper。
至此,综上,我们可以知道 epoll_wait() 只负责等待事件,不会提前返回“第一条消息”,它只会返回“有事件触发”的信号,具体执行哪个消息是 MessageQueue.next() 的逻辑,它会选择最早应该执行的消息,这就是 Handler 的阻塞唤醒的核心逻辑所在!
八、Handler 处理消息
public void dispatchMessage(Message msg) {if (msg.callback != null) {msg.callback.run();} else {handleMessage(msg);}
}
最终执行:
@Override
public void handleMessage(Message msg) {// 需要用户实现
}
九、核心组件之间的关系
Thread└── ThreadLocal<Looper>└── Looper└── MessageQueue└── Message1 → Message2 → ...↑Handler
Handler持有对MessageQueue的引用(间接通过Looper)因为Handler中的MessageQueue是从Looper中获取的;
public Handler(@Nullable Callback callback, boolean async) {//..mQueue = mLooper.mQueue;//..}
- 每个线程通过 ThreadLocal 绑定自己的 Looper;
- Looper 管理其对应的 MessageQueue;
这样它们的关系就清晰了,每个线程只有一个Looper(是由ThreadLocal确保的),可以有多个Handler。
public final class Looper {// 线程本地存储,每个线程一个Looper实例static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();private Looper(boolean quitAllowed) {mQueue = new MessageQueue(quitAllowed);mThread = Thread.currentThread();}public static void prepare() {prepare(true);}private static void prepare(boolean quitAllowed) {if (sThreadLocal.get()!= null) {throw new RuntimeException("Only one Looper may be created per thread");}sThreadLocal.set(new Looper(quitAllowed));}
}
关于ThreadLocal的详细介绍可以看这篇文章:深入剖析Java中ThreadLocal原理
十、内存泄漏问题分析及解决方案
我们都知道判断内存泄漏的依据是:短生命周期对象是否被长生命周期对象引用!既然使用Handler不当会导致内存泄漏,那么我们只需要找到被引用的源头,然后去解决。
Handler 导致内存泄漏的完整引用流程
- 匿名内部类或非静态内部类的隐式引用:
众所周知,在Java中 匿名内部类或非静态内部类会持有外部类的引用,如下:
public class MainActivity extends AppCompatActivity {private Handler mHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {// 处理消息}};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mHandler.sendEmptyMessageDelayed(0, 10000);}
}
这里的mHandler是一个非静态内部类。非静态内部类会隐式持有外部类(这里是MainActivity)的引用。这意味着mHandler对象中包含了对MainActivity实例的引用。
- MessageQueue 对 Message 的持有:
在上面示例中,我们发送了一个延迟的Message,尽管只传了一个0,但是其内部也会封装为Message,这时候Handler 会将 Message对象并将其发送到与之关联的MessageQueue中,MessageQueue会持有这个Message对象,直到该消息被处理。
- Message 对 Handler 的持有:
由上面第四小节的sendMessage()可知,在放入队列的时候,会将Handler 与 Message 关联:
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {msg.target = this; // 绑定 Handlerreturn queue.enqueueMessage(msg, uptimeMillis);
}
主要作用是,让Message知道是从哪个Handler发送的,并最终让那个Handler 的 handleMessage去处理。
public final class Looper {static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();@UnsupportedAppUsageprivate static Looper sMainLooper; // guarded by Looper.class//...
}
我们都知道,在主线程中,主线程的Looper会一直运行下去(或者说 Looper被 静态 ThreadLocal<Looper> 所引用),不能被停止,而MessageQueue 又被Looper 所引用,这就产生了一条完整的引用链:ThreadLocal<Looper> - Looper - MessageQueue - Message - Handler - MainActivity
** 解决方案**
- 使用静态内部类 + WeakReference:
要解决内存泄漏,就是把引用链上任意一条引用断开,让GC不可达就行了,其实我们能操作的就只有 Handler - **MainActivity **这一条引用:
static class MyHandler extends Handler {private final WeakReference<MyActivity> ref;MyHandler(MyActivity activity) {ref = new WeakReference<>(activity);}@Overridepublic void handleMessage(Message msg) {MyActivity activity = ref.get();if (activity != null) {// Safe to use activity}}
}
- 在 Activity 的
onDestroy()中清除消息:
handler.removeCallbacksAndMessages(null);
其实,只要消息不是延迟很久或者反复堆积,就不会在 MessageQueue 中长时间滞留,从而也就不会延长 Handler 或其持有对象的生命周期。
想想,在实际开发中,谁会在Activity中延迟发送一个很长时间的消息,所以我们不必为 Handler 导致内存泄漏,过度紧张,稍微留意一下就可以避免了 😃
十一、最后
Handler 是 Android 消息机制的基础组成部分。通过对 Handler、Looper、MessageQueue 之间关系的理解,我们可以更深入掌握 Android 的线程模型与 UI 更新流程。
由于本人能力有限,并没有对 Handler 进行过度深入全面了解,比如同步屏障等,如果文章内容解读有误,还望不吝赐教。
相关文章:
深入理解 Android Handler
一、引言 Handler 在安卓中的地位是不言而喻的,几乎维系着整个安卓程序运行的生命周期,但是这么重要的一个东西,我们真的了解它吗?下面跟随着我的脚步,慢慢揭开Hanler的神秘面纱吧! 本文将介绍Handler 的运…...
C++ 什么是隐式类型转换,什么是显式类型转换
在 C 中,类型转换是将一种数据类型的值转换为另一种数据类型的过程,分为 隐式类型转换(由编译器自动完成)和 显式类型转换(由程序员手动指定)。以下是它们的区别和示例:…...
NVIDIA 自动驾驶技术见解
前言 参与 NVIDIA自动驾驶开发者实验室 活动,以及解读了 NVIDIA 安全报告 自动驾驶 白皮书,本文是我的一些思考和见解。自动驾驶技术的目标是为了改善道理安全、减少交通堵塞,重塑更安全、高效、包容的交通生态。在这一领域,NVI…...
【Flask】Explore-Flask:早期 Flask 生态的实用指南
开源项目:explore-flask/README.rst at master rpicard/explore-flask (github.com) 一、Coding conventions Summary Try to follow the coding style conventions laid out in PEP 8. Try to document your app with docstrings as defined in PEP 257. def…...
STM32 中断系统深度剖析
在嵌入式系统开发领域,STM32 系列微控制器凭借其强大的性能和丰富的资源被广泛应用。中断系统作为 STM32 的关键特性之一,能够极大地提升系统的实时响应能力和多任务处理效率。本文将基于 STM32F4 系列芯片,深入剖析中断与外设中断的原理、配…...
FAST‘25论文解读:HaSiS单索引存储架构实现HTAP数据处理新范式
想象一下这样的场景:每一笔线上交易都能实时更新库存分析,金融应用能在交易发生那一刻完成欺诈检测——既不延迟也不损失性能。这正是HTAP(Hybrid Transactional and Analytical Processing,混合事务与分析处理)带来的…...
FastAPI:现代高性能Python Web框架的技术解析与实践指南
一、FastAPI的诞生背景与技术定位 在数字化转型的浪潮中,API(应用程序接口)作为连接服务与数据的核心枢纽,其性能与开发效率直接影响业务迭代速度。传统Python框架如Django和Flask虽功能丰富,但在高并发场景下面临性能瓶颈,且缺乏对异步编程的原生支持。FastAPI应运而生…...
缓存 --- 缓存击穿, 缓存雪崩, 缓存穿透
缓存 --- 缓存击穿, 缓存雪崩, 缓存穿透 缓存击穿(Cache Breakdown)概念原理实际场景代码实现(互斥锁方案) 缓存雪崩(Cache Avalanche)概念原理实际场景代码实现(随机过期时间) 缓存…...
Android 中实现图片翻转动画(卡片翻转效果)
1、简述 通过 ObjectAnimator 和 AnimatorSet 可以实现图片的翻转动画,并在翻转过程中切换图片,同时避免图片被镜像。 ObjectAnimator 是 Android 动画框架中的一个类,用于对对象的属性进行动画效果处理。它通过改变对象的属性值来实现动画效果,非常适合实现复杂的动画,如…...
【论文阅读21】-PSOSVM-CNN-GRU-Attention-滑坡预测(2024-12)
这篇论文主要提出并验证了一种新型的混合智能模型(PSOSVM-CNN-GRU-Attention),用于准确预测滑坡的点位移,并构建可靠的位移预测区间。通过对Baishuihe滑坡和Shuping滑坡的案例分析,展示了该模型的出色性能。 [1] Zai D…...
蓝牙 6.0 发布,解锁无线科技新可能
在5G和Wi-Fi 7高速发展的时代,蓝牙技术始终以独特优势深度融入日常生活。从无线耳机到智能家居,它凭借低功耗、高兼容的特性,悄然连接各类智能设备,打造无缝的数字生活体验。无论是聆听音乐、智能门禁还是健康监测,蓝牙…...
EasyCVR视频智能分析平台助力智慧园区:全场景视频监控摄像头融合解决方案
一、方案背景 在智慧园区建设的浪潮下,设备融合、数据整合与智能联动已成为核心诉求。视频监控作为智慧园区的“视觉中枢”,其高效整合直接影响园区的管理效能与安全水平。然而,园区内繁杂的视频监控设备生态——不同品牌、型号、制式的摄像…...
PHP发送邮件
一、安装PHPMailer 进入项目目录下,执行:composer require phpmailer/phpmailer 二、使用 <?php use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\Exception;require vendor/autoload.php;$mail new PHPMailer(true); header("…...
为您的照片提供本地 AI 视觉:使用 Llama Vision 和 ChromaDB 构建 AI 图像标记器
有没有花 20 分钟浏览您的文件夹以找到心中的特定图像或屏幕截图?您并不孤单。 作为工作中的产品经理,我总是淹没在竞争对手产品的屏幕截图、UI 灵感以及白板会议或草图的照片的海洋中。在我的个人生活中,我总是捕捉我在生活中遇到的事物&am…...
Spark on K8s 在 vivo 大数据平台的混部实战与优化
一、Spark on K8s 简介 (一)定义与架构 Spark on K8s 是一种将 Spark 运行在 Kubernetes(K8s)集群上的架构,由 K8s 直接创建 Driver 和 Executor 的 Pod 来运行 Spark 作业。其架构如下。 Driver Pod:相当于 Spark 集群中的 Driver,负责作业的调度和管理,它会根据作业…...
K8S节点出现Evicted状态“被驱逐”
在Kubernetes集群中,Pod状态为“被驱逐(evicted)”表示Pod无法在当前节点上继续运行,已被集群从节点上移除。 问题分析: 节点磁盘空间不足 ,使用df -h查看磁盘使用情况 可以看到根目录 / 已100%满&#x…...
重学React(一):描述UI
背景:React现在已经更新到19了,文档地址也做了全面的更新,上一次系统性的学习还是在16-17的大版本更新。所以,现在就开始重新学习吧~ 学习内容: React官网教程:https://zh-hans.react.dev/lea…...
CSS核心笔记002
margin塌陷问题 第一个子元素的上margin会作用在父元素上, 最后一个子元素的下margin会作用在父元素上解决 1. 给父元素设置 不为0的pandding 2. 给父元素设置宽度不为0 的border 3. 给父元素设置样式 overflow:hiddenmargin合并问题 兄弟元素的下外margin和会下面兄弟的上…...
遨游通讯发布国产化旗舰三防手机AORO AU1:以自主可控重塑工业安全
在全球产业链加速重构的背景下,国产化技术突破已成为工业领域高质量发展的核心驱动力。作为专精特新中小企业,遨游通讯始终以“让世界更安全、更高效、更简单”为使命,深耕“危、急、特”场景智能通信设备的研发。近日,遨游通讯正…...
【Python】Selenium切换网页的标签页的写法(全!!!)
在使用selenium做网站爬取测试的时候,我们经常会遇到一些需要点击的元素,才能点击到我们想要进入的页面, 于是我们就要模拟 不断地 点点点击 鼠标的样子。 这个时候网页上就会有很多的标签页,你的浏览器网页标签栏 be like: 那…...
洛谷P1177【模板】排序:十种排序算法全解(1)
扯谈 之前我已经把十大排序算法全讲了一遍(具体详见专栏C排序算法),今天我们来用一道简单的题目总结实战一下。 算法实现 一、桶排序(Bucket Sort) 适用场景:数据范围已知且较小(需根据测试数据调整…...
pytorch 51 GroundingDINO模型导出tensorrt并使用c++进行部署,53ms一张图
本专栏博客第49篇文章分享了将 GroundingDINO模型导出onnx并使用c++进行部署,并尝试将onnx模型转换为trt模型,fp16进行推理,可以发现推理速度提升了一倍。为此对GroundingDINO的trt推理进行调研,发现 在GroundingDINO-TensorRT-and-ONNX-Inference项目中分享了模型导出onnx…...
中间件--ClickHouse-11--部署示例(Linux宿主机部署,Docker容器部署)
一、Linux宿主机部署 1、环境准备 操作系统:推荐使用 CentOS 7/8 或 Ubuntu 18.04/20.04。硬件要求: 至少 2 核 CPU 和 4GB 内存。足够的磁盘空间(根据数据量评估)。CPU需支持SSE4.2指令集(可通过以下命令检查&#…...
DeepSeek和Excel结合生成动态图表
文章目录 一、前言二、3D柱状图案例2.1、pyecharts可视化官网2.2、Bar3d-Bar3d_puch_card2.3、Deepseek2.4、WPS2.5、动态调整数据 一、前言 最近在找一些比较炫酷的动态图表,用于日常汇报,于是找到了 DeepseekExcel王牌组合,其等同于动态图…...
[Python入门学习记录(小甲鱼)]第6章 函数
函数就是把代码整理打包的东西 6.1 Python的函数基操 函数的基本操作 6.1.1 创建和调用函数 def myfunc():print(1)print(2)print(3) myfunc() # 输出 1 2 3 带换行 调用时会自动找函数定义6.1.2 函数的参数 def add(num1, num2):print(num1 num2) add(1, 2) # 输出 3…...
Ubuntu20.04 部署llama-factory问题集
llama3 微调教程之 llama factory 的 安装部署与模型微调过程,模型量化和gguf转换。_llamafactory 部署-CSDN博客 1.跟着教程 llama-factory下载不下来 来,试着换源,多试几次,就可以成功了。。。 2.跟着教程,发现无法…...
海量聊天数据处理:基于Spring Boot与SharingJDBC的分库分表策略及ClickHouse冷热数据分离
引言 随着互联网应用的快速发展,每天产生的聊天记录数量级已经达到了惊人的程度。以2000万条/天为例,一年下来就是大约7.3亿条记录。如此庞大的数据量给数据库的设计和管理带来了前所未有的挑战。本文将探讨如何使用SharingJDBC整合Spring Boot技术来实…...
EAL4+与等保2.0:解读中国网络安全双标准
EAL4与等保2.0:解读中国网络安全双标准 在当今数字化时代,网络安全已成为各个行业不可忽视的重要议题。特别是在金融、政府、医疗等领域,保护信息的安全性和隐私性显得尤为关键。在中国,EAL4和等级保护2.0(简称“等保…...
GreatSQL启动崩溃:jemalloc依赖缺失问题排查
GreatSQL启动崩溃:jemalloc依赖缺失问题排查 故障现象: 之前协助用户安装 GreatSQL 测试环境时,遇到一个 case,数据库初始化时没有报错,但是使用mysqld_safe去启动,会直接 crash ,详情报错如下࿱…...
大语言模型助力 Support Case 分析,提升云服务效率
1. 背景 技术工单(Support Case)是企业在进行云平台操作的时候通常会用到的一种技术支持类型,提供的技术支持通常包括所有的云服务的使用问题、账单问题、限制额度提升等等。对于云平台的管理者而言,对各个 BU 所提的工单进行统计…...
