基本认识
认识4个类:
- Message,负责描述消息,包括各种需要传递的数据,区分消息的what等
- Handler,负责发送消息和处理消息
- MessageQuene,是一个消息队列,存储Message的顺序,
- Looper,负责轮询消息
Message负责描述消息,,Looper负责发送消息和消化消息,Looper负责传送消息,MessageQuene负责存储消息,这是最基础的。
用法
- 常用于延时消息发送和处理
- 用于子线程与主线程之间的消息通知与转换
- 一些场景使用Handler简化消息通知。
源码分析
我们首先从消息发送的方法开始,先不看构造,下面是public
的发送消息方法
1 | post(@NonNull Runnable r) |
上面就是我们Handler
提供的public
的方法,用于发送消息,他们最终都会调用到enqueueMessage
这个方法
1 | private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,long uptimeMillis) { |
通过调用关系我们知道传入enqueueMessage
的MessageQueue
对象就是当前Handler
的mQueue
变量,它的赋值是通过mLooper.mQueue
获得,而mLooper.mQueue
是在Looper
初始化的时候赋值。而Handler中获取Looper对象是在它的构造方法中,所有的Handler构造方法都会最终调用到下面两个构造方法的之一:
1 | // 创建Handler的时候传入,该构造方法不对外 |
可以看到无论我们是传入还是通过Looper.myLooper()
拿,Looper都必须不为空,那么我们是如何才能创建一个Looper对象呢?通过源码我们知道我们必须要调用Looper.prepare()
才可以创建一个当前线程专属的Looper对象(使用了ThreadLocal
),即每个线程只能有一个,互不影响。而且我们在创建Handler之前就必须创建好Looper了,这就是我们为什么在一些子线程中使用Handler
不调用Looper.prepare()
会直接崩溃的原因。
通过上面的流程分析,我们知道使用流程是先调用Looper.prepare()
,然后就会创建好MessageQueue,再然后我们创建Handler对象,然后进行使用(当然还需要调用Looper.loop()
,后续说)。
从上面代码我们也知道,一个线程对应一个Looper,对应一个MessageQueue,对应多个Handler,每个Handler对应可以收发多个Message,而某个线程的Handler可以被不同线程调用。
MessageQueue的enqueueMessage
该方法是在MessageQueue中插入一个元素。首先,我们MessageQueue是一个队列(链表形式),它的元素是Message,有表头,每个元素都有指向下一个元素的引用,即大概定义是:
1 |
|
组织这个列表的方式是消息的发送时间,因为我们发送消息的时候,他是可以指定消息的发送时间的,我们的MessageQueue就是以发送时间短的在前面,发送时间靠后的在链表后面。
然后这时候我们再看enqueueMessage
方法
1 | boolean enqueueMessage(Message msg, long when) { |
从上面的代码,我们知道enqueueMessage
负责两件事
- 插入新消息到队列中
- 看情况是否需要唤醒
next()
方法
Looper.loop()方法
我们知道通过Looper.prepare()
给线程创建一个Looper对象,通过Looper.myLooper()
获取到Looper对象,但是我们还需调用Looper.loop()
来开启消息的通知监听。
1 | public static void loop() { |
省略了其中的关于日志以及慢消息等的代码,可以看到loop()方法是一个死循环,只有在Queue的next()返回空的时候才会终止。它通过调用MessageQueue的next()方法来获取消息,通过调用Message中的target的dispatch来分发消息,这里会有几个问题:
- next()方法是如何阻塞的
- 为啥loop()方法不会导致ANR
我们先看问题1,直接看next的源码
1 | Message next() { |
经过next()
我们拿到了消息,那就回到Looper中,看到loopOnce
方法中调用了msg.target.dispatchMessage(msg);
进行了消息分发
Handler的dispatchMessage
1 | public void dispatchMessage(@NonNull Message msg) { |
每个Handler都只能监听到自己发送的消息,无法监听到别的Handler的消息。
初步总结
我们再会看整个流程:
- 调用Looper.prepare()生成Looper对象
- 调用Looper.myloop()获取Looper对象
- 传入Looper对象,生成Handler对象
- 在Handler中,通过Looper对象给Handler中的mQueue赋值,类型是MessageQueue
- 调用Looper.loop()开启消息处理循环,他是一个死循环方法,循环中调用了MessageQueue的next()方法,next()方法在无消息的时候会通过nativePollOnce阻塞(休眠)住,避免线程资源一直运行浪费cpu
- 其他线程可调用上面Handler对象发送消息,有各种
send
和post
方法,然后最终都会调用MessageQueue的enqueueMessage
,他会插入到队列中,然后他会通过调用nativeWake()
来唤醒next()方法,去继续执行loop()循环。
深入
epoll机制 & 为何不会ANR
epoll是Linux独有即机制,是实现IO多路复用的一种方案,还有select
和poll
,属于高并发下的事件驱动,有推荐阅读:Android Handler 中的 epoll 机制
我们在MessageQueue中使用到两个很重要的方案nativeWake(long ptr)
和nativePollOnce(long ptr, int timeoutMillis)
就是epoll
机制提供,前者会唤醒线程,后者会阻塞(休眠线程)
1 | long nativeInit() :返回一个底层对象的指针,赋值给ptr,创建epoll对象 |
首先,我们先看下何为ANR:application no response
,即应用程序无响应,它本质上来说也是一种Message
,即有Handler
发出来的,如果我们的主线程处于等待或者是阻塞的情况下,操作系统无法调用onDraw()
函数对屏幕进行重新的绘制,就会导致应用冻结,并有可能导致弹出应用无响应(ANR)
对话框。
ANR发生的场景有四种:
- 5s内无法响应屏幕的触摸或者是键盘输入事件
- 执行前台广播的
onReceive()
函数超过10s,后台60s - Service:前台20s,后台200s
- ContentProvider:他的publish在10s内没有进行完成
- startForegroundService:应用调用startForegroundService,然后5s内未调用startForeground出现ANR或者Crash,此问题属于应用未适配sdk。
我们的应用的主线程是在ActivityThread
中调用了Looper.loop()之后,在没有消息的时候,就会陷入休眠,在有消息的时候就会被子线程调用主线程的Handler唤醒,比我们的H
类。我们所有的消息都是运行在该死循环之中的,加入该循环退出了,我们的应用也就退出了。
IdleHandler
是MessageQueue中定义的一个接口,在Looper线程处于空闲状态下的时候执行一些优先级不高的操作,可以通过MessageQueue的`addIdleHandler()方法来提交需要执行的操作,接口定义:
1 | public static interface IdleHandler { |
我们可以在Activity/Fragment
的onCreate/onViewCreated
方法中,通过Looper.getMainLooper().getQueue().addIdleHandler(IdleHandler)
添加一个监听,会在Looper线程空闲的时候执行,假如queueIdle
返回true则可能重复执行,false则只会执行一次。具体的实现是在MessageQueue
的next()
方法中,
关于IdleHandler的添加和删除的代码如下:
1 | private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>(); |
next()
方法处理IdleHandler的解析如下:
1 | Message next() { |
mBlocked的赋值
他是MessageQueue中的变量,true的时候就代表着线程被阻塞(休眠)着,false就是没有,表示正在处理消息中。在next()方法中,只有找到了可用的可处理的消息,他就会设置为false,假如没有找到消息,而且又没有需要处理的IdleHandler,那么就需要阻塞(休眠)当前线程,mBlocked就会被设置为false。
然后在enqueueMessage
方法中,在线程休眠的时候,即mBlocked为true的时候会唤醒线程
- 假如插入的新的消息是需要立即执行的
- 假如当前队列头部是一个屏障消息,而且新消息是一个异步消息
异步消息与屏障消息
屏障消息一般是需要配合异步消息一起使用。
异步消息是指Message
中isAsynchronous()
返回的是true
的消息。创建的方式有两种:
- 创建
Message
的时候可以调用setAsynchronous()
,那么该消息就是一个异步消息。 - 在创建Handler对象的时候,他的构造函数的
async
传入为true,那么所有通过该Handler的消息都会变成异步消息。
我们通过Handler无法创建以及发送屏障消息,屏障消失是指他的target
是空的消息,即它没有需要处理消息Handler对象,只能通过MessageQueue中的方法去发送,但是目前这些方法我们都无法调用,假如我们需要调用只能通过发射的方式。
int postSyncBarrier()
,发射一个屏障消息,返回值是他的tokenint postSyncBarrier(long when)
,发射一个定时消息,返回值是他的tokenremoveSyncBarrier(int token)
,移除一个屏障消息
异步消息与屏障消息有何作用:我们在看next()
方法源码的时候就有解读到,假如我们的头部的消息是一个屏障消息,那么就会直接去队列中查找到第一个异步消息,然后进行处理,假如该消息的时间比当前的大,则延迟这个休眠时间差,然后再次执行这个逻辑找到这个异步消息去处理。
从这里我们可以看出,我们的屏障消息可以提高异步消息的执行优先级,一些非异步消息都不回先执行,只有等待异步消息执行完之后才会去执行。在这些消息执行完成之前都不会执行同步消息。
目前我们的ViewRootImpl就有通过该机制来使得View的绘制有限处理。
我们可以在有需要的时候也可以发送异步消息,这样在系统发送发送屏障消息的时候也会顺带优先执行我们的这些异步消息。
Looper退出
我们知道一旦开始调用Looper.loop()
之后,线程就会陷入死循环中不会往下执行,比如:
1 | public void createAsyncHandler(){ |
我们假如需要退出Looper,可以通过
- Looper.myLooper().quit()
- Looper.myLooper().quitSafely()
他们的区别:是调用MessageQueue的quit()
方法的时候的传参,前者是false后者是true。
在MessageQueue的quit()
1 | void quit(boolean safe) { |
在唤醒线程只,执行MessageQueue的next()方法的时候,假如mQuitting
是true,则会调用
1 | Message next() { |
我们在创建Looper对象的时候,调用的prepare()
方法就会传入一个quitAllowed
参数,MessageQueue
的构造函数也需要传入一个quitAllowed
,这个也就是在MessageQueue的quit()方法执行之前的前缀,需要传入true才可以执行Looper.myLooper()的相关退出方法。
主线程
我们知道App启动的入口是在ActivityThread
的main()
方法中,我们的主线程使用的Looper就是在这里去调用的。
1 | public static void main(String[] args) { |
可以看到,假如我们的主线程他是一个死循环来的,假如我们的主线程的Looper退出了,那么他就会直接跑出异常。
所以主线程的Looper他是不能调用quit()
方法的,构建的prepareMainLooper()
方法传入的quitAllowed
是false。
日志与调试与慢消息
- 我们可以通过调用Looper.myloop().setMessageLogging()方式来打印loop()方中的日志。
在主线程中也有通过下面代码来打印,但是他是不可见的,因为设置为了false,在ActivityThread的main方法中:
1 | Looper.myLooper().setMessageLogging(new |
来查看主线程的日志,我们也可以自己去覆盖这个LogPrinter。
- 通过调用
setObserver(@Nullable Observer observer)
的方式,设置全局的Looper消息传递(每个线程的都可以观测到),但是该方法是hide()
的,无法通过普通调用来设置,只能通过反射的方式,我们一般也不要去设置,因为可能系统某处地方设置了,我们设置就会覆盖了。1
2
3
4
5
6
7
8
9/** {@hide} */
public interface Observer {
// 定义token
Object messageDispatchStarting();
//消息分发
void messageDispatched(Object token, Message msg);
//分发出现异常
void dispatchingThrewException(Object token, Message msg, Exception exception);
} - 通过调用
dump
方法来打印相关日志,Looper有提供 几个dump方法,但是我们只能调用其中的一个:1
2
3
4
5
6
7dump(@NonNull Printer pw, @NonNull String prefix)
// 我们可以调用这个,他会打印当前Looper的toString()方法,包括线程名称,线程id,以及一个hashCode。还会打印当前MessageQueue的消息队列的具体信息。
dump(@NonNull Printer pw, @NonNull String prefix, Handler handler) 被系统hide了,只能通过反射调用
dumpDebug(ProtoOutputStream proto, long fieldId) 被系统hide了,只能通过发射调用 - 我们可以通过adb设置慢消息,然后打印出来。在Looper中,调用
loopOnce()
方法的时候,有传入一个thresholdOverride
,这个值是代表了消息分发的耗时值。首先我们看下他默认的mSlowDispatchThresholdMs
和mSlowDeliveryThresholdMs
。- mSlowDispatchThresholdMs:调用Handler的
dispatchMessage
过程的耗时,单位ms,超过mSlowDispatchThresholdMs就会打印日志 - mSlowDeliveryThresholdMs: 消息Msg分发的目标时间与当前开始分发的时间差值,单位是ms,超过mSlowDeliveryThresholdMs就会打印日志
他们二者是可以通过setSlowLogThresholdMs
设置,但是该方法是hide
,我们无法调用,假如我们通过adb命令设置了thresholdOverride
,就会覆盖mSlowDispatchThresholdMs
与mSlowDeliveryThresholdMs
。
- mSlowDispatchThresholdMs:调用Handler的
1 | public void setSlowLogThresholdMs(long slowDispatchThresholdMs, long slowDeliveryThresholdMs) { |
在Looper的loopOnce
方法中
1 | private static boolean loopOnce(final Looper me, |
Message的复用
在Android系统中,到处存在Message消息,会很多地方都需要收发消息,Message对象的创建是很频繁的,系统因此提供了一套Message重用机制。
在Message类中,有一个sPool
的Message
对象,他就是我们的消息的重用的存储对象,是存储一些已经被使用过,标记为无用的Message对象的列表。
在Message中有下列几个方法来实现消息的复用:
boolean isInUse()
:判断Message是否正在使用,比如我们把发送一个消息的时候,最终通过MessageQueue的enqueueMessage
方法存入,在这个方法会判断存入的Message是否是正在使用,如果是的话,就会抛出异常。void markInUse()
,把Message标记为正在使用,比如我们的消息进入了MessageQueue
之后,就会调用该方法,把消息标记为正在使用中void recycleUnchecked()
,在消息分发完成之后会调用该方法,它会把消息的所有数据重置,并该Message对象存入sPool
中,而且是存入队列的头部。recycle()
,它会先判断当前消息是否是正在使用,是的话会抛出异常,不是的话就调用recycleUnchecked()
,进行消息的复用。比如我们推出了Looper循环,他就会调用该方法。
我们一般也不会,不需要手动去调用上面的上面去重置Message的状态,它会在轮询消费的过程中自动的重置。
我们的sPool
缓存的最大长度是50,即MAX_POOL_SIZE
字段。所有的线程共享这个消息缓存池的Message对象。
然后我们后续只需要通过调用Message.obtain()
系列的方法,就会能够去从缓存中读取了。
还有一点需要注意的是,假如我们在处理消息的过程中,把Message回调消息的对象存到了另外的现场,那么就有可能导致Message消息的内容丢失了。比如:
1 | override fun onCreateView( |
原因就是我们的消息在执行完成消息的分发之后,它会回收掉,就执行完上面的handleMessage
方法之后,该消息就会调用recycleUnchecked()
了,这是我们开发过程中需要注意的。
一些有趣的方法
- 我们的Handler可以结合
Messenger
,实现跨进程通信,具体可以参考Android 基于Message的进程间通信 Messenger完全解析 - Handler的回调监听有三种:第一种是我们可以复写Handler的
handleMessage
方法,第二种是在创建Handler对象的时候,传入一个Handler.Callback
对象,假如该对象的handleMessage
返回了true就不会执行Handler自身的handleMessage,否则就会。第三种是创建Message的时候传入
callback`(类型Runable),但是该方法是不对外公开,我们使用不了,假如给Message设置了这个对象,就不会调用前面的两个方法 - 使用
postAtFrontOfQueue
或者是sendMessageAtFrontOfQueue
会提高优先级,消息发送的when是0,即该消息一般会被立即执行,但是官方不提倡我们直接使用,因为它可能导致其他Message一直得不到处理或者其他意想不到的情况。 - 假如我们需要再子线程发送一个消息到主线程,需要在消息处理完成之后,才继续执行子线程,那么有提供一个
runWithScissors
方法,但是该方法也是被hide
了。
避免内存泄露
我们在Activity/Fragment中使用创建并Handler,加入在页面销毁的时候,我们的Handler还在处理一些延时消息的话,就会存在内存泄露的风险。通常,我们可以通过调用removeCallbacksAndMessages(null)
来避免,他会移除属于该Handler发送的消息,假如插入的不是null
而是Message的obj
,就通过==
判断,如果是相同则会移除这个消息。
还有一个类似的removeCallbacksAndEqualMessages
,传入null的话语removeCallbacksAndMessages
作用一样,但是假如是传入某个具体的obj,就会通过equal
比较,相关就会移除。
还有对于的removeMessages
与removeEqualMessages
也是类似。