这是一个系列文章,总共三篇分别讲:
Handler(一):基本原理和知识点
Handler(二):Java层源码分析
Handler(三):Native层源码分析
一. 前言
每个Android开发者应该都了解过使用过Android Handler,它在整个 Android 开发体系中占据着很重要的地位, Handler是用来解决线程通信以及线程切换问题的。
Android本身是一个事件驱动的系统, Handler 是一种标准的事件驱动模型,从某种角度可以说是有异曲同工之妙,只不过Android系统更为复杂庞大。掌握Handler 的原理对理解事件驱动模型以及学习Android都会有所帮助。
不仅是帮助笔者自身整理相关知识,也希望对其他读者有所帮助。
后文源码基于Android API 35。
本文主要分三个部分:
1.通过尝试设计一个线程通信框架来理解Handler的设计思想
2.对Handler原理分析以及知识点
3.总结
二. 尝试设计一个线程通信框架
本章节通过尝试设计一个线程通信框架来理解Handler的设计思想。
现在让我们暂时的忘掉Handler。
如果我们要自研一个线程通信框架,大概分为以下几个步骤:
- 确认通信框架应该具有的功能。
- 如何实现这些功能。
- 动手实现。
那么我们根据以上步骤思考
1. 确认通信框架应该具有的功能
确认以下几个基本功能:
-
需要满足多个线程之间互相通信。
-
满足相互通信的线程不能阻塞, 这里其实两个不阻塞和一个必然阻塞。分开说:
- 线程2与线程1通信, 线程1在处理通信数据时,线程2不能阻塞。这个好理解,总不能两个线程都在傻等。
- 线程2与线程3等100个线程都要与线程1通信,对于线程1来说,,在同一时刻,肯定只能接收执行一个线程的消息,假如说是2,那么线程2与线程3等100个线程都不能阻塞。
- 以上两种情况,线程1在处理通信数据时,只有线程1是可以阻塞的。
其实, 以上哪个线程能或不能阻塞都是我们定义的,不过毕竟一个线程阻塞总比一堆线程阻塞要好,都是取舍。
以及为了满足更高需求的高级功能:
- 满足把一个任务或者说是一个方法/一段代码发给另一个线程执行。实现线程切换。
- 满足发一个延时执行的消息或者任务。
以及为了满足使用方面的需求:
- 使用方式尽量简洁,无感(高度封装)。
2. 如何实现这些功能
实现我们上述的功能,可以考虑java常用的线程通信方式, 常用的有以下几种:
- 使用
volatile
关键字。 - 使用原子类
AtomicXXX
(CAS)。 - 使用
Condition
接口,或者说 锁ReentrantLock
等。 - 使用**
synchronized+Object.wait/notify
**常用的等待唤醒机制。
单单使用以上几个方案都不能满足我们的需求,在此需要引入一种模型:生产者-消费者模型。
生在一个生产者-消费者模式中,通常会有三个角色:生产者、消费者和消息队列。它们的工作流程如下图所示:
- 生产者: 负责生产消息,放到共享的消息队列中,生产者可以有多个。
- 消费者: 负责从共享消息队列中取出消息,执行消息对应的任务。
- 消息队列: 负责存取消息。
这三个角色中,因为生产者和消费者要从同一个消息队列取放消息,所以消息队列的数量要求是唯一的,生产者和消费者的数量可以任意。
通过生产者-消费者模型可以发现,这个模型就是一个异步模型,生产者把消息放入消息队列后,生产者就可以执行其他逻辑,不回发生阻塞。消费者从消息队列中一个个有序的把消息取出后执行,在执行时是阻塞的。
如果我们把线程通信和生产者-消费者模型结合,比如线程A和线程B通信,让线程A给线程B发消息,线程A是生产者,线程B是消费者。如果反过来,线程B给线程A发消息呢,那么现场B是生产者,线程A是消费者,那么就需要把作为消费者的线程和一个消息队列绑定使用。
还有一个小问题,消费者如何从消息队列中取数据呢?
按照生产者-消费者模型来看,数据源只有一个,就是消息队列,消费者需要不停的从队列中取数据,就需要使用一个死循环。那么又有两个问题了:1.死循环便利非常消耗cpu资源,队列中没数据的时候也要开启死循环吗? 2. 开启死循环后,就无法执行后续代码了,线程不是要卡死了?
其实,问题1可以通过线程挂起的方式解决,当没有消息的时候挂起线程就不会一直占用cpu资源,当有消息进入消息队列时,可以唤醒线程来继续读取消息执行。
问题2:确实,开启死循环后无法执行后续代码,但这并不意味着线程卡死了,只是线程以另一种方式来运行:即通过读取消息队列中的消息,执行相应代码,也就是消息驱动的方式运行。
由此,我们的通信框架就有了一个雏形。

