文章已授权『郭霖』公众号发布
前言
很高兴遇见你~ 欢迎阅读我的文章。
关于Handler的博客可谓是俯拾皆是,而这也是一个老生常谈的话题,可见的他非常基础,也非常重要。但很多的博客,却很少有从入门开始介绍,这在我一开始学习的时候就直接给我讲Looper讲阻塞,非常难以理解。同时,也很少有系统地讲解关于Handler的一切,知识比较零散。我希望写一篇从入门到深入,系统地全面地讲解Handler的文章,帮助大家认识Handler。
这篇文章的讲解深度循序渐进,不同程序的读者可选择对应的部分查看:
- 第一部分是对于Handler的入门概述。了解一个新事物,需要问三个问题:是什么、为什么、怎么用。包括关于Handler的结构等都有介绍。
- 第二部分是在对Handler有一定的认知基础上,对各个类进行详细的讲解和源码分析。
- 第三部分是整体的流程分析以及常见问题的解析。
- 最后一部分是Android对于消息机制设计的讲解以及全文总结。
文章基本涵盖了关于Handler相关的知识,因而篇幅也比较长
考虑过把文章分割成几篇小文章,考虑到阅读的整体性以及方便性,最终还是集成了一篇大文章
文章成体系,全面地讲解知识点,而不是把知识碎片化,否则很难真正去理解单一的知识,更不易于对整体知识的把握
读者可自行选择感兴趣的章节阅读
那么,我们开始吧。
概述
什么是Handler?
准确来说,是Handler机制,Handler只是Handler机制中的一个角色。只是我们对Handler接触比较多,所以经常以Handler来代称。
Handler机制是Android中基于单线消息队列模式的一套线程消息机制。
他的本质是消息机制,负责消息的分发以及处理。这样讲可能有点抽象,不太容易理解。什么是“单线消息队列模式”?什么是“消息”?
通俗点来说,每个线程都有一个“流水线”,我们可往这条流水线上放“消息”,流水线的末端有工作人员会去处理这些消息。因为流水线是单线的,所有消息都必须按照先来后到的形式依次处理(在Handler机制中有“加急线”:同步屏障,这个后面讲)。如下图:

放什么消息以及怎么处理消息,是需要我们去自定义的。Handler机制相当于提供了这样的一套模式,我们只需要“放消息到流水线上”,“编写这些消息的处理逻辑”就可以了,流水线会源源不断把消息运送到末端处理。最后注意重点:每个线程只有一个“流水线”,他的基本范围是线程,负责线程内的通信以及线程间的通信。每个线程可以看成一个厂房,每个厂房只有一个生产线。
两个关键问题
了解Handler的作用前需要了解Handler背景下的两个关键问题:
- 不能在非UI创建线程去操作UI
- 不能在主线程执行耗时任务
我们普遍的认知是:不能在非主线程更新UI。但这是不准确的,如果我们在子线程更新了UI,看看报错信息是什么:

笔者留下了英语渣渣的眼泪,百度翻译一下:

只有创建视图层次结构的原始线程才能访问其视图。但为什么我们一直都说是非主线程不能更新ui?这是因为我们的界面一般都是由主线程进行绘制的,所以界面的更新也就一般都限制在主线程内。这个异常是在viewRootIimpl.checkThread()方法中抛出来的,那可不可以绕过他?当然可以,在他还没创建出来的时候就可以偷偷更新ui了。阅读过Activity启动流程的读者知道,ViewRootImpl是在onCreate方法之后被创建的,所以我们可以在onCreate方法中创建个子线程偷偷更新UI。(Actvity启动流程解析传送门)但还是那句话,可以,但没必要去绕过这个限制,因为这是谷歌为了我们的程序更加安全而设计的。
为什么不能在子线程去更新UI?因为这会让界面产生不可预期的结果。例如主线程在绘制一个按钮,绘制一半另一个线程突然过来把按钮的大小改成两倍大,这个时候再回去主线程继续执行绘制逻辑,这个绘制的效果就会出现问题。所以UI的访问是决不能是并发的。但,子线程又想更新UI,怎么办?加锁。加锁确实可以解决这个问题,但是会带来另外的问题:界面卡顿。锁对于性能是有消耗的,是比较重量级的操作,而ui操作讲究快准狠,加锁会让ui操作性能大打折扣。那有什么更好的方法?Handler就是解决这个问题的。
第二个问题,不能在主线程执行耗时操作。耗时操作包括网络请求、数据库操作等等,这些操作会导致ANR(Application Not Responding)。这个是比较好理解的,没有什么问题,但是这两个问题结合起来,就有大问题了。数据请求一般是耗时操作,必须在子线程进行请求,而当请求完成之后又必须更新UI,UI又只能在主线程更新,这就导致必须切换线程执行代码,上面讨论了加锁是不可取的,那么Handler的重要性就体现出来了。
不用Handler可不可以?可以,但没必要。Handler是谷歌设计来方便开发者切换线程以及处理消息,然后你说我偏不用,我自己用Java工具类,自己弄个出来不可以吗?那。。。请收下小的膝盖。
为什么要有Handler?
先给结论:
- 切换代码执行的线程
- 按顺序规则地处理消息,避免并发
- 阻塞线程,避免让线程结束
- 延迟处理消息
第一个作用是最明显也是最常用的,上一部分已经讲了Handler存在的必要性,android限制了不能在非UI创建线程去操作UI,同时不能在主线程执行耗时任务,所以我们一般是在子线程执行网络请求等耗时操作请求数据,然后再切换到主线程来更新UI。这个时候就必须用到Handler来切换线程了。上面讨论过了这里不再赘述。
这里有一个误区是:我们的activity是执行在主线程的,我们在网络请求完成之后回调主线程的方法不就切换到主线程了吗?咳咳,不要笑,不要觉得这种低级错误太离谱,很多童鞋刚开始接触开发的时候都会犯这个思维错误。这其实是理解错了线程这个概念。代码本身并没有限制运行在哪个线程,代码执行的线程环境取决于你的执行逻辑是在哪个线程。这样讲可能还是有点抽象。例如现在有一个方法void test(){}
,然后两个不同的线程去调用它:
new Thread(){
// 第一个线程调用
test();
}.start();
new Thread(){
// 第二个线程调用
test();
}
此时虽然都是test这个方法,但是他的执行逻辑是由不同的线程调用的,所以他是执行在两个不同的线程环境下。而当我们想要把逻辑切换到另一个线程去执行的时候,就需要用到Handler来切换逻辑。
第二个作用可能看着有点懵。但其实他解决了另一个问题:并发操作。虽然切换线程解决了,如果主线程正在绘制一个按钮,刚测量好按钮的长宽,突然子线程一个新的请求过来打断了,先停下这边的绘制操作,把按钮改成了两倍大,然后逻辑切回来继续绘制,这个时候之前的测量的长宽已经是不准确的了,绘制的结果肯定也不准确。怎么解决?单线消息队列模型。在讲什么是Handler那部分简单介绍过,就是相当于一个流水线一样的模型。子线程的请求会变成一个个的消息,然后主线程依次处理,那么就不会出现绘制一半被打断的问题了。
同时这种模型也不止用于解决ui并发问题,在ActivityThread中有一个H类,他其实就是个Handler。在ActivityThread中定义了一百多中消息类型以及对应的处理逻辑,这样,当需要让ActivityThread处理某一个逻辑的时候,只需要发送对应的消息给他即可,而且可以保证消息按顺序执行,例如先调用onCreate再调用onResume。而如果没有Hanlder的话,就需要让ActivityThread有一百多个接口对外开放,同时还需要不断进行回调保证任务按顺序执行。这显然复杂了非常多。
我们执行一个Java程序的时候,从main方法入口,执行完成之后,马上就退出了,但是我们android应用程序肯定是不可以的,他需要一直等待用户的操作。而Handler机制就解决了这个问题,但消息队列中没有任务的时候,他就会把线程阻塞,等到有新的任务的时候,再重新启动处理消息。
第四个作用让延迟处理消息得到了最佳解决方案。假如你想让应用启动5秒后界面弹出一个对话框,没有handler的情况下,会如何处理?开一个Thread然后使用Thread.sleep让线程睡眠一对应的时间对吧,但如果多个延迟任务呢?而开启线程也是个比较重量级的操作且线程的数量有限。而可以直接给Handler发送延迟对应时间的消息,他会在对应时间之后准时处理该消息(当然有特殊情况,如单件消息处理时间过长或者同步屏障,后面会讲到)。而且无论发送多少延迟消息都不会对性能有任何影响。同时,也是通过这个功能来记录ANR的时间。
讲这些作用可能读者心中并没有一个很形象的概念,也可能看完就忘了。但是关于Handler的定义不能忘:Handler机制是Android中基于单线消息队列模式的一套线程消息机制。,上述四个作用是为了让读者更好地理解Handler机制。
如何使用Handler
我们平常使用Handler有两种不同的创建方式,但总体流程是相同的:
- 创建Looper
- 使用Looper创建Handler
- 启动Looper
- 使用Handler发送信息
Looper可理解为循环器,就像“流水线”上的滚带,后面会详细讲到。每个线程只有一个Looper,通常主线程已经创建好了,追溯应用程序启动流程可以知道启动过程中调用了Looper.prepareMainLooper,而在子线程就必须使用如下方法来初始化Looper:
Looper.prepare();
第二步是创建Handler,也是最熟悉的一步。我们有两种方法来创建Handler:传入callBack对象和继承。如下:
public class MainActivity extends AppComposeActivity{
...;
// 第一种方法:使用callBack创建handler
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
Handler handler = Handler(Looper.myLooper(),new CallBack(){
public Boolean handleMessage(Message msg) {
TODO("Not yet implemented")
}
});
}
// 第二种方法:继承Handler并重写handlerMessage方法
static MyHandler extends Hanlder{
public MyHandler(Looper looper){
super(looper);
}
@Override
public void handleMessage(Message msg){
super.handleMessage(msg);
// TODO(重写这个方法)
}
}
}
注意第二种方法,要使用静态内部类,不然可能会造成内存泄露。原因是非静态内部类会持有外部类的引用,而Handler发出的Message会持有Handler的引用。如果这个Message是个延迟的消息,此时activity被退出了,但Message依然在“流水线”上,Message->handler->activity,那么activity就无法被回收,导致内存泄露。
两种Handler的写法各有千秋,继承法可以写比较复杂的逻辑,callback法适合比价简单的逻辑,看具体的业务来选择。
然后再调用Looper的loope方法来启动Looper:
Looper.loop();
最后就是使用Handler来发送信息了。当我们获得handler的实例之后,就可以通过他的sendMessage相方法和post相关方法来发送信息,如下:
handler.sendMessage(msg);
handler.sendMessageDelayed(msg,delayTime);
handler.post(runnable);
handler.postDelayed(runnable,delayTime);
然后一般情况下是哪个Handler发出的信息,最终由哪个Handler来处理。这样,只要我们拿到Handler对象,就可以往对应的线程发送信息了。
Handler内部模式结构
经过前面的介绍对于Looper已经有了一定的认知,但可能对他内部的模式还不太清楚。这一部分先讲解Handler的大概内部模式,目的是为下面的详解做铺垫,为做整体概念感知。先上图:

