💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
[TOC] ## 什么是 Handler? Handler 是 Android 的一种消息处理机制,与 Looper,MessageQueue 绑定,可以用来进行线程的切换。常用于接收子线程发送的数据并在主线程中更新 UI ## Handler线程通信的原理 > 你刚说 Handler 可以切换线程,它是怎么实现的? “切换线程”其实是“线程通信”的一种。为了保证主线程不被阻塞,我们常常需要在子线程执行一些耗时任务,执行完毕后通知主线程作出相应的反应,这个过程就是线程间通信。 Linux 里有一种进程间通信的方式叫消息队列,简单来说当两个进程想要通信时,一个进程将消息放入队列中,另一个进程从这个队列中读取消息,从而实现两个进程的通信。 ![](https://img.kancloud.cn/3e/74/3e74c814cf7f372c13e568036bd90bf7_463x346.png) Handler 就是基于这一设计而实现的。在 Android 的多线程中,每个线程都有一个自己的消息队列,线程可以开启一个死循环不断地从队列中读取消息。 当 B 线程要和 A 线程通信时,只需要往 A 的消息队列中发送消息,A 的事件循环就会读取这一消息从而实现线程间通信 ![](https://img.kancloud.cn/2e/62/2e62c2579d6aaf9dfd52c5054af7aaee_628x448.png) ### 事件循环和消息队列的实现(Looper MessageQueue) Android 的事件循环和消息队列是通过 Looper 类来实现的 Looper.prepare() 是一个静态方法。它会构建出一个 Looper,同时创建一个 MessageQueue 作为 Looper 的成员变量。MessageQueue 是存放消息的队列 当调用 Looper.loop() 方法时,会在线程内部开启一个死循环,不断地从 MessageQueue 中读取消息,这就是事件循环 每个 Handler 都与一个 Looper 绑定,Looper 包含 MessageQueue ![](https://img.kancloud.cn/ee/46/ee46cdf633f39c882e4cbf16d6d74963_328x283.png) ### Looper 被存放在ThreadLocal Looper 是存放在线程中的。但如何把 Looper 存放在线程中就引入了 Android 消息机制的另一个重点 --- **ThreadLocal** 前面我们提到。Looper.prepare() 方法会创建出一个 Looper,它其实还做了一件事,就是将 Looper 放入线程的局部变量 ThreadLocal 中。 ~~~ // Looper.java#private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } // sThreadLocal是一个静态对象,类型是ThreadLocal<Looper> sThreadLocal.set(new Looper(quitAllowed)); } ~~~ ### 什么是 ThreadLocal ThreadLocal 又称线程的局部变量。它最大的神奇之处在于,**一个 ThreadLocal 实例在不同的线程中调用 get 方法可以取出不同的值。** 用一个例子来表示这种用法: ~~~ public void set(T value) { // ① 获取当前线程对象 Thread t = Thread.currentThread(); // ② 获取线程的成员属性map ThreadLocalMap map = getMap(t); // ③ 将value放入map中,如果map为空则创建map if (map != null) map.set(this, value); else createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } private Entry[] table; ~~~ **ThreadLocal.set 可以将一个实例变成线程的成员变量** 因为 Looper 要放在线程中的,每个线程只需要一个事件循环,只需要一个 Looper。事件循环是个死循环,多余的事件循环毫无意义。ThreadLocal.set 可以将 Looper 设置为线程的成员变量 同时为了方便在不同线程中获取到 Looper,Android 提供了一个静态对象 Looper.sThreadLocal。这样在线程内部调用 sThreadLocal.get 就可以获取线程对应的 Looper 对象 综上所述,使用 ThreadLocal 作为 Looper 的设置和获取工具是十分方便合理哒 ## Looper 的这个死循环会一直“空转” 当然不会!如果事件循环中没有消息要处理但仍然执行循环,相当于无意义的浪费 CPU 资源!Android 是不允许这样的 为了解决这个问题,在 MessageQueue 中,有两个 native 方法,`nativePollOnce` 和 `nativeWake`。 nativePollOnce 表示进行一次轮询,来查找是否有可以处理的消息,如果没有就阻塞线程,让出 CPU 资源 nativeWake 表示唤醒线程 所以这两个方法的调用时机也就显而易见了 ~~~ // MessageQueue.java boolean enqueueMessage(Message msg, long when) { ··· if (needWake) { nativeWake(mPtr); } ··· } ~~~ 在 MessageQueue 类中,`enqueueMessage` 方法用来将消息入队,如果此时线程是阻塞的,调用 `nativeWake` 唤醒线程 ~~~ // MessageQueue.java Message next() { ··· nativePollOnce(ptr, nextPollTimeoutMillis); ··· } 复制代码 ~~~ `next()` 方法用来取出消息。取之前调用 `nativePollOnce()` 查询是否有可以处理的消息,如果没有则阻塞线程。等待消息入队时唤醒。 ### nativePollOnce 与 nativeWake 在linux新的内核中使用了epoll来替换它,相比于select,epoll最大的好处在于它不会随着监听文件描述符数目的增长而效率降低,select机制是采用轮询来处理的,轮询的fd数目越多,效率也就越低。epoll的接口非常简单就只有三个函数: int epoll_create(int size);创建一个epoll句柄,当这个句柄创建完成之后,在/proc/进程id/fd中可以看到这个fd。 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);注册事件函数。 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);等待事件的发生,参数timeout是超时时间毫秒值,0会立即返回,-1将不确定,也就是说有可能永久阻塞。该函数返回需要处理的事件数目,如返回0表示已超时。 ### Looper 是个死循环,为什么不会导致 ANR 首先要明确一下概念。**ANR 是应用在特定时间内无法响应一个事件时抛出的异常。** 典型例子的是在主线程中执行耗时任务。当一个触摸事件来临时,主线程忙于处理耗时任务而无法在 5s 内响应触摸事件,此时就会抛出 ANR。 但 Looper 死循环是事件循环的基石,本身就是 Android 用来处理一个个事件的。正常情况下,触摸事件会加入到这个循环中被处理。但如果前一个事件太过耗时,下一个事件等待时间太长超出特定时间,这时才会产生 ANR。所以 Looper 死循环并不是产生 ANR 的原因。 ## 消息队列中的消息是如何进行排序 这个就要看 MessageQueue 的 enqueueMessage 方法了 enqueueMessage 是消息的入队方法。Handler 在进行线程间通信时,会调用 sendMessage 将消息发送到接收消息的线程的消息队列中,消息队列调用 enqueueMessage 将消息入队。 ~~~ // MessageQueue.java boolean enqueueMessage(Message msg, long when) { synchronized (this) { // ① when是消息入队的时间 msg.when = when; // ② mMessages是链表的头指针,p是哨兵指针 Message p = mMessages; boolean needWake; if (p == null || when == 0 || when < p.when) { msg.next = p; mMessages = msg; needWake = mBlocked; } else { needWake = mBlocked && p.target == null && msg.isAsynchronous(); Message prev; for (;;) { prev = p; p = p.next; // ③ 遍历链表,比较when找到插入位置 if (p == null || when < p.when) { break; } if (needWake && p.isAsynchronous()) { needWake = false; } } // ④ 将msg插入到链表中 msg.next = p; prev.next = msg; } if (needWake) { nativeWake(mPtr); } } return true; } ~~~ 消息入队分为 3 步: ① 将入队的时间绑定在 when 属性上 ② 遍历链表,通过比较 when 找到插入位置 ③ 将 msg 插入到链表中 这就是消息的排序方式 ## 异步消息和同步屏障 在 Android 的消息机制中,消息分为**同步消息**、**异步消息**和**同步屏障**三种。(没错,同步屏障是 target 属性为 null 的特殊消息)。通常我们调用 sendMessage 方法发送的是同步消息。异步消息需要和同步屏障配合使用,来提升消息的优先级。 同步屏障理解起来其实很简单。刚才说同步屏障是一种特殊的消息,当事件循环检测到同步屏障时,之后的行为不再像之前那样根据 when 的值一个个取消息,而是遍历整个消息队列,查找到异步消息取出并执行。 这个特殊的消息在消息队列中像一个标志,事件循环探测到它时就改变原来的行为,转而去查找异步消息。表现上看起来像一个屏障一样拦住了同步消息。所以形象地称为同步屏障。 源码实现非常非常简单: ~~~ //MessageQueue.java Message next() { ··· // ① target为null表明是同步屏障 if (msg != null && msg.target == null) { // ② 取出异步消息 do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()); } ··· } ~~~ ### 同步屏障,不移除,会发生什么事呢? 同步屏障是用来“拦住”同步消息,处理异步消息的。如果同步屏障不移除,消息队列里的异步消息会一个一个被取出处理,直到异步消息被取完。如果此时队列中没有异步消息了,则线程会阻塞,队列中的同步消息永远不会执行。所以同步屏障要及时移除。 ### 那你知道同步屏障的应用场景 同步屏障的核心作用是提高消息优先级,保证 Message 被优先处理。Android 为了避免卡顿,应用在了 view 绘制中。具体可以看之前关于 view 绘制的总结~ ## Handler相关的内存泄漏问题 内存泄漏归根到底其实是生命周期“错位”导致的:**一个对象本来应该在一个短的生命周期中被回收,结果被一个长生命周期的对象引用,导致无法回收。** Handler 的内存泄漏其实是内部类持有外部类引用导致的。 形成方式有两种: (1)匿名内部类持有外部类引用 ~~~ class Activity { var a = 10 fun postRunnable() { Handler(Looper.getMainLooper()).post(object : Runnable { override fun run() { this@Activity.a = 20 } }) } } ~~~ Handler 在发送消息时,message.target 属性就是 handler 本身。message 被发送到消息队列中,被线程持有,线程是一个无比“长”生命周期的对象,导致 activity 无法被及时回收从而引起内存泄漏。 解决办法是在 activity destory 时及时移除 runnable (2)非静态内部类持有外部类引用 ~~~ //非静态内部类 protected class AppHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { } } } ~~~ 解决方案是用静态内部类,并将外部引用改为弱引用 ~~~ private static class AppHandler extends Handler { //弱引用,在垃圾回收时,被回收 WeakReference<Activity> activity; AppHandler(Activity activity){ this.activity = new WeakReference<Activity>(activity); } public void handleMessage(Message message){ switch (message.what){ } } } ~~~ ## HandlerThread HandlerThread 顾名思义就是 Handler+Thread 的结合体,它本质上是一个 Thread。 我们知道,子线程是需要我们通过 Looper.prepare()和 Looper.loop()手动开启事件循环的。HandlerThread 其实就帮我们做了这件事,它是一个实现了事件循环的线程。我们可以在这个线程中做一些 IO 耗时操作。 ## IdleHandler IdleHandler 虽然叫 Handler,其实和同步屏障一样是一种特殊的”消息"。不同于 Message,它是一个接口 ~~~ public static interface IdleHandler{ boolean queueIdle(); } 复制代码 ~~~ Idle 是空闲的意思。与同步屏障不同,同步屏障是提高异步消息的优先级使其优先执行,IdleHandler 是事件循环出现空闲的时候来执行。 这里的“空闲”主要指两种情况 (1)消息队列为空 (2)消息队列不为空但全部是延时消息,也就是 msg.when > now 利用这一特性,我们可以将一些不重要的初始化操作放在 IdleHandler 中执行,以此加快 app 启动速度;由于 View 的绘制是事件驱动的,我们也可以在主线程的事件循环中添加一个 IdleHandler 来作为 View 绘制完成的回调,等等。 但应该注意的是,如果主线程中一直有任务执行,IdleHandler 被执行的时机会无限延后,使用的时候要注意哦~ ## 参考资料 [【面试官爸爸】继续给我讲讲Handler?](https://juejin.cn/post/6995341995015143432)