还是以线程B给线程A发消息的场景为例,我们定义一个类H和它的实例h,h用来提供给线程B一个通信入口(这里其实因为是java中同一进程中的多个线程数据是共享的,所以传递变量h是可行的,在其他模型中或者进程间通信的情况下需要使用其他方法)。线程B通过发送数据到h再到线程A的H类中的消息队列中等待执行,线程B无需阻塞。然后执行任务类L可以从消息队列中取出数据执行,完成通信。
大概异步通信框架都可以看作是这个简单模型演化出来的,不过各种实现不同,这也是生产者-消费者模型的基本应用。
到这里我们就通过自己一步步的思考设计了一个简单模型,接下来分析Handler是如何设计与使用的,加上对源码的分析,来进一步思考,为何这样设计,优缺点在哪,最终帮助我们融会贯通,以及更进一步,学以致用。
三. Handler机制原理分析
本章节主要介绍Handler的原理,以及Handler中使用的一些技术点.
1. Handler原理
Handler消息处理机制四个主要组件(Handler+Looper+Message+MessageQueue)的作用:
1.1 Handler
Handler通信机制的入口,是生产者,同时消息的执行者,通过 Handler 提供的方法,可以发送和处理消息或 Runnable 对象。
1.2 Message
消息对象,消息载体,可以携带信息。Handler 对象最终发送的消息就是 Message 对象,Message 对象中含有该 Handler 对象的引用。线程间内存共享,使用享元模式设计。
1.3 MessageQueue
共享消息队列,底层由链表实现,Handler 发送的消息全部存储在该队列中。每个线程中只会存在一个 MessageQueue 对象。
1.4 Looper
负责维护一个消息循环(Message Loop),是消费者, 内部持有MessageQueue 并从中循环读取消息,并将他们分配给对应的 Handler 进行处理。每个线程只有一个 Looper 对象。
2. Handler 机制工作流程
- 应用程序进程创建后,相关环境准备,创建消息队列 MessageQueue,创建 Looper 实例。通过 Looper 实例启动一个无限循环,不断从 MessageQueue 中取消息。
- 发送消息,可以通过 Handler 对象发送消息,该消息会进入到 MessageQueue 中,MessageQueue 中的消息是按照执行时间进行排列,需要先执行的消息会先被取出。
- Looper 的无限循环会不断从 MessageQueue 中取消息。获取消息的方式是阻塞式的,当队列为空时,线程进入休眠状态(基于Linux的
epoll
机制,后边会讲),避免CPU资源浪费。 - Message 对象中含有 Handler 对象的引用。从 MessageQueue 获取到消息后会将消息分发给对应的Handler处理。
一个线程可以有多个 Handler 实例。每个 Handler 负责不同类型的消息或处理不同的任务,提供了更灵活的消息处理机制。
一个线程只有一个 Looper、一个 MessageQueue。同一线程中的每一个 Handler 实例都与同一个 Looper 有关。一个线程只有一个 Looper 的设计是为了保持消息队列的一致性,以确保消息的有序处理,一个线程通常只有一个消息队列是为了避免混淆和提供明确的线程间通信机制。
3. ThreadLocal用法
ThreadLocal叫做线程本地变量,表示ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,是当前线程独有的变量。
原理:
Thread 对象中有一个变量 ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap 是一个map, key为ThreadLocal 的弱引用,vaalue为 Object 。
scala
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
ThreadLocal类的主要源码:
java
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
//createMap
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
}
通过源码可get到,当在线程A中向ThreadLocal中set数据Object时,会把ThreadLocal和Object放到线程A的threadLocals 中,在线程B中执行则会放入线程B的map中,这样通过相同的ThreadLocal对象,在线程A和B下,都操作的当前线程的对象,实现了线程隔离。
在Handler中每个线程都有一个Looper用来从消息队列中取数据处理,Looper是线程唯一且隔离的,正好符合ThreadLocal的使用场景。
java
public final class Looper {
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
//主线程(UI线程)的Looper 单独处理,是static类型的,方便的获取主线程的Looper。
private static Looper sMainLooper;
//Looper 所属的线程的消息队列
final MessageQueue mQueue;
//Looper 所属的线程
final Thread mThread;
private static void prepare(boolean quitAllowed) {
...
sThreadLocal.set(new Looper(quitAllowed));
}
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
}
Looper 中有一个静态变量ThreadLocal类型的sThreadLocal ,这样不管在哪个线程,通过sThreadLocal 都能获取当前线程对应的Looper对象。
4. 同步屏障机制
我门知道,Android中主线程也是使用Handler进行通信的,而且必须在主线程进行更新UI,那么ui更新消息会不会在消息太多的时候阻塞呢?
在我们自己设计的消息队列中吗,如果消息太多肯定会阻塞ui更新消息。
应该如何解决呢?
为什么一个Looper只有一个 MessageQueue ,如果有两个可以吗?有一个普通消息队列,有一个队列作为优先队列,优先级高的消息比如ui更新消息放在里面,每次looper取数据先从优先队列取数据,如果没有再从另一个队列中取数据? 我认为是可以的,只不过需要维护两个队列,逻辑以及性能都有影响。 而且Handler中,针对上述场景有自己的解决方案,我们称之为 同步屏障。
Handler中有三种消息: 同步消息 异步消息 屏障消息。
- 同步消息:使用默认的构造函数构造handler,然后使用send方法发送。这样发送的消息都是普通消息也就是同步消息,发出去的消息就会在MessageQueue中排队。
- 异步消息:通过接口设置为异步消息。
msg.setAsynchronous(true);
正常情况下跟同步消息没有区别,只有在设置了同步屏障之后,才会出现差异。 - 屏障消息:将消息Message的target设置为空,就是屏障消息。作用是开启同步屏障。
MessageQueue 中的postSyncBarrier()
方法用来发送同步屏障,此方法被标记为hide。它的作用是让异步消息优先得到执行。当方法被调用时会往MessageQueue中添加一条屏障消息。此时Looper将优先消费异步消息,同步消息被挂起无限等待。直到调用removeSyncBarrier(token)
找到屏障消息并将其移除并回收,就会恢复所有消息的正常处理。
5. 享元模式
问题又来了,如果我们的任务比较多而且耗时短,需要发送大量的消息给Handler,则需要创建大量的Message数据,而且又会被快速销毁,容易占用内存,造成内存溢出,或者内存抖动,更容易触发GC,进而引发性能问题(如界面卡顿、响应延迟)。那么怎么解决这个问题呢?
可以通过使用享元模式,减少内存使用,降低出现问题的可能。
享元模式: 通过缓存池(共享对象)来减少创建大量相似对象时的内存消耗,和提高性能。这种类型的设计模式属于结构型模式。
优点:通过减少内存中对象的数量来减少内存消耗。 减少了对象创建的时间,提高了系统效率。
缺点:增加了设计和实现的复杂性, 可能会引起线程安全问题。
在Handler中通过Message.*obtain*()
来获取一个缓存message。
java
public static final Object sPoolSync = new Object();
private static Message sPool; //链表头
private static int sPoolSize = 0; //缓存池长度
private static final int MAX_POOL_SIZE = 50; //缓存池最大长度
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
通过源码可以看出,Message的缓存池是一个链表,sPool 一直指向链表头,obtain方法把链表头返回,然后指向下一个Message,如果缓存池中没用Message了,则取出最后一个Message后,sPool=null,后续还是会new出新的Message返回。所以享元模式只是在一定程度上缓解了内存的使用与波动。
线程安全问题是指,在Handler.handleMessage后,会调用Message.recycle()方法回收msg,并清空数据,如果该msg被传入其他异步方法中使用,可能会有同步问题。
6. IdleHandler
IdleHandler 其实就是 Handler 机制提供的一种,可以在 Looper 事件循环的过程中,当出现空闲的时候,允许我们执行任务的一种机制。在IdleHandler 中可以执行一些懒加载的任务。
那么问题来了,如何才算是空闲的时候呢?我们可以从源码中找到答案。
ini
MessageQueue.java
Message next() {
int nextPollTimeoutMillis = 0;
for (;;) {
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
//场景1: now < msg.when
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
...
return msg;
}
} else {
//场景2: msg == null
nextPollTimeoutMillis = -1;
}
...
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
mBlocked = true;
continue;
}
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
}
pendingIdleHandlerCount = 0;
nextPollTimeoutMillis = 0;
}
}
可以看出其实是在从 MessageQueue 取数据时,当没用数据能取出来,也就是没用消息需要执行时,算是空闲。队列出现空闲存在两种场景。
- MessageQueue 中最近需要处理的消息,是一个延迟消息(now < msg.when),需要滞后执行;
- MessageQueue 为空,没有消息;
这两个场景,都会尝试执行 IdleHandler。
7. handler导致内存泄漏问题(handler的妥协)
问题的由来
思考一个问题,由于Handler可以在任意位置定义,sendMessage到对应的线程可以通过线程对应的Looper--MessageQueue来执行,那handleMessage的时候,如何能找到对应的Handler来处理呢?我们可没有好的办法能直接检索到每个消息对应的Handler。
两种解决思路
- 通过公共总线,比如定义Map<Message,Handler>来索引,这种方式要求map必须定义到所有的线程都能方便获取到的地方,比如可以定义为static。
- 通过消息带Message来携带属性target到对应线程,当消息被消费后,可以通过Message来获得Handle。
第一种方式的问题比较明显,公共总线需要手动维护它的生命周期,google采用的是第二种方式。
妥协造成Handler泄露问题的根源
由于Message持有了Handler的引用,当我们通过内部类的形式定义Handler时,持有链为
Thread->MessageQueue->Message->Handler->Activity/Fragment
长生命周期的Thread持有了短生命周期的Activity,可能造成内存泄漏。
解决方式 使用静态内部类定义Handler,静态内部类不持有外部类的引用,所以使用静态的handler不会导致activity的泄露。
1. 静态内部类 + 弱引用组合方案
- 使用静态内部类
MyHandler
避免隐式持有Activity
引用。 - 通过
WeakReference
允许Activity
被GC回收。 - 在
onDestroy()
中移除所有任务,彻底切断引用链。
2. 生命周期绑定与任务清除
- 使用
Handler(Looper.getMainLooper())
确保消息队列与主线程绑定。 - 在
onDestroy()
中调用removeCallbacksAndMessages(null)
清除所有未执行的任务。
3. 使用HandlerThread
的优化方案
- 将耗时操作移至独立线程,避免阻塞主线程。
- 在
onDestroy()
中调用quitSafely()
释放资源。
四. 总结
我们现在应该对Handler有了一个初步认识,并且发现系统实现的Handler比我们自己想的,功能更多,比如:
- 通过引入ThreadLocal,让线程和Loper绑定,既确保线程独享Looper(Looper都是该线程私有的,其他线程无法直接访问),又简洁高效(使用map无需全局查找),还安全(线程间,同一个ThreadLocal不用加锁不用控制同步,在每个线程内部,多个ThreadLocal变量互不干扰)。
- 通过引入同步屏障机制,让消息产生了的优先级。
- 通过使用享元模式,减少内存使用。
- 通过IdleHandler,让Handler忙里偷闲的执行一些不紧急的任务。
- Handler中也有一些不完美,需要妥协的地方。
通过对Handler的学习,我们不仅掌握了Handler的原理,更应该学习到Handler的设计思想。当我们学过Handler之后,比如在开发中遇到类似需要线程隔离的场景,会想到ThreadLocal,更进一步,我们学过看过的东西在开发的时候能够活学活用, 就等于是站在巨人的肩膀上,才能帮助我们走的更高更远。