1. 目录
- 1–目录
- 2–前言
- 3–简单总结
- 4–Looper的区别:MainLooper和普通Looper
- 5–handler发送的消息过程
- 6–MessageQueue怎么把这条消息放进队列的
- 7–Looper读取消息:loop()
- 8–MessageQueue读取消息:next()
- 9–如何提高消息的优先级?同步消息,屏障消息,异步消息
- 10–handler知识点总结
前言
Android程序是一个以消息驱动的程序,页面的跟新,Activity生命周期的变化,点击事件等等都与消息息息相关。
简单总结
简单的理解Handler发送消息的流程:Handler发送消息(message)到MessageQueue,然后,Looper通过loop()方法循环从MessageQueue里面读取消息。然后,发送给对应的target(Handler)。
我们带着问题来理解这个流程,最后,我们在重新总结一下。辣么问题就来了:
- handler都是一样的,为什么Looper会分Looper.getMainLooper()和普通的Looper?
- handler发送的消息过程是什么样子的?
- Looper怎么读取消息的?
- handler发送消息能发送延时消息,Looper读取到消息之后,怎么确定是立刻发送回去,还是隔多久发送回去?
- 怎么提升消息的优先级?
- 我们项目里面可能会用到的Looper.prepare(),Looper.loop(),这是做什么操作?
我们结合源码一起来看一下这些问题:
Looper的区别:MainLooper和普通Looper
第一个问题,handler都是一样的,为什么Looper会分Looper.getMainLooper()和普通的Looper?我们都知道
1 | Handler handler = new Handler(Looper.getMainLooper()) |
通过这个Looper.getMainLooper()方式得到得Handler,可以改变UI,其他的不行,这是为什么呢?我们都知道,UI线程才能改变UI。
ps:app的启动入口是在ActivityThread的main方法。
捡一些 (我看的懂的),呸,是主要的,跟我们聊的这个相关的位置贴出来,源码如下:
1 | public static void main(String[] args) { |
注释应该写的比较清楚了吧?这里我想说的是,main启动的时候,这个线程就是UI线程,这个是系统给规定的,只有在这个线程里面才能改变UI。
我们再来看看这个Looper.prepareMainLooper()里面做了什么操作
1 | @Deprecated |
注释都有了,最开始调用的prepare(false)方法,看完上面的注释,我大致串一下。
ps:说一下这个sThreadLocal变量,我也不知道怎么说,反正就是它的类型是:ThreadLocal
我们再说回这个方法,主要就是,
- 先判断这个变量是不是空的,如果不是空的,就抛异常了,因为Looper是不允许我们自己手动创建的。
- 如果是空,就创建一个Looper,放进sThreadLocal变量里面;
- 然后,创建Looper的时候,顺道就创建了MessageQueue。主线程创建的MessageQueue是不允许主动退出的,如果消息队列退出了,退出app了。
- 并且,Looper的mQueue,mThread也都赋值好了,一个是消息队列,一个是当前线程(这两个变量用的也比较多)。
prepare()方法,到这里就说完了,我们再看剩下的代码,往上面翻一下,再看一下。
ps:sMainLooper变量,类型就是Looper
剩下的代码就是一个锁方法,
- 判断sMainLooper是不是不等于null,如果,不等于null就抛出了异常
- 如果等于null,就把上面创建的looper,赋值给sMainLooper
我们Looper.getMainLooper()获取的Looper就是这个sMainLooper,也就是我们当前线程(UI线程)的Looper,我们只有绑定了这个looper的handler才能改变UI,因为,这个handler是在给UI线程传递消息。
为什么不等于null就抛出异常了呢?因为sMainLooper只在系统的时候创建,不能在其他的时候创建,如果,在其他的时候创建,说明系统启动的时候没有创建Looper,那么,主线程就没法通信,这是有问题的。
第一个问题我说明白了吧?Looper.getMainLooper()获取到的是主线程的Looper,跟它绑定的handler能改变UI,没有跟它绑定的hanler都不能改变UI
handler发送的消息过程
第二个问题,handler发送的消息过程是什么样子的?
说到这里,我们先聊一下Message类
1 | public final class Message implements Parcelable { |
写代码这么长时间,我们发了那么多消息,是不是都没有仔细看看Message的成员变量?看看上面这几个变量。
- what,arg1,arg2,obj可能我们用的比较多。
- 这个long 类型的when,很重要,是消息放在队列哪个位置的重要依据。是放在队头?还是队尾?(重点)
- Handler类型的target变量,我们之前没注意过吧?字面意思:目标。目标handler(重点)
- 下面还有两个Message类型的变量,一个next,一个sPool;next字面意思:下一条消息。pool:水池。类型又是Message;那么,sPool:池子的消息
- Object类型的sPoolSync:异步池子。根据经验来看,碰到过很多这种Object类型的东西,大部分都是加锁用的。synchronized(sPoolSync),一般都是这样用
- int类型的两个变量,sPoolSize值是0,再就是MAX_POOL_SIZE,值是50。字面的意思就是池子的大小是0,池子的最大值是50.
什么池子啊,什么最大值啊。我相信很多人跟我的反应都是一样的,线程池,复用。所以这里就是消息池,消息能复用,消息池最大的消息个数是50个,异步。
延申到这里,引出我想问的第一个问题,消息的创建,消息创建的两种方式:一种是new出来,一种是obtain的方式,它有一系列的重载方法。
1 | //第一种:new的方式 |
第一种没啥好说的,我们看第二种:Message.obtain()
1 | public static Message obtain() { |
简单的理解就是,不需要重新创建消息,从消息池里面取出一条消息,赋值给我们需要创建的msg对象。
这里为什么要加锁?什么情况下需要加锁?当然是防止并发呀,handler可以随时随地的发消息,所以,为了防止并发,加锁。
问题来了,这个sPool是什么时候赋值的呢?我们创建消息的时候没有赋值。创建的时候没有赋值,我们在Message类里面,检索sPool对象,我们找到如下方法:
1 | @UnsupportedAppUsage |
看这个方法名就应该能猜到,消息回收的时候调用的。所以,在消息回收的时候,就把这条消息重置,把这条回收的消息赋值给sPool,这里就是赋值的位置。在消息回收的时候赋值。
所以,只要你并发量不大,你每次都是obtain创建消息,基本上都是复用的,不会重新创建消息。
消息说完了,跑题了,跑题了,言归正传
handler发送消息的流程
欢迎来到走进科学之Android消息发送流程,我们来一步一步的剖析这条消息是怎么一步一步放进消息队列的。
1 | Message msg = Message.obtain(); |
我们来看这个sendMessage的源码。
1 | public final boolean sendMessage(@NonNull Message msg) { |
msg.target是后面Looper拿到这条消息之后,发送的目的地,不然,Looper怎么知道要发送给谁(handler)?
提升消息优先级的位置。同步消息,同步屏障,异步消息。也是比较重要,后面再细唠。
MessageQueue怎么把这条消息放进队列的
到这里handler的发送就完了,MessageQueue怎么把这条消息放进去的呢?方法如下:
1 | boolean enqueueMessage(Message msg, long when) { |
总结下来就是:三个条件
- 根据当前队列是否空闲(p == null)
- 当前消息执行的时间when(when == 0)
- 当前队列执行的消息是否需要在新增消息的后面执行(when < p.when)
来判断当前消息是否需要插到队首,只要满足上面的任意一个条件,就需要放进队首;否则,for循环判断当前消息需要放到消息队列的哪个位置。需要插队的话就记得把队列中后面的消息放到当前消息的后面。
再重复一遍,这个时间是SystemClock.uptimeMillis() + delayMillis,系统开机时间+你传递的延时时间。
到这里,消息就被插件消息队列了。代码基本上每行都有注释,一遍没有看懂的话就多看几遍。
Looper读取消息:loop()
消息已经放进队列了,第二个问题就结束了,接下来就是第三个问题:Looper怎么读取消息的?
Looper是通过loop()方法循环读取消息的。代码如下:
代码比较多,我把无关的(看不懂的)都去掉了
1 | public static void loop() { |
我们先看一下这个handler的dispatchMessage方法:
1 | public void dispatchMessage(@NonNull Message msg) { |
到此,消息的发送,入队,取消息,处理,就形成了闭环了。整个流程:
- 新建handler,发送消息sendMessage
- 此时消息的创建obtain复用模式,后面可能会造成正在使用的异常,所以,需要加锁同步一下
- 然后,消息进队,target(目的地的handler)和when(执行的时间系统开机时间+延时时间)
- 判断的三个条件,是放进队首(队列中是空的,时间是0,时间在队列消息时间的前面),还是队中(需要循环判断队列中是否还有消息和时间)
- 通过loop方法取出来消息,通过这个消息的target发送消息
我们接下来说第四个问题:handler发送消息能发送延时消息,Looper读取到消息之后,怎么确定是立刻发送回去,还是隔多久发送回去?
我们上面分析完,好像并没有看到这个延时消息的问题啊,Looper的loop方法是,只要queue.next()返回给它消息了,它就直接发送回去了,没有什么延时。
MessageQueue读取消息:next()
重点就在这里queue.next(),读取消息。这里也是提升消息优先级的位置(同步屏障,异步消息)。
1 | @UnsupportedAppUsage |
流程就是:
- 先判断looper有没有,系统有没有重启,如果重启了,looper没有,那就直接返回一个null对象,Looper接收到了一个null对象,会直接return
- 然后,判断是不是屏障消息(屏障消息消息的target等于null),如果是屏障消息,就进行do..while循环,直到取出一条异步消息为止
- 正常的判断消息,是同步消息还是延时消息,同步消息立刻返回,延时消息,提醒底层多长时间之后再调用我
看到了吗?MessageQueue取消息的流程,通过msg的执行时间与当前系统的开机时间进行比较,延时消息就是判断了延时多长时间之后,告诉底层多长时间之后,你还要调用一次这个取消息的方法。这就是延时消息的实现。
如何提高消息的优先级?同步消息,屏障消息,异步消息
既然说到这里,我们就直接聊一下这个消息的优先级
ps:这里的异步消息,同步消息,并不是说多线程去处理消息。异步消息是有一个属性是true
1 | //同步消息 |
我们平时发消息是下面这个样子的:
1 | Message msg1 = Message.obtain(handler,new Runnable(){ |
上面就是new了两条消息,一条同步消息,一条异步消息,如果没有屏障消息的情况下,同步消息和异步消息是一样的,没啥区别。程序运行完,过三秒钟同步消息回调,再过两秒打印异步消息回调,上面消息的打印结果如下:
1 | 2021-11-21 08:53:10.363 29452-29452/com.haichenyi.myapplication V/hcy: 两条消息都发送完了 |
那么,什么是屏障消息呢?怎么实现呢?我们先说怎么实现的。
1 | Message msg1 = Message.obtain(handler,new Runnable(){ |
打印结果如下:
1 | 2021-11-21 09:05:36.915 29743-29743/com.haichenyi.myapplication V/hcy: 两条消息都发送完了 |
代码执行完之后,先是过了5秒回调了异步消息,然后立刻回调了同步消息,为什么呢?因为,同步消息是延时3秒执行呀,异步消息是延时5秒执行,因为加了消息屏障,会把异步消息的优先级提到同步消息的前面,所以,执行完异步消息,同步消息的执行时间早就过了,肯定要立刻执行呀。
说了这么多,那么,这个提升优先级是怎么实现的呢?透过现象去看本质。两段代码的区别,就是通过反射,执行了两个方法postSyncBarrier,removeSyncBarrier。其中还有一个带参数的方法。
1 | //执行消息屏障 |
我们先来看看这个消息屏障的方法:
1 | @UnsupportedAppUsage |
上面这个执行消息屏障说的很清楚了吧?多看注释,多理解。
我们再来看看这个移除消息屏障
1 | @UnsupportedAppUsage |
总结一下这个提升消息优先级的方式就是:把你想发送的消息定义view异步消息发送,光这样还不行,还要发送一条屏障消息,具体流程:
- 往消息队列里面插入一条target为null的消息
- MessageQueue.next()方法读取的时候,会先判断这条消息是不是屏障消息,如果是,他就会执行do..while循环,直到找到一条异步消息为止。
- MessageQueue拿到消息之后,正常的取消息流程
- 在你执行完这条异步消息之后,记得要移除屏障消息,不然所有的异步消息都在同步消息前面执行。
其实有个更简单的方法,handler发消息的api都给出来了
1 | //发送消息到队列前面 |
经过上面的整个流程之后,最后一个问题就比较简单了,自己看一下源码吧,我给出结论:
1 | Looper.prepare():给当前线程创建Looper,MessageQueue的过程,这个MessageQueue是可退出的 |
handler知识点总结
总结一下handler的东西:整理了一个流程图: