深入理解 Android Handler

一、引言

Handler 在安卓中的地位是不言而喻的,几乎维系着整个安卓程序运行的生命周期,但是这么重要的一个东西,我们真的了解它吗?下面跟随着我的脚步,慢慢揭开Hanler的神秘面纱吧!

本文将介绍Handler 的运行机制、MessageQueue、Looper 的关系,ThreadLocal,以及Handler 导致的内存泄漏问题


二、Handler 系统组成概览

Handler 的源码中,主要涉及以下核心组件:

  • Message:封装消息的数据结构。
  • MessageQueue:存储 Message 的队列,内部是单链表
  • Looper:负责循环读取 MessageQueue 并分发消息。
  • Handler:对外提供 sendMessage()post() 发送消息,并处理 MessageQueue 中的消息。

它们之间关系如下图所示:


三、Handler 的创建

Handler 被创建时,它会绑定当前线程的 Looper

java 复制代码
public Handler() {
    this(Looper.myLooper(), null, false);
}
java 复制代码
public Handler(Looper looper) {
    this(looper, null, false);
}

最终调用:

java 复制代码
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() 时:

java 复制代码
handler.sendMessage(msg);

实际上调用:

java 复制代码
public boolean sendMessage(Message msg) {
    return sendMessageDelayed(msg, 0);
}

最终:

java 复制代码
public boolean sendMessageDelayed(Message msg, long delayMillis) {
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

最终调用 enqueueMessage()

java 复制代码
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this; // 绑定 Handler
    return queue.enqueueMessage(msg, uptimeMillis);
}
java 复制代码
@UnsupportedAppUsage
/*package*/ Handler target;

也就是说 Message 引用了 Handler,这也为内存泄漏埋下伏笔


五、MessageQueue 插入消息

java 复制代码
boolean enqueueMessage(Message msg, long when) {
    synchronized (this) {
        // 插入 MessageQueue
                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.next
                prev.next = msg;
            }
    }
    return true;
}

enqueueMessage 方法负责将消息按照时间顺序正确地插入到单链表结构的队列 中,按 when 进行排序。


六、Looper 如何处理消息

Looper.loop() 读取消息
java 复制代码
public static void loop() {
    for (;;) {
        Message msg = queue.next(); // 取出消息
        //...
        msg.target.dispatchMessage(msg); // 交给 Handler 处理
    }
}
MessageQueue.next()
java 复制代码
Message next() {
    // 检查消息队列是否已销毁,若销毁则返回 null
    if (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;
            }
            // 若消息队列正在退出,释放资源并返回 null
            if (mQuitting) {
                dispose();
                return null;
            }
        }
    }
}

nativePollOnce() 让当前线程进入阻塞状态,直到有新的消息到来或者超时

nativePollOnce() 的主要功能是:

  • 线程阻塞:让当前线程进入等待状态,避免空转消耗CPU资源
  • 事件唤醒:当有新消息到达或超时发生时,立即唤醒线程处理
  • Native 层集成:与 Linux 的 epoll 机制对接,实现高效I/O多路复用
java 复制代码
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 层调用
java 复制代码
// MessageQueue.java
private native void nativePollOnce(long ptr, int timeoutMillis);

JNI 本地方法 ,由 MessageQueue 调用,用于等待消息。

MessageQueue.next() 方法中:

java 复制代码
// MessageQueue.java
nativePollOnce(mPtr, nextPollTimeoutMillis);

它的作用是:

  • 如果 MessageQueue 里有消息,立即返回。
  • 如果没有消息 ,则阻塞,直到有新的消息到来或 timeoutMillis 超时。
JNI 层调用
cpp 复制代码
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

cpp 复制代码
void MessageQueue::pollOnce(int timeoutMillis) {
    mLooper->pollOnce(timeoutMillis);
}

调用了 Looper::pollOnce(),进入 消息轮询 逻辑。

Looper 的 pollOnce()

Looper.cpp

cpp 复制代码
int Looper::pollOnce(int timeoutMillis) {
    return pollInner(timeoutMillis);
}

这里调用 pollInner(timeoutMillis),它的核心逻辑是 使用 epoll_wait() 监听事件

epoll 监听消息事件

pollInner(timeoutMillis) 的核心逻辑:

cpp 复制代码
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 写入数据:
bash 复制代码
  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 处理消息

java 复制代码
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        msg.callback.run();
    } else {
        handleMessage(msg);
    }
}

最终执行:

java 复制代码
@Override
public void handleMessage(Message msg) {
    // 需要用户实现
}

九、核心组件之间的关系

复制代码
Thread
  └── ThreadLocal<Looper>
        └── Looper
              └── MessageQueue
                    └── Message1 → Message2 → ...
                          ↑
                       Handler
  • Handler 持有对 MessageQueue 的引用(间接通过 Looper)因为Handler中的 MessageQueue 是从 Looper 中获取的;
java 复制代码
    public Handler(@Nullable Callback callback, boolean async) {
        //..
        mQueue = mLooper.mQueue;
        //..
    }
  • 每个线程通过 ThreadLocal 绑定自己的 Looper;
  • Looper 管理其对应的 MessageQueue;

这样它们的关系就清晰了,每个线程只有一个Looper(是由ThreadLocal确保的),可以有多个Handler。

java 复制代码
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中 匿名内部类或非静态内部类会持有外部类的引用,如下:

java 复制代码
public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // 处理消息
        }
    };

    @Override
    protected 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 关联:

java 复制代码
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this; // 绑定 Handler
    return queue.enqueueMessage(msg, uptimeMillis);
}

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

java 复制代码
public final class Looper {
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    @UnsupportedAppUsage
    private static Looper sMainLooper;  // guarded by Looper.class
    //...
}

我们都知道,在主线程中,主线程的Looper会一直运行下去(或者说 Looper被 静态 ThreadLocal<Looper> 所引用),不能被停止,而MessageQueue 又被Looper 所引用,这就产生了一条完整的引用链:ThreadLocal<Looper> - Looper - MessageQueue - Message - Handler - MainActivity

** 解决方案**
  • 使用静态内部类 + WeakReference:

要解决内存泄漏,就是把引用链上任意一条引用断开,让GC不可达就行了,其实我们能操作的就只有 Handler - **MainActivity **这一条引用:

java 复制代码
static class MyHandler extends Handler {
    private final WeakReference<MyActivity> ref;
    MyHandler(MyActivity activity) {
        ref = new WeakReference<>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
        MyActivity activity = ref.get();
        if (activity != null) {
            // Safe to use activity
        }
    }
}
  • 在 Activity 的 onDestroy() 中清除消息:
java 复制代码
handler.removeCallbacksAndMessages(null);

其实,只要消息不是延迟很久或者反复堆积,就不会在 MessageQueue 中长时间滞留,从而也就不会延长 Handler 或其持有对象的生命周期。

想想,在实际开发中,谁会在Activity中延迟发送一个很长时间的消息,所以我们不必为 Handler 导致内存泄漏,过度紧张,稍微留意一下就可以避免了 😃


十一、最后

Handler 是 Android 消息机制的基础组成部分。通过对 Handler、Looper、MessageQueue 之间关系的理解,我们可以更深入掌握 Android 的线程模型与 UI 更新流程。

由于本人能力有限,并没有对 Handler 进行过度深入全面了解,比如同步屏障等,如果文章内容解读有误,还望不吝赐教。

相关推荐
_一条咸鱼_11 分钟前
Android ARouter 处理器模块深度剖析(三)
android·面试·android jetpack
nofaluse13 分钟前
JavaWeb开发——文件上传
java·spring boot
_一条咸鱼_17 分钟前
Android ARouter 基础库模块深度剖析(四)
android·面试·android jetpack
難釋懷17 分钟前
bash的特性-bash中的引号
开发语言·chrome·bash
_一条咸鱼_42 分钟前
Android ARouter 核心路由模块原理深度剖析(一)
android·面试·android jetpack
火柴就是我1 小时前
android 基于 PhotoEditor 这个库 开发类似于dlabel的功能
android
爱的叹息1 小时前
【java实现+4种变体完整例子】排序算法中【插入排序】的详细解析,包含基础实现、常见变体的完整代码示例,以及各变体的对比表格
java·算法·排序算法
_一条咸鱼_1 小时前
Android ARouter 编译器模块深度剖析(二)
android·面试·android jetpack
Hello eveybody1 小时前
C++按位与(&)、按位或(|)和按位异或(^)
开发语言·c++
爱的叹息1 小时前
【java实现+4种变体完整例子】排序算法中【快速排序】的详细解析,包含基础实现、常见变体的完整代码示例,以及各变体的对比表格
java·算法·排序算法