当前位置: 首页 > news >正文

深入理解 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.xdnf.cn/news/15553.html

相关文章:

  • Rocky8 升级 Python 3.9.20 并部署 Airflow 2.10.5
  • STM32F407的引脚说明
  • ActivityRecord、TaskRecord、ActivityStack、ActivityStackSupervisor、ProcessRecord
  • 【工具】在Cursor/VS Code中配置Python调试环境的完整指南
  • VASP 6.4.1 Ubuntu系统编译安装手册
  • STM32学习2
  • LeadeRobot具身智能应用标杆:无人机X柔韧具身智能,空中精准作业游刃有余
  • Python 浮点数运算之谜:深入解析round(0.675, 2)等输出异常
  • 人工智能在WEB开发中的应用与实践
  • string函数具体事例
  • 数字化音乐教育软件 UI 设计的关键要点
  • 如何删除 Launchpad 中 Chrome 的图标
  • orcad csi 17.4 DRC规则设置及检查
  • 使用人工智能大模型kimi,如何免费制作PPT?
  • flutter app实现分辨率自适应的图片资源加载
  • 论文阅读:2023 arxiv Safe RLHF: Safe Reinforcement Learning from Human Feedback
  • Git-使用教程(新手向)
  • STM32CubeMX-H7-15-SPI通信协议读写W25Q64
  • 【springsecurity oauth2授权中心】简单案例跑通流程
  • 游戏APP如何抵御DDoS攻击与黑客勒索?实战防护全攻略
  • Java中的函数式编程详解
  • 【笔记】【C++】【基础语法】作用域(scope)、持续时间(duration)和链接(linkage)
  • OpenStack Yoga版安装笔记(22)Swift笔记20250418
  • 【Java面试系列】Spring Boot微服务架构下的分布式事务设计与实现详解 - 3-5年Java开发必备知识
  • 浏览器的存储机制 - Storage
  • 元宇宙概念兴起,B 端数字孪生迎来哪些新机遇?
  • leetcode-sql数据库面试题冲刺(高频SQL五十题)
  • 03、GPIO外设(三):标准库代码示例
  • 第11篇:Linux程序访问控制FPGA端HEX<四>
  • 服务器架构:SMP、NUMA、MPP及Docker优化指南