一、Handler的重要性
- handler作为高级安卓面试必问问题之一,其重要性不言而喻。 它对上层应用开发的影响无处不在, 例如 handler内存泄漏、线程通信、消息循环模式、数据库操作应该放在哪个线程、handler.handleMessage能否执行耗时任务、屏幕触摸事件分发机制、Animator动画机制、Activity启动流程… …跟handler有关的东西无处不在,但凡是安卓java层的东西几乎都能跟handler扯上一点关系。
- 全面的理解Handler需要涉及到android系统的许多方面,如:安卓主线程消息轮询机制、linux线程通信、队列数据结构(入队\出队\查找)、享元模式、多线程数据安全、AMS… …
- 或许每个安卓开发都能对handler说上一两个甚至三四个知识点。我也曾经自认为很仔细的看过Handler源码,但是过一两个月又全都忘记。写这篇帮助来自己巩固对handler的全面理解,以及做个笔记,下次需要相关知识时来看一眼,能很快理清每一个细节,以便在不同的业余场景下能合理运用相关技术点做出最优解,顺便帮助其它读者(PS:做某件事之前最好树立一个比较崇高的立意,这样能给自己更强大的动图,也不至于轻敌)
二、Handler灵魂拷问
- 一个线程有几个 Handler实例? 有几个Looper? 有几个MessageQueue?
- Handler内存泄漏的原因? 为什么其它的内部类(如viewHolder)不会内存泄漏?
- 为什么主线程可以随意new Handler? 子线程需要怎样才能 new Handler?
- 子线程中维护的Looper,当它的消息队列再无消息时如何妥善处理?
- 主线程的handler,其它所有线程都可以使用这个handler发消息,它是如何确保线程安全的?
- 发消息时如何创建Message最合适?new Message吗?
- 使用handler.postDelay后消息队列有什么变化?
- Looper.loop的死循环为什么不会导致应用卡死?
- postDelay指定时长的消息,为何在指定时长后会得到执行?内部如何实时定时器?
- 系统所提供postDelay的时长绝对准确吗?
- Message与Messenger的是什么关系?
- MessageQueue的IO复用原理?
针对上面的问题,如果读者你能很快给出准确答案,并指出相关类的相关方法,那恭喜你,你已经出神入化了,不需要再看我这篇文章。 如果你的感觉是模棱两可,那跟随本博主一起遍历一下handler的每一个细节吧。
三、Handler.java分析
handler翻译过来是“处理者”,那自然是用来处理某些事物的,准确说是用于处理需要在多线程间相互通信的事物。例如主线程需要处理某件事,但何时处理则需要等待其它线程来通知。
Handler工作流程图
下面以一个简化化版的 Handler为例,只列举核心方法,说明handler类的结构:
##简化版的 Handler、只提取核心变量和方法
public classs Handler{
private Looper looper;
private MessageQueue queue;
public Handler(){
//获取当前线程的 looper对象
looper = Looper.mylooper()
//获取此Looper对象的消息队伍
queue = looper.mQueue;
}
public void handleMessage(@NonNull Message msg){
重写此方法,用于处理事件
}
public void sendXXXMessage(){
各种发送消息的方法,最终都会调用 enqueueMessage 方法
}
private void enqueueMessage(MessageQueue queue, Message msg,long uptimeMillis) {
将消息放入到消息队列,若消息队列读端因无消息而阻塞,则会唤醒读端的线程
}
public void dispatchMessage(@NonNull Message msg) {
分发事件:将事件交给对应的回调(回调可能是msg.callback、handler.mCallback、handler.handleMessage)
}
public static Message obtain() {
通过Message的静态变量 sPoolSync 缓存池头指针,以享元模式获取可利用的Message对象,避免内存抖动
}
}
单纯看handler类的方法,它可圈可点的地方并不多,它的一些重要的功能点都是由Looper和MessageQueue去完成。
3.1、sendXXXMessage
它的各种sendXXXMessage方法,其内部最终都是调用enqueueMessage方法,将新的消息插入到MessageQueue的对应位置,对应的规则是按消息所需要执行的时间点来排序的有序消息队列。
3.2、sendMessageDelayed
发送延迟消息,延迟指定的毫秒数后才来执行这个消息。
- 例如延迟1秒执行,那么delayMillis=1000,其内部是调用sendMessageAtTime方法,
- 此方法第二个参数是系统自启动以来的毫秒时长 加上 延迟的时长。
- 由此可以联想到,消息队列里的消息都是按 执行时间点 排序的有序队列。
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
3.3、enqueueMessage
- msg.target = this; 将消息插入到MessageQueue时,给消息target赋了一个值为当前handler,它的目的是便于在消费此消息时,更快知道应该由哪个handler来消费。
- 最终是调用到MessageQueue的enqueueMessage方法,将消息插入到消息队列中。MessageQueue.enqueueMessage()插入新消息的源码分析会在下文继续讲解。
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
3.4、obtainMessage(XXXX)
- handler中有一些 obtainMessage 方法,其内部实现都是调用Message.obtain(this, xxx)相关方法。
- 在Message内部使用了享元模式维护了已经被消费掉的Message对象池,池子最大容纳50个Message对象,每次通过Message.obtain()方法创建对象时会优先能对象池中取,池子为空时才会创建新的对象。
- 通过这种方式避免大量Message很快创建完就被废弃等待GC回收,造成内存抖动,内存抖动的容易造成内存碎片,小碎片过多,当需要创建大对象时就容易引起OOM。
3.5、dispatchMessage(Message msg)
消费此消息的方式
- Message中赋带的calllback优先级最高,当Message指定了callback时,只会交给此callback处理。
- handler初始化时传入的callback优先级其次。
- handler.mCallback.handleMessage(msg) 返回false时,还可以继续有 handler重写的handleMessage(msg)方法继续处理。
##源码分析
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
四、MessageQueue.java分析
- MessageQueue用于管理某线程的Looper的所有消息,其内部以队列的数据结构来组织消息,提供出队、入队、删除的方法,并且通过synchronized (this)来保证线程安全。 下面通过简化版的MessageQueue.java类来分析它的作用
##简化版的 MessageQueue.java, 只提取核心变量和方法
public final class MessageQueue {
//native层通信的信号量
private long mPtr;
//消息队列头指针
Message mMessages;
//是否允许退出此消息循环(目前只有主线程的消息循环会用到)
private final boolean mQuitAllowed;
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
将新消息按执行时间点的排序插入到 消息队列中
根据消息的执行时间戳,决定是否唤醒读端
}
}
Message next() {
//取出队头的消息,若队列为空,则会阻塞住,直接有新消息时被唤醒
死循环{
nativePollOnce(xx)
synchronized (this) {
判断队头消息是否达到执行时间戳,若达到则队头出队,否则继续阻塞,指定时长后唤醒
}
}
}
//native方法,唤醒读端
private native static void nativeWake(long ptr);
//native方法,读端判断是否有消息可读,若没有则阻塞信
private native void nativePollOnce(long ptr, int timeoutMillis)
//退出消息循环
void quit(boolean safe) {
if (!mQuitAllowed) {
throw new IllegalStateException("Main thread not allowed to quit.");
}
}
}
4.1、消息队列数据结构
- MessageQueue持有消息队列的头指针,队列中的消息都必须按执行时间戳的排序放入有序队列中,每次读端被唤醒时,会直接将“队头元素”进行“出队操作”,抛给上层消费掉。(队列作为基本数据结构操作,这里不再多说)
4.2、enqueueMessage(Message msg, long when)源码分析
- 关于能否在队列中间插入元素,可以有两种理解,
- 数据结构应该活学活用,不必限制于队列只能队尾入队、队头出队的概念.
- 此队列可以认为是有序队列,把队列细分为“无序队列”和“有序队列”,“无序队列”只能队尾入队、队头出队。“有序队列”可以按某种排序进入插队操作,我们现实生活不是一样有vip插队操作吗,这样就可以口服心服的理解通透了。
4.3、next()源码分析
- 如果因when<msg.when导致写端提前唤醒了读端, 读端也会判定队头消息时间戳未到,而继续阻塞。
- 那么为什么还需要提前唤醒?
答:这样可以保证在写端入队的异步消息之前,如果还有前面的异步消息在响应时消耗时长 大于 本次消息延迟的时长,可以保证此消息仍能得到尽快执行。 - 由于可以看出poseDelay的延迟时长并不是绝对的准确可靠。
4.3、addIdleHandler(IdleHandler handler)
- MessageQueue还有一个不常用的方法,但如果知道它,在某些业务情况下又多了一招。它的作用是当handler消息队列闲置时,会执行注册的IdleHandler的queueIdle()方法,表示此时消息队列进入空闲状态,以便业务层处理自己的需求。
- IdleHandler是MessageQueue的一个静态内部接口,如下图 .
- 执行原理,它的处理逻辑是 MessageQueue.next()方法中的一段代码
4.4、quit(boolean safe)
- 退出消息循环,若创建此消息队列时入参mQuitAllowed为false,表示不允许退出此消息循环,当一个不允许退出的消息队列执行quite时,会直接抛出异常。目前只在主线程中使用到,当然业务使用方可以自由决定是否需要允许退出。
- removeAllFutureMessagesLocked 和 removeAllMessagesLocked 的区别在于:
- removeAllMessagesLocked会直接从队头开始遍历队列进行出队,每个消息出队时会调用msg.recycleUnchecked()方法,这个方法会将消息的所有成员变量重置为初始状态,并加入到sPoolSync共享元素池中,以便下次创建时不需要重新创建。
- removeAllFutureMessagesLocked 会保证在调用quite(true)时刻之前的所有消息都能得到执行,它只对在调用quite(true)时刻之后的消息进行出队操作。
五、Looper.java分析
- 从字面翻译Looper表示“循环器”,精略的看,可以猜想到它的“核心功能”就是“用来循环做某件事情”,它内部的所有变量和方法都是用来辅助完成这个功能,即“循环做某件事情”。
- 当然Looper内部还有很多细节的问题需要考虑,如:设计模式、线程安全、运行效率、如何退出循环等。在分析Looper的原理时,我们暂时把这些细节当成黑盒,认为黑盒已经帮我们解决了这些问题,否则就会陷入类似回调地狱的状态,忘记自己最初的主线目的;至于黑盒是如何解决这些问题的,下文会继续分析。
下面还是以简化版的Looper.java源码为例,来说明Looper如何实现“循环做某件事情”的:
简化版的 Looper.java, 只提取核心变量和方法
public final class Looper {
//用于保存各个线程独有的Looper,所以是静态变量
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
//当前Looper实例维护的消息队列
final MessageQueue mQueue;
//静态方法,为执行此方法的线程创建一个Looper
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));
}
//创建主线程的Looper,只能由操作系统调用,应用层不需要调用,调了也会异常
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
public static void loop() {
Looper me = 取出调用此方法的线程的独有Looper
//取出消息队列
MessageQueue queue = me.mQueue;
for(;;){
//尝试读取队头的消息,若队列为空,则阻塞。直至写端写入新消息,或延时消息到达执行时间就会被唤醒。通信机制由native负责完成。
Message msg = queue.next();
//分发出队的消息
msg.target.dispatchMessage(msg);
}
}
}
5.1、Looper.prepare()
- Looper要求一个线程想要使用Looper来循环处理消息时,必须先调用Looper.prepare()方法,并且每个线程只能调用一次,同一个线程重复调用会直接抛出异常。
- 入参quitAllowed表示是否允许退出消息循环。
5.2、loop()
- 使执行Looper.loop() 方法的线程进入循环读消息的状态,当消息队列中无消息时,会阻塞以便减少cpu资源占用。
- 当成功出队一条消息时,会通过 msg.target.dispatchMessage(msg); 将消息分发下去 由接收者响应, msg.target 就是发送这条消息的 handler。
5.3、Looper.prepareMainLooper()
- 翻译源码注释:执行此方法使当前线程初始化为“循环器”,并让这个此线程作为程序的主线程循环器。它必须由安卓运行时环境创建,app应用层不允许调用此方法。
- 它的调用是系统运行时负责调用,具体是在ActivityThread.main()方法中调用,这个方法是应用进程启动时调用的第一个方法。
六、ThreadLocal.java分析
- 在Looper源码分析中,我们知道了消息循环的原理,但为了让Looper的逻辑更加清晰,我们有意忽略线程安全方面的问题,把当成了黑盒。那么Looper是如何确保每个线程都有自己独有且唯一的Looper呢?
下面还是以简化版的ThreadLocal.java源码为例,来说明ThreadLocal如何帮助Looper解决线程独有且安全的问题,先给出结论:
- 同一个 threadLocal 实例 可以在不同线程中调用 set(T value),用于给不同线程保存 各自线程独有 的对象
- 不同 threadLocal 实例在同一线程中调用 set(T value),可以保存多个 此线程独有的 对象
##简化版的 ThreadLocal, 只提取核心变量和方法
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) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
//线程独有的Map,内部结构类似 HashMap
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 void set(ThreadLocal<?> key, Object value){
}
private Entry getEntry(ThreadLocal<?> key) {
}
}
}
6.1、threadLocal.set(T value)
- 给调用此方法的线程 保存一个此线程独有 的 对象
- createMap(t, value) 实际是只是给 thread 对象的 threadLocals 变量 初始化赋值而已。
- t.threadLocals 竟然是 Thread 类的一个成员变量,第一次见到的童鞋可能会有点不可思议,贴张图,眼见为实。
6.1、threadLocal.get()
6.1、ThreadLocalMap
- ThreadLocalMap 可以认为是简化版的 hashMap。
- ThreadLocalMap 保存新的数据时,内部类似 hashMap的机制,通过hash值来寻找 存储的位置,数组存满75%时就会扩容并重新hash。
- 这里可以发现一个很有趣的问题,也经常作为面试题: hash扩容的是判断超过 75% 就扩容, 为什么是 75% ?
**答: 阈值太大了会更容易冲突,太小了又会不必要的扩容。 这里有数据公式可以计算出大约是0.715附近的值, 但考虑到执行效率,就把它定义为75%。那为什么75%就效率高,看下图
75%可以换算为 原值 减去 原值除以 4, 除以4可以优化为右移两位, 而移位操作是最高效的,比加减法还要高效。 一个减法操作和一次移位操作,比乘除操作要高效几十上百倍,非75的值不可避免的需要乘除操作。
七、回答文章开头的问题
- 一个线程有几个 Handler实例? 有几个Looper? 有几个MessageQueue?
一个线程只能创建一个Looper实例,对应一个MessageQueue,而handler可以随意创建,只要在创建handler之前调用过 Looper.prepare的线程都可以创建handler任意个。 - Handler内存泄漏的原因? 为什么其它的内部类(如viewHolder)不会内存泄漏?
当handler发送延迟消息时,此时activity又关闭了就会短暂泄漏,因为handler被msg.target引用,msg被MessqgeQueue引用,messageQueue被Looper引用,Looper被主线程的thread.ThreadLocalMap引用,而主线程肯定是常驻内存的,只要主线程不销毁,那么handler永远是在引用链上的,而handler又作为匿名内部类隐式持有 Activity/Fragment 导致 activity内存泄漏。 - 为什么主线程可以随意new Handler? 子线程需要怎样才能 new Handler?
任何线程只要调用了Looper.prepare()后就可以new Handler,主线程的Looper.prepare是 ActivityThread.main方法是由系统环境调用的。 子线程调用完Looper.prepare后也可以任意new Handler()。 - 子线程中维护的Looper,当它的消息队列再无消息时如何妥善处理?
当子线程Looper.prepare时入参传的是允许退出,那么可以调用Looper.myLooper().quit() 来退出子线程的消息循环 - 主线程的handler,其它所有线程都可以使用这个handler发消息,它是如何确保线程安全的?
是的,所有线程都可以调用。其内部是在MessageQueue.enqueueMessage()方法内通过 synchronized(this)来保证多线程安全的。 - 发消息时如何创建Message最合适?new Message吗?
handler.obtain() 或者 Message.obtain() 利用 共享元素模式(享元模式) 来复用 Message对象,避免快速创建后双回收,引起的内存抖动,抖动多了就会有大量内存碎片,当需要创建大对象时就有容易OOM。 - 使用handler.postDelay后消息队列有什么变化?
往要对应消息队列中按 执行时间戳顺序 插入到队列中,当然这样称呼为队列不太准确,严格意义上的队列只能是队尾入队,而这里却在队中间插入元素。 虽然不准确,但数据结构应该活学活用,以满足业务要求,而不是为了学术概念而限制自己。 - Looper.loop的死循环为什么不会导致应用卡死?
死循环这个词描述的不太准确,死循环用来描述日常生活中的现象这没问题,但用来描述loop那就太以偏概全了, 安卓主线程本来就是 阻塞/唤醒模式的消息循环机制,并不是死循环,无消息时就会阻塞以便释放cpu资源,有消息时才会处理。
另外,应用层的几乎绝大部分类都跟此消息机制有关,例如application生命周期方法、四大组件生命周期方法、屏幕触摸事件、屏幕刷新60、90、120hz都是通过 handler发消息来通知 应用层。 这如果能卡死,这些功能都没有,那你的app启动后就是白板一块。
而且,iOS、windows开发,但凡是有界面的应用程序,都有类似的消息循环机制,它这样设计的根本目的就是要保证用户交互不能被阻塞。 - postDelay指定时长的消息,为何在指定时长后会得到执行?内部如何实时定时器?
定时器是通过native方法 nativePollOnce()来实现的,它的第二参数可以指定一个延时参数,当参数大于0时,此方法调用完后会进入阻塞状态,达到指定延时时长后才会自动唤醒。 - 系统所提供postDelay的时长绝对准确吗?
不是绝对准确,有可能受前面的消息执行耗时的影响,例于延时1秒的消息,之前还有一条消息执行耗时2秒,那必须等到这2秒消息执行完,才会再来执行这个延时1秒的消息。 - Message与Messenger的是什么关系?
Message是消息体。 Messenger是用于进程间通信的上层包装类,它和Handler结合起来,可以让进程间通信,看起来像handler线程间通信那样简单。当然Messenger内部通信机制肯定都是binder,但是手写binder极其麻烦,即使用aidl也远没有Messenger方便。 - MessageQueue的IO复用原理?
(待验证)android_os_MessageQueue_nativePollOnce ,因为这里的 IO 机制采用 epool ,当它没有消息时会调用 wait() 函数释放 CPU 进入休眠等待,当有消息来临会通过管道写入来通知唤醒。
Linux的IO多路复用模型还有 poll 和 epoll ,这里说一下它们之间的区别,poll 可监视的 IO数量大于 select,而 epoll 和其他两个函数的区别就是不会轮询文件描述符来操作 IO,当一个IO完毕就直接通知刷新,而不是一直轮询判断可读写的状态来刷新,简单的说,epoll 只会刷新已经成功的 IO,而其他两个函数判断 IO 是否已成功是用轮询的方式,细心的朋友会发现,我们的这个 IO 多路复用好像也没有比阻塞或非阻塞 IO 模型强到哪去,而且还要往函数里添加 socket 监听回调,IO 多路复用的核心就在于同一时刻一个逻辑流也就是一个线程可以监听操作多个 IO,而其他 IO 模型只能通过多线程来进行多个 IO 的需求。