深入理解 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
  • mQueueLooper 提供,确保所有 Handler 在同一个 Looper 线程内共享 MessageQueue

重点:主线程默认初始化 Looper,但子线程默认没有,需要手动 Looper.prepare()

如果一定要在子线程中使用,推荐使用 HandlerThread,比于手动创建 LooperHandlerThread 封装了 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++) {// 处理事件}}
}

其中:

  • mEpollFdepoll 文件描述符,用于监听多个文件描述符(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上的可读事件,当有数据写入wakeFdepoll_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()可知,在放入队列的时候,会将HandlerMessage 关联:

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {msg.target = this; // 绑定 Handlerreturn queue.enqueueMessage(msg, uptimeMillis);
}

主要作用是,让Message知道是从哪个Handler发送的,并最终让那个HandlerhandleMessage去处理。

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 进行过度深入全面了解,比如同步屏障等,如果文章内容解读有误,还望不吝赐教。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/77247.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

读书笔记 -- MySQL架构

1、MySQL逻辑架构 最上层的服务并不是 MySQL所独有的&#xff0c;大多数基于网络的客户端/服务器的工具或者服务都有类似的架构。比如连接处理、授权认证、安全等等。 第二层架构是 MySQL 比较有意思的部分。大多数 MySQL 的核心服务功能都在这一层包括查询解析、分析、…

linux 4.14内核jffs2文件系统不自动释放空间的bug

前段时间在做spi-nor flash项目的时候&#xff0c;使用jffs2文件系统&#xff0c;发现在4.14内核下存在无法释放空间的bug&#xff0c;后来进行了修复&#xff0c;修复后功能正常&#xff0c;现将修复patch公开&#xff0c;供后来者学习&#xff1a; diff --git a/fs/jffs2/ac…

vue3+vite 实现.env全局配置

首先创建.env文件 VUE_APP_BASE_APIhttp://127.0.0.1/dev-api 然后引入依赖&#xff1a; pnpm install dotenv --save-dev 引入完成后&#xff0c;在vite.config.js配置文件内加入以下内容&#xff1a; const env dotenv.config({ path: ./.env }).parsed define: { // 将…

Oracle 19c部署之手工建库(四)

#Oracle #19c #手工建库 手工创建Oracle数据库&#xff08;也称为手工建库&#xff09;是指在已经安装了Oracle数据库软件的基础上&#xff0c;通过手动执行一系列命令和步骤来创建一个新的数据库实例。这种方法与使用Database Configuration Assistant (DBCA)等工具自动创建数…

【Reading Notes】(8.3)Favorite Articles from 2025 March

【March】 雷军一度登顶中国首富&#xff0c;太厉害了&#xff08;2025年03月02日&#xff09; 早盘&#xff0c;小米港股一路高歌猛进&#xff0c;暴涨4%&#xff0c;股价直接飙到52港元的历史新高。这一波猛如虎的操作&#xff0c;直接把雷军的身家拉到了2980亿元&#xff0c…

【Python爬虫基础篇】--1.基础概念

目录 1.爬虫--定义 2.爬虫--组成 3.爬虫--URL 1.爬虫--定义 网络爬虫&#xff0c;是一种按照一定规则&#xff0c;自动抓取互联网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。随着网络的迅速发展&#xff0c;万维网成为大量信息的载体…

C语言超详细结构体知识

1.自定义类型&#xff1a;结构体的介绍 在之前的博客中&#xff0c;我们简单介绍过了关于结构体的基本知识&#xff0c;这里我们稍微复习一下。 结构体(struct)是C语言中一种重要的复合数据类型&#xff0c;它允许将不同类型的数据组合成一个整体。 1.1结构体的定义 结构体使…

C++学习:六个月从基础到就业——内存管理:new/delete操作符

C学习&#xff1a;六个月从基础到就业——内存管理&#xff1a;new/delete操作符 本文是我C学习之旅系列的第十七篇技术文章&#xff0c;也是第二阶段"C进阶特性"的第二篇&#xff0c;主要介绍C中动态内存管理的核心操作符——new和delete。查看完整系列目录了解更多…

15~30K,3年以上golang开发经验

继续分享最新的面经&#xff0c;前面发的两篇大家也可以看看&#xff1a; 「坐标上海&#xff0c;20K的面试强度」「北京七猫&#xff0c;薪资25~35K&#xff0c;瞧瞧面试强度」 今天分享的是golang开发岗面经&#xff0c;要求是3年以上golang开发经验&#xff0c;薪资为15~3…

Python爬虫实战:获取优志愿专业数据

一、引言 在信息爆炸的当下,数据成为推动各领域发展的关键因素。优志愿网站汇聚了丰富的专业数据,对于教育研究、职业规划等领域具有重要价值。然而,为保护自身数据和资源,许多网站设置了各类反爬机制。因此,如何高效、稳定地从优志愿网站获取计算机专业数据成为一个具有…

ArcPy工具箱制作(下)

在上一篇博客中&#xff0c;我们已经初步了解了如何制作ArcPy工具箱&#xff0c;包括工具箱的基本概念、准备工作、脚本编写以及将脚本转换为工具箱的步骤。今天&#xff0c;我们将继续深入探讨ArcPy工具箱的制作&#xff0c;重点介绍一些进阶技巧和优化方法. 一、优化工具箱的…

不一样的flag 1(迷宫题)

题目 做法 下载压缩包&#xff0c;解压&#xff0c;把解压后的文件拖进Exeinfo PE进行分析 32位&#xff0c;无壳 扔进IDA&#xff08;32位&#xff09;&#xff0c;找到main&#xff0c;F5反编译 没啥关键词&#xff0c;ShiftF12也找不到什么有用的点 从上往下分析吧 puts(…

工程化实践:Flutter项目结构与规范

工程化实践&#xff1a;Flutter项目结构与规范 在Flutter项目开发中&#xff0c;良好的工程化实践对于提高开发效率、保证代码质量和团队协作至关重要。本文将从项目结构、代码规范、CI/CD流程搭建以及包管理等方面&#xff0c;详细介绍Flutter项目的工程化最佳实践。 项目结…

[Java · 初窥门径] Java 语言初识

&#x1f31f; 想系统化学习 Java 编程&#xff1f;看看这个&#xff1a;[编程基础] Java 学习手册 0x01&#xff1a;Java 编程语言简介 Java 是一种高级计算机编程语言&#xff0c;它是由 Sun Microsystems 公司&#xff08;已被 Oracle 公司收购&#xff09;于 1995 年 5 …

1187. 【动态规划】竞赛总分

题目描述 学生在我们USACO的竞赛中的得分越多我们越高兴。我们试着设计我们的竞赛以便人们能尽可能的多得分。 现在要进行一次竞赛&#xff0c;总时间T固定&#xff0c;有若干类型可选择的题目&#xff0c;每种类型题目可选入的数量不限&#xff0c;每种类型题目有一个si(解答…

使用KeilAssistant代替keil的UI界面

目录 一、keil Assistant的优势和缺点 二、使用方法 &#xff08;1&#xff09;配置keil的路径 &#xff08;2&#xff09;导入并使用工程 &#xff08;3&#xff09;默认使用keil自带的ARM编译器而非GUN工具链 一、keil Assistant的优势和缺点 在日常学…

【React】通过 fetch 发起请求,设置 proxy 处理跨域

fetch 基本使用跨域处理 fetch 基本使用 在node使用原生ajax发请求&#xff1a;XMLHttpRequest()1.获取xhr对象 2.注册回调函数 3.设置参数&#xff0c;请求头 4.发起连接原生ajax没有带异步处理 promise&#xff1b;原生ajax封装一下&#xff0c;以便重复调用jQuery&#…

Redis(二) - Redis命令详解

文章目录 前言一、启动Redis并进入客户端1. 启动Redis2. 进入Redis客户端3. 使用IDEA连接Redis 二、查看命令帮助信息1. 查看所有命令2. 查看指定命令帮助 三、键操作命令1. set命令2. mset命令3. keys命令4. get命令5. mget命令6. dump命令7. exists命令8. type命令9. rename命…

【Qt】初识Qt(二)

目录 一、显示hello world1.1 图形化界面1.2 写代码 二、对象树三、使用输入框显示hello world四、使用按钮显示hello world 一、显示hello world 有两种方式实现hello world&#xff1a; 通过图形化界面&#xff0c;在界面上创建出一个控件&#xff0c;显示hello world通过写…

空调制冷量和功率有什么关系?

空调的制冷量和功率是衡量空调性能的两个核心参数,二者既有区别又紧密相关,以下是具体解析: 1. 基本定义 制冷量(Cooling Capacity)指空调在单位时间内从室内环境中移除的热量,单位为 瓦特(W) 或 千卡/小时(kcal/h)。它直接反映空调的制冷能力,数值越大,制冷效果越…