Handler机制内部有三大关键角色:Handler,Looper,MessageQueue。其中MessageQueue是Looper内部的一个对象,MessageQueue和Looper每个线程有且只有一个,而Handler是可以有很多个的。他们的工作流程是:
- 用户使用线程的Looper构建Handler之后,通过Handler的send和post方法发送消息
- 消息会加入到MessageQueue中,等待Looper获取处理
- Looper会不断地从MessageQueue中获取Message然后交付给对应的Handler处理
这就是大名鼎鼎的Handler机制内部模式了,说难,其实也是很简单。
Handler机制关键类
一、ThreadLocal
概述
ThreadLocal是Java中一个用于线程内部存储数据的工具类。
ThreadLocal是用来存储数据的,但是每个线程只能访问到各自线程的数据。我们一般的用法是:
ThreadLocal<String> stringLocal = new ThreadLocal<>();
stringLocal.set("java");
String s = stringLocal.get();
不同的线程之间访问到的数据是不一样的:
public static void main(String[] args){
ThreadLocal<String> stringLocal = new ThreadLocal<>();
stringLocal.set("java");
System.out.println(stringLocal.get());
new Thread(){
System.out.println(stringLocal.get());
}
}
结果:
java
null
线程只能访问到自己线程存储的数据。
ThreadLocal的作用
ThreadLocal的特性适用于同样的数据类型,不同的线程有不同的备份情况,如我们这篇文章一直在讲的Looper。每个线程都有一个对象,但是不同线程的Looper是不一样的,这个时候就特别适合使用ThreadLocal来存储数据,这也是为什么这里要讲ThreadLocal的原因
ThreadLocal内部结构
ThreadLocal的内部机制结构如下:

每个Thread,也就是每个线程内部维护有一个ThreadLocalMap,ThreadLocalMap内部存储多个Entry。Entry可以理解为键值对,他的本质是一个弱引用,内部有一个object类型的内部变量,如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry是ThreadLocalMap的一个静态内部类,这样每个Entry里面就维护了一个ThreadLocal和ThreadLocal泛型对象。每个线程的内部维护有一个Entry数组,并通过hash算法使得读取数据的速度达到O(1)。由于不同的线程对应的Thread对象不同,所以对应的ThreadLocalMap肯定也不同,这样只有获取到Thread对象才能获取到其内部的数据,数据就被隔离在不同的线程内部了。
ThreadLocal工作流程
那ThreadLocal是怎么实现把数据存储在不同线程中的?先从他的set方法入手:
TheadLocal.class
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
逻辑不是很复杂,首先获取当前线程的Thread对象,然后再获取Thread的ThreadLocalMap对象,如果该map对象不存在则创建一个并调用他的set方法把数据存储起来。我们继续看ThreadLocalMap的set方法:
ThreadLocalMap.class
private void set(ThreadLocal<?> key, Object value) {
// 每个ThreadLocalMap内部都有一个Entry数组
Entry[] tab = table;
int len = tab.length;
// 获取新的ThreadLocal在Entry数组中的下标
int i = key.threadLocalHashCode & (len-1);
// 判断当前位置是否发生了Hash冲突
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果数据存在且相同则直接返回
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 若当前位置没有其他元素则直接把新的Entry对象放入
tab[i] = new Entry(key, value);
int sz = ++size;
// 判断是否需要对数组进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
这里的逻辑和HashMap是很像的,我们可以直接使用HashMap的思维来理解ThreadLocalMap:ThreadLocalMap的key是ThreadLocal,value是ThreadLocal对应的泛型。他的存储步骤如下:
- 根据自身的threadLocalHashCode与数组的长度进行相与得到下标
- 如果此下标为空,则直接插入
- 如果此下标已经有元素,则判断两者的ThreadLocal是否相同,相同则更新value后返回,否则找下一个下标
- 直到找到合适的位置把entry对象插入
- 最后判断是否需要对entry数组进行扩容
是不是和HashMap非常像?和HashMap的不同是:hash算法不一样,以及这里使用的是开发地址法,而HashMap使用的是链表法。ThreadLocalMap牺牲一定的空间来换取更快的速度。具体的Hash算法这里就不再深入了,有兴趣的读者可以阅读这篇文章ThreadLocal传送门
然后继续看ThreadLocal的get方法:
ThreadLocal.class
public T get() {
// 获取当前线程的ThreadLocalMap
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 根据ThreadLocal获取Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果没找到也会执行初始化工作
if (e != null) {
@SuppressWarnings("unchecked")
// 把获取到的对象进行返回
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
前面讲到ThreadLocalMap其实非常像一个HashMap,他的get方法也是一样的。使用ThreadLocal作为key获取到对应的Entry,再把value返回即可。如果map尚未初始化则会执行初始化操作。下面继续看下ThreadLocalMap的get方法:
ThreadLocalMap.class
private Entry getEntry(ThreadLocal<?> key) {
// 根据hash算法找到下标
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 找到数据则返回,否则通过开发地址法寻找下一个下标
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
利用ThreadLocal的threadLocalHashCode得到下标,然后根据下标找到数据。没找到则根据算法寻找下个下标。
内存泄露问题
我们会发现Entry中,ThreadLocal是一个弱引用,而value则是强引用。如果外部没有对ThreadLocal的任何引用,那么ThreadLocal就会被回收,此时其对应的value也就变得没有意义了,但是却无法被回收,这就造成了内存泄露。怎么解决?在ThreadLocal回收的时候记得调用其remove方法把entry移除,防止内存泄露。
ThreadLocal总结
ThreadLocal适合用于在不同线程作用域的数据备份
ThreadLocal机制通过在每个线程维护一个ThreadLocalMap,其key为ThreadLocal,value为ThreadLocal对应的泛型对象,这样每个ThreadLocal就可以作为key将不同的value存储在不同Thread的Map中,当获取数据的时候,同个ThreadLocal就可以在不同线程的Map中得到不同的数据,如下图:

ThreadLocalMap类似于一个改版的HashMap,内部也是使用数组和Hash算法来存储数据,使得存储和读取的速度非常快。
同时使用ThreadLocal需要注意内存泄露问题,当ThreadLocal不再使用的时候,需要通过remove方法把value移除。
二、Message
概述
Message是负责承载消息的类,主要是关注他的内部属性:
// 用户自定义,主要用于辨别Message的类型
public int what;
// 用于存储一些整型数据
public int arg1;
public int arg2;
// 可放入一个可序列化对象
public Object obj;
// Bundle数据
Bundle data;
// Message处理的时间。相对于1970.1.1而言的时间
// 对用户不可见
public long when;
// 处理这个Message的Handler
// 对用户不可见
Handler target;
// 当我们使用Handler的post方法时候就是把runnable对象封装成Message
// 对用户不可见
Runnable callback;
// MessageQueue是一个链表,next表示下一个
// 对用户不可见
Message next;
循环利用Message
当我们获取Message的时候,官方建议是通过Message.obtain()方法来获取,当使用完之后使用recycle()方法来回收循环利用。而不是直接new一个新的对象:
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null)