作者:彭泰强
有时候在Handler相关的文章中可以看到,会把Handler机制的几个角色类比成一个传送带场景来理解。
例如,这篇文章中写到: 我们可以把传送带上的货物看做是一个个的Message,而承载这些货物的传送带就是装载Message的消息队列MessageQueue。传送带是靠发送机滚轮带动起来转动的,我们可以把发送机滚轮看做是Looper,而发动机的转动是需要电源的,我们可以把电源看做是线程Thread,所有的消息循环的一切操作都是基于某个线程的。一切准备就绪,我们只需要按下电源开关发动机就会转动起来,这个开关就是Looper的loop方法,当我们按下开关的时候,我们就相当于执行了Looper的loop方法,此时Looper就会驱动着消息队列循环起来。
但感觉并不是很准确。
所以接下来这篇文章的内容,就是讲一下自己如何用传送带模型来理解Handler体系,并且会用这个模型去解释一些基本的(面试)问题。
这篇文章并不会系统的去分析Handler机制的方方面面,因为网上有太多太多文章了,或者能直接自己去读读源码,那是更好不过了。这篇文章的作用仅仅是引入一个易理解的正确的模型,以便帮助你自己分析Handler机制的时候减少困惑、更加容易。
这篇文章仅仅是抛砖引玉。
首先要知道,为什么要引入Handler机制、Handler机制解决了什么问题、为什么选择了它而非别的模型。
这两篇文章对这些问题的解读非常好,所以这里直接引用过来并加以概括了。
理解一
Android 应用中触摸事件的分发、View的绘制、屏幕的刷新以及 Activity 的生命周期等都是基于消息实现的。这意味着在 Android 应用中随时都在产生大量的 Message,同时也有大量的 Message 被消费掉。
另外在 Android 系统中,UI更新只能在主线程中进行。因此,为了更流畅的页面渲染,所有的耗时操作包括网络请求、文件读写、资源文件的解析等都应该放到子线程中。在这一场景下,线程间的通信就显得尤为重要。因为我们需要在网络请求、文件读写或者资源解析后将得到的数据交给主线程去进行页面渲染。
那在这样的背景下,就需要一套消息机制,即具有缓冲功能又能实现线程切换,而”生产者-消费者“模型正好能实现这个需求。因此Handler的设计就采用了生产者-消费者这一模型。
理解二
比方说网络请求等耗时操作是要在其他线程进行的,那么多个线程同时对同一个UI控件进行更新,就容易发生不可控的错误。那么,最简单的处理方式就是加锁,但不是加一个,而是每层都要加锁,但这样也意味着更多的耗时,也更容易出错。而如果每一层共用一把锁的话,其实就是单线程,所以Android的消息机制没有采用线程锁,而是采用单线程的消息队列机制。
接下来,我将给出我理解的传送带模型,并用它来理解和解释一些问题。
注:分析过程中几乎不会贴代码,请自己去阅读源码,这很重要,关键处我都给了源码的变量或函数。
如果直接从传送带开始,那肯定很突兀,我们必须站在全局去思考,那么,在这套系统内,“最大”的角色便是进程,因此,我们从进程开始。
既然是生产者-消费者模型,生产,一般是在工厂里生产,那么我们的整个场景,就是工厂的场景。
进程,就是一个工厂。
现实世界里,每个工厂之间肯定不是紧挨着的,是隔了很远甚至是在异地两个不同城市的——进程之间是数据隔离的。
线程,就是一座工厂里的一个车间。
那么,我们的App就是在一个单独的工厂(进程)里运转,那么它可能会有多个车间(线程),且有一个主车间(主线程、UI线程),我们这时候就需要一套处理机制,来完成主车间和其它车间(线程)之间的协作,这套机制就是Handler机制。Handler机制,或者说这种“生产者-消费者”模型,就可以看作是“传送带运送货物”的模型。
Handler机制的四大主角分别是Handler、Looper、MessageQueue、Message,我们接着往下看,先从最简单的Message开始。
Message,就是放货物的货槽(篮筐)。
你以为Message是货物?那就错了。Message更准确的理解应该是装货物的货槽(就好像超市里那种购物推车)。
什么是放货物的货槽/篮筐?货物自然是我们想要运输的东西,但是,直接把一个货物扔到传送带上输送,是不是有点不太合理?比如,这个货物要输送到哪?它的标识信息又是什么?
再说具体些,假设我们这里的货物是苹果。那么,我们是把苹果放到货槽 (或者叫篮筐、货框之类的吧,我也不知道叫什么,自己意会一下) 里面,然后再扔到传送带上,而非直接扔到传送带上去运输的。这有什么好处?
这样,你的货槽就能装任意类型的货物,而货槽本身,也就可以携带一些信息,例如:这个货物发往何处、什么时候该被送达、这个货物是什么(货物本身的信息)。
让我们看看Message类的属性:
what: int
用于标识Message
arg1、arg2: int
少量数据
obj: Object
任意数据
data: Bundle
大量数据
flag: int
标志,该标志实际上是两个标志,为了节省变量,合二为一了,一是该Message是否为异步消息,二是该Message是否被消费
when: long
要被处理的目标时间
target: Handler
交给谁去处理(即分发到哪里、目的地)
callback: Runnable
消息处理的回调
next: Message
下一个货物
我们继续想,货槽本身应该是类似于筐子一样的东西,那么,它是随随便便就能生产(new)的吗?在这座工厂内,应该要有一个专门的取出货槽的点(就像超市的购物推车,它有一个专门的存放点,然后我们购物是去那里取一个出来用,而不是自己造一个出来,就算一开始没有任何推车,需要造,那也不是我们去造,我们只是取来用。)
好,那既然对于我们来说,我们只是取货槽来用的话,那么这个工厂就得给我们提供一个“取”的方法(Message.obtain),而这样,也就自然而然实现了货槽的复用,货槽只是货物的载体,又不是货物,那我肯定是可以复用的嘛,当货槽的货物被分发处理后,货槽空了,就回到了工厂的货槽存放点(sPool),下次要取,又可以从工厂的货槽存放点来取货槽用。
而货槽存放点在整个工厂内就只有一处,但工厂可能有多个车间(线程),那么不同车间如果同时去这个存放点取货槽,是不是得有个先后顺序?是不是得排队?同时如果多个车间同时用完了货槽,想把它扔回存放处,是不是也得有个先后顺序?ok,所以代码中对sPool的访问需要synchronized(sPoolSync)。
关于货槽(Message),就先说到这里。
Looper,就是一个车间里的一条传送带。
Looper是这个车间的传送带,翻译一下就是,Looper当前线程消息循环的实际控制者。
还有一件事,既然只有一条传送带,那么怎么样最恰当地决定传送带上的货槽的顺序?那当然是按货槽预定要被处理的时间去排序咯。你想,如果我们在把货槽加入货槽链的时候,就按照预定要被处理的目标时间去排好序,那传送带是不是只管往前转就行了?传送带往前转,到达尽头的就一定是下一个要被处理的货物,预定时间一到,就可以把它取出来进行后续处理,而如果还没到预定的处理时间,我们也仅仅只需要把传送带停下,让它等在那儿,等时间到就好了,不再需要任何多余的操作了。这就是为什么message在enqueue时要按照message.when进行排序,因为这是最方便的。
Looper就先说这么多。
MessageQueue,就是操纵货槽链的操纵器。
我们如果想往传送带上扔货物,之前提了,得有个按when排序的操作,如果每次都是我们亲自来进行这个操作,那就太麻烦啦,于是我们只管把货槽交给控制器,让控制器去帮我们搞定(enqueue),同理,当货槽到达传送带的尽头,需要取出来用时,也是控制器去帮我们去取(next),如果我们在货槽还没到达传送带尽头时,中途不想要它了,那也是控制器去代操作(remove)。所以说我们是需要这样一个货槽链的控制器的。
那么,因为货槽链的控制器需要能直接对货槽链去操作,所以它(MessageQueue类)自然需要能够持有货槽链(Message)的引用。
此外,一条传送带也只有一条货槽链,那也就只需要一个控制器,那么也就是,一个车间只有一条传送带、一条货槽链、一个控制器,翻译:一个Thread只有一个Looper、一个头部Message节点、一个MessageQueue。
好,MessageQueue就先说这么多,接下来就是整个Handler机制最后的一个主要角色了。
Handler,就是车间里的打工人。
有了工厂、有了车间、有了传送带、有了传送带的操作装置、有了货槽货物,还差啥啊?那当然是打工人了。
打工人(Handler)先去货槽存放处取一个空货槽(obtain),把货物包封装好,放进货槽(得到了Message),然后设置好各种参数,然后交给货槽链控制器(Handler的各类postXX、sendXX等),然后控制器负责把新的货槽添加进货槽链(messageQueue.enqueue)。与此同时传送带(Looper)也一直在运行(Looper.loopOnce),然后当传送带上有货槽到达了尽头,如果预定的处理时间没到,就暂停传送带,等时间到,而如果到了,控制器就会把这个货槽交给传送带下游的打工人(Handler),打工人取到货槽,就去作处理。而传送带则是进入下一个循环,如此往复(messageQueue.next返回,继续loopOnce)。
我画了一张草图,大概描绘了一个车间内的工作,辅助理解(画的很抽象,不要介意)。
那么以上就是以传送带模型的视角讲述的Handler机制的最基本的完整的流程,现在我们已经非常清楚了。接下来我们再以传送带模型的视角来思考一些问题,或者来看看,这些所谓的面试题,会瞬间感觉非常简单、理所当然。
随便解释几个问题,一部分是理解上的问题(我自己编的问题),一部分是一些常见面试题。
当然你面试时可不能这么答,你说传送带面试官肯定不鸟你,还是得讲代码的,但代码就自己去看了。
Q1:同一线程内,Handler和Looper都持有同一个MessageQueue对象?
是的,这个车间里每个打工人都需要与货槽链交互(放货物、取货物),因此都会需要操纵货槽链控制器(MessageQueue),同时,Looper是传送带,它也需要操纵货槽链,因此也需要有同一个控制器。
Q2:Handler的作用仅仅是更新UI?
NoNo,格局小了,你这个工厂,不会只做UI吧?你做的可是一个完整的APP。因此,UI事件只是一种类别的货物,那肯定还有其它多种多样的货物啊,整个工厂都依赖于这套Handler机制来进行协作呢。去看AOSP,处处有Handler机制。可以说,Binder负责的是工厂之间的交互,那么工厂内不同车间的很大一部分交互,都是交给Handler机制的,所以问起Handler机制的作用,说它是用于更新UI的,未免小看它了。
Q3:Handler.send和post的区别?
send的是货槽(Message),已经封装好了的货槽,直接进行后续的入队操作就行。而post的是一个“事件”,这个“事件”就是一个抽象的货物,它被记录在货槽(Message)的callback属性上,下游打工人(Handler)收到这个货槽,一看,哦,发现它表示着一个事件要被执行了,那么接下来就是去执行这个事件。
Q4:Handler如何分发处理消息?
这就是在问下游打工人收到货槽怎么办(Handler.dispatchMessage),那自然是:
Q5:Handler机制如何实现线程的切换?
就是在问,一个货物,是怎么从A车间分发到B车间的。
打工人A(handlerA)在A车间(ThreadA)工作,打工人B(handlerB)在B车间(ThreadB)工作,打工人A封装了一个货槽,把货槽的目的地(Message.target)设置给打工人B,那好,当货槽到达A车间的LooperA的下游且要处理时,就会被交给B车间的打工人B(同一进程不同线程数据共享),那打工人B在车间B拿到了货槽,这不就完成了车间之间的交互了吗?也就是实现了线程的切换。
Q6:写代码时怎么创建Message?
Message是货槽,之前也分析了,那自然是要从货槽存放处去获取(obtain,更具体地,可以Handler去obtain,也可以Message.obtain),而不是我们自己去创建(new),就算要造新货槽,也是存放处的职责(obtain时sPool里没有了,则new),这样的好处就是避免了频繁的创建和销毁的开销(对象复用,以减少内存抖动),货槽就像购物推车,买完东西就放回存放点,本来就是可以复用的。
Q7:Handler机制是如何保证多线程下,MessageQueue的有序性的?
那答案肯定是加锁嘛。不过我们再用传送带模型想想,假设有两个不同车间(Thread)的打工人(Handler)都需要去往同一个车间的货槽链放东西,那就得保证先来后到嘛,因为货槽链必须是按when顺序排列的。所以就要加锁,锁谁?锁货槽链的操纵装置就行了呗,也就是synchronized(MessageQueue.this)。
Q8:为什么主线程可以直接new Handler(),子线程不行?
new Handler就是说,在这个车间(线程)新入职一个打工人,那为了不出错误,他入职这个车间的时候,就要提前先跟他说好:你主要就负责这个车间的传送带工作。并且,为了不出错,在他入职(new Handler)之前,这个传送带得先跑起来(Looper.prepare&loop)。
而主车间(主线程)的传送带在整个工厂一开工的时候就跑起来了(App进程的main调用了Looper.prepare&loop),自然之后主车间不需要再去启动了,如果有新打工人,直接开始干活就行了。
但一个新的车间(子线程)不行,它想开始传送货槽,肯定得先开传送带嘛,然后再让新打工人来干活,这样就万无一失了。
Q9:Handler所发送的Delayed消息时间一定准确吗?
基本上是准确的,但也有很少数的例外,比如,多个车间的打工人(不同线程的Handler)同时想去取同一个货槽链的货槽(同时MessageQueue.next),那总得排个先后顺序(即加锁,synchronized(MessageQueue.this)),这时,可能会因此而产生小的延迟。
Q10:主线程的Looper什么时候退出?为什么主线程Looper没消息不会ANR?
主线程Looper正常情况下不会退出。如果连你工厂的主车间都倒了,那你工厂也倒了得了。
顶多货槽链为空(没有消息)时,打工人终于可以休息一下了(休眠)。至于ANR,这里不深入展开,大概的理解就是,ANR是由于打工人没有及时处理消息(但是这个说法是很不准确的啊,只是为了大概理解一下,ANR的细节很多,往深了说可以很深,不展开了不展开了)导致的,这么看,主线程一直loop死循环,和ANR一点关系都没有,反而主线程必须一直靠死循环来运行,否则程序就退出了,所以这个问题就是很无聊的一个问题,但一直出现在面试题里。
Q11:同步屏障机制?
那就是这个货槽要被加急处理呗,说白了就是想插队,那么当一个货槽被标记为FLAG_ASYNCHRONOUS,它就可以插队(它入队的时候也是和普通的同步消息一起按时间排列的,整个消息队列中不论同步异步消息,一并按when排列)。
当我们想给异步的货槽插队时,就可以通过货槽链控制器去给一个插队指令(即MessageQueue.postSyncBarrier,这个插队指令就是同步屏障,它也是一个Message,但target是null,意味着没有目的地Handler)。
那么,在传送带不断取消息(Message.next)时,如果发现货槽链的头部是一个插队指令货槽,就会从头开始遍历整个货槽链,去寻找标记为异步的可插队货槽,优先把它取出,而非再按队列顺序了。直到这个插队指令货槽被remove掉,所以发布了插队指令,就一定要记得移除,也正因如此,发布插队指令(postSyncBarrier)的操作被标记为hide,不让一般开发者使用。
为了保证UI及时更新,那么UI更新的消息,就是个异步消息,这里不展开了,自行了解。
Q&A的这部分就先说这么多吧。
这篇文章以传送带模型的视角来分析了Handler机制,然而,Handler机制远比这些要复杂,但这篇文章的作用本来也不是为了能让你看完就掌握Handler机制,而是抛砖引玉,让你能够清楚地对整个Handler机制有个大体的认识,这样至少不会在看源码时云里雾里。想更进一步,必须自己读源码,然后去实践。并且,越往后学,越会发现,很多东西的设计都是贯通的,Handler的设计思想,包括它的native层的思想,都是有借鉴的。
在开发中,我们除了要对Framework 中的Handler 知识点要有所了解,还有对其他知识点了解,这边耗时两个多星期时间进行精细化整理,将这《Android Framework学习笔记》 整理好了,里面记录了:有Handler、Binder、AMS、WMS、PMS、事件分发机制、UI绘制……等等,几乎把更Framework相关的知识点全都记录在册了,学习视频整理好了,由于平台限制就不展示了
https://qr18.cn/AQpN4J
Handler 机制实现原理部分:
1.宏观理论分析与Message源码分析
2.MessageQueue的源码分析
3.Looper的源码分析
4.handler的源码分析
5.总结
Binder 原理:
1.学习Binder前必须要了解的知识点
2.ServiceManager中的Binder机制
3.系统服务的注册过程
4.ServiceManager的启动过程
5.系统服务的获取过程
6.Java Binder的初始化
7.Java Binder中系统服务的注册过程
Zygote :
AMS源码分析 :
深入PMS源码:
1.PMS的启动过程和执行流程
2.APK的安装和卸载源码分析
3.PMS中intent-filter的匹配架构
WMS:
1.WMS的诞生
2.WMS的重要成员和Window的添加过程
3.Window的删除过程
https://qr18.cn/AQpN4J