彻底理解Handler的设计之传送带模型

作者:彭泰强

0 这篇文章的目的

有时候在Handler相关的文章中可以看到,会把Handler机制的几个角色类比成一个传送带场景来理解。

例如,这篇文章中写到: 我们可以把传送带上的货物看做是一个个的Message,而承载这些货物的传送带就是装载Message的消息队列MessageQueue。传送带是靠发送机滚轮带动起来转动的,我们可以把发送机滚轮看做是Looper,而发动机的转动是需要电源的,我们可以把电源看做是线程Thread,所有的消息循环的一切操作都是基于某个线程的。一切准备就绪,我们只需要按下电源开关发动机就会转动起来,这个开关就是Looper的loop方法,当我们按下开关的时候,我们就相当于执行了Looper的loop方法,此时Looper就会驱动着消息队列循环起来。
但感觉并不是很准确。

所以接下来这篇文章的内容,就是讲一下自己如何用传送带模型来理解Handler体系,并且会用这个模型去解释一些基本的(面试)问题。

这篇文章并不会系统的去分析Handler机制的方方面面,因为网上有太多太多文章了,或者能直接自己去读读源码,那是更好不过了。这篇文章的作用仅仅是引入一个易理解的正确的模型,以便帮助你自己分析Handler机制的时候减少困惑、更加容易。

这篇文章仅仅是抛砖引玉。


1 Handler机制?

首先要知道,为什么要引入Handler机制、Handler机制解决了什么问题、为什么选择了它而非别的模型。

这两篇文章对这些问题的解读非常好,所以这里直接引用过来并加以概括了。

理解一

Android 应用中触摸事件的分发、View的绘制、屏幕的刷新以及 Activity 的生命周期等都是基于消息实现的。这意味着在 Android 应用中随时都在产生大量的 Message,同时也有大量的 Message 被消费掉。

另外在 Android 系统中,UI更新只能在主线程中进行。因此,为了更流畅的页面渲染,所有的耗时操作包括网络请求、文件读写、资源文件的解析等都应该放到子线程中。在这一场景下,线程间的通信就显得尤为重要。因为我们需要在网络请求、文件读写或者资源解析后将得到的数据交给主线程去进行页面渲染。

那在这样的背景下,就需要一套消息机制,即具有缓冲功能又能实现线程切换,而”生产者-消费者“模型正好能实现这个需求。因此Handler的设计就采用了生产者-消费者这一模型。

理解二

比方说网络请求等耗时操作是要在其他线程进行的,那么多个线程同时对同一个UI控件进行更新,就容易发生不可控的错误。那么,最简单的处理方式就是加锁,但不是加一个,而是每层都要加锁,但这样也意味着更多的耗时,也更容易出错。而如果每一层共用一把锁的话,其实就是单线程,所以Android的消息机制没有采用线程锁,而是采用单线程的消息队列机制。

接下来,我将给出我理解的传送带模型,并用它来理解和解释一些问题。

注:分析过程中几乎不会贴代码,请自己去阅读源码,这很重要,关键处我都给了源码的变量或函数。


2 传送带模型

彻底理解Handler的设计之传送带模型_第1张图片

如果直接从传送带开始,那肯定很突兀,我们必须站在全局去思考,那么,在这套系统内,“最大”的角色便是进程,因此,我们从进程开始。

既然是生产者-消费者模型,生产,一般是在工厂里生产,那么我们的整个场景,就是工厂的场景。

2.1 进程

进程,就是一个工厂。

现实世界里,每个工厂之间肯定不是紧挨着的,是隔了很远甚至是在异地两个不同城市的——进程之间是数据隔离的

2.2 线程

线程,就是一座工厂里的一个车间

  • 一个工厂肯定会有很多个车间——一个进程下可以有多个线程
  • 既然同属一座工厂,那么不同的车间之间,肯定是可以比较方便地通过一些手段去运输货物的(比如工厂内的那种小的货车)——线程之间数据可以共享

那么,我们的App就是在一个单独的工厂(进程)里运转,那么它可能会有多个车间(线程),且有一个主车间(主线程、UI线程),我们这时候就需要一套处理机制,来完成主车间和其它车间(线程)之间的协作,这套机制就是Handler机制。Handler机制,或者说这种“生产者-消费者”模型,就可以看作是“传送带运送货物”的模型。

Handler机制的四大主角分别是Handler、Looper、MessageQueue、Message,我们接着往下看,先从最简单的Message开始。

2.3 Message

Message,就是放货物的货槽(篮筐)。

你以为Message是货物?那就错了。Message更准确的理解应该是装货物的货槽(就好像超市里那种购物推车)。

什么是放货物的货槽/篮筐?货物自然是我们想要运输的东西,但是,直接把一个货物扔到传送带上输送,是不是有点不太合理?比如,这个货物要输送到哪?它的标识信息又是什么?

再说具体些,假设我们这里的货物是苹果。那么,我们是把苹果放到货槽 (或者叫篮筐、货框之类的吧,我也不知道叫什么,自己意会一下) 里面,然后再扔到传送带上,而非直接扔到传送带上去运输的。这有什么好处?

这样,你的货槽就能装任意类型的货物,而货槽本身,也就可以携带一些信息,例如:这个货物发往何处、什么时候该被送达、这个货物是什么(货物本身的信息)。

让我们看看Message类的属性:

  • what: int 用于标识Message
    • 这个就代表了货物本身的简要信息,表明了这个货物是什么
  • arg1、arg2: int 少量数据
    • 如果这一个货槽只需要装少量的货物,那就装在这里
  • obj: Object 任意数据
    • 如果这一个货槽想装任意类型的货物,可以装在这里
  • data: Bundle 大量数据
    • 如果这一个货槽想装大量的货物,可以装在这里
  • flag: int 标志,该标志实际上是两个标志,为了节省变量,合二为一了,一是该Message是否为异步消息,二是该Message是否被消费
    • 异步的意思是这个货槽有能力在传送带上插队(优先被处理)
    • 是否被消费即这个货槽是不是空的
  • when: long 要被处理的目标时间
    • 这个货槽应该在什么时间到达传送带的尽头然后被分发处理
  • target: Handler 交给谁去处理(即分发到哪里、目的地)
    • 货槽到了传送带的尽头,然后要开始处理了,target标志了要交给谁去处理(可以交给自己处理,所以说Handler又是生产者、又可以是消费者)
  • callback: Runnable 消息处理的回调
    • 这个属性表明,货物的类型不仅仅局限于一个具体的货物,更可以灵活地是一个“事件”,可被执行。
  • next: Message 下一个货物
    • 这就意味着,每个货槽的尾部,有一个钩子,能直接勾住下一个货槽,把所有货槽这样链起来,从第一个货槽开始,就是一条货槽的链表(队列)

我们继续想,货槽本身应该是类似于筐子一样的东西,那么,它是随随便便就能生产(new)的吗?在这座工厂内,应该要有一个专门的取出货槽的点(就像超市的购物推车,它有一个专门的存放点,然后我们购物是去那里取一个出来用,而不是自己造一个出来,就算一开始没有任何推车,需要造,那也不是我们去造,我们只是取来用。)

好,那既然对于我们来说,我们只是取货槽来用的话,那么这个工厂就得给我们提供一个“取”的方法(Message.obtain),而这样,也就自然而然实现了货槽的复用,货槽只是货物的载体,又不是货物,那我肯定是可以复用的嘛,当货槽的货物被分发处理后,货槽空了,就回到了工厂的货槽存放点(sPool),下次要取,又可以从工厂的货槽存放点来取货槽用。

而货槽存放点在整个工厂内就只有一处,但工厂可能有多个车间(线程),那么不同车间如果同时去这个存放点取货槽,是不是得有个先后顺序?是不是得排队?同时如果多个车间同时用完了货槽,想把它扔回存放处,是不是也得有个先后顺序?ok,所以代码中对sPool的访问需要synchronized(sPoolSync)。

关于货槽(Message),就先说到这里。

2.4 Looper

Looper,就是一个车间里的一条传送带。

  • 为了每个车间内的生产消费工作能有条不紊地有序进行,设计者决定,一个车间内,只能有一条传送带——一个线程只能有一个Looper
  • 既然只有一条传送带,那么自然而然地,也就只有一条货槽链。(刚刚2.3已经说过了,货物装在货槽里,每个货槽后面都有个钩子,构成了一条链)

Looper是这个车间的传送带,翻译一下就是,Looper当前线程消息循环的实际控制者。

还有一件事,既然只有一条传送带,那么怎么样最恰当地决定传送带上的货槽的顺序?那当然是按货槽预定要被处理的时间去排序咯。你想,如果我们在把货槽加入货槽链的时候,就按照预定要被处理的目标时间去排好序,那传送带是不是只管往前转就行了?传送带往前转,到达尽头的就一定是下一个要被处理的货物,预定时间一到,就可以把它取出来进行后续处理,而如果还没到预定的处理时间,我们也仅仅只需要把传送带停下,让它等在那儿,等时间到就好了,不再需要任何多余的操作了。这就是为什么message在enqueue时要按照message.when进行排序,因为这是最方便的。

Looper就先说这么多。

2.5 MessageQueue

MessageQueue,就是操纵货槽链的操纵器。

我们如果想往传送带上扔货物,之前提了,得有个按when排序的操作,如果每次都是我们亲自来进行这个操作,那就太麻烦啦,于是我们只管把货槽交给控制器,让控制器去帮我们搞定(enqueue),同理,当货槽到达传送带的尽头,需要取出来用时,也是控制器去帮我们去取(next),如果我们在货槽还没到达传送带尽头时,中途不想要它了,那也是控制器去代操作(remove)。所以说我们是需要这样一个货槽链的控制器的。

那么,因为货槽链的控制器需要能直接对货槽链去操作,所以它(MessageQueue类)自然需要能够持有货槽链(Message)的引用。

此外,一条传送带也只有一条货槽链,那也就只需要一个控制器,那么也就是,一个车间只有一条传送带、一条货槽链、一个控制器,翻译:一个Thread只有一个Looper、一个头部Message节点、一个MessageQueue。

好,MessageQueue就先说这么多,接下来就是整个Handler机制最后的一个主要角色了。

2.6 Handler

Handler,就是车间里的打工人。

有了工厂、有了车间、有了传送带、有了传送带的操作装置、有了货槽货物,还差啥啊?那当然是打工人了。

打工人(Handler)先去货槽存放处取一个空货槽(obtain),把货物包封装好,放进货槽(得到了Message),然后设置好各种参数,然后交给货槽链控制器(Handler的各类postXX、sendXX等),然后控制器负责把新的货槽添加进货槽链(messageQueue.enqueue)。与此同时传送带(Looper)也一直在运行(Looper.loopOnce),然后当传送带上有货槽到达了尽头,如果预定的处理时间没到,就暂停传送带,等时间到,而如果到了,控制器就会把这个货槽交给传送带下游的打工人(Handler),打工人取到货槽,就去作处理。而传送带则是进入下一个循环,如此往复(messageQueue.next返回,继续loopOnce)。

2.7 总结

我画了一张草图,大概描绘了一个车间内的工作,辅助理解(画的很抽象,不要介意)。

彻底理解Handler的设计之传送带模型_第2张图片

那么以上就是以传送带模型的视角讲述的Handler机制的最基本的完整的流程,现在我们已经非常清楚了。接下来我们再以传送带模型的视角来思考一些问题,或者来看看,这些所谓的面试题,会瞬间感觉非常简单、理所当然。

3 尝试随便解释几个问题

随便解释几个问题,一部分是理解上的问题(我自己编的问题),一部分是一些常见面试题。

当然你面试时可不能这么答,你说传送带面试官肯定不鸟你,还是得讲代码的,但代码就自己去看了。

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),那自然是:

  • 如果货槽本身意味着一个待处理的事件,那就直接处理这个事件(handleCallback)
  • 但如果不是,就由打工人去处理:
    • 打工人如果自己能处理,就自己处理(mCallback.handleMessage)。
    • 如果打工人不能自己处理,则交给特定工种打工人(即Handler的子类,重写了handleMessage方法)去处理。

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的这部分就先说这么多吧。

4 结语

这篇文章以传送带模型的视角来分析了Handler机制,然而,Handler机制远比这些要复杂,但这篇文章的作用本来也不是为了能让你看完就掌握Handler机制,而是抛砖引玉,让你能够清楚地对整个Handler机制有个大体的认识,这样至少不会在看源码时云里雾里。想更进一步,必须自己读源码,然后去实践。并且,越往后学,越会发现,很多东西的设计都是贯通的,Handler的设计思想,包括它的native层的思想,都是有借鉴的。

在开发中,我们除了要对Framework 中的Handler 知识点要有所了解,还有对其他知识点了解,这边耗时两个多星期时间进行精细化整理,将这《Android Framework学习笔记》 整理好了,里面记录了:有Handler、Binder、AMS、WMS、PMS、事件分发机制、UI绘制……等等,几乎把更Framework相关的知识点全都记录在册了,学习视频整理好了,由于平台限制就不展示了

《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 :

  1. Android系统的启动过程及Zygote的启动过程
  2. 应用进程的启动过程

AMS源码分析 :

  1. Activity生命周期管理
  2. onActivityResult执行过程
  3. AMS中Activity栈管理详解

深入PMS源码:

1.PMS的启动过程和执行流程
2.APK的安装和卸载源码分析
3.PMS中intent-filter的匹配架构

WMS:
1.WMS的诞生
2.WMS的重要成员和Window的添加过程
3.Window的删除过程

《Android Framework学习手册》:https://qr18.cn/AQpN4J

  1. 开机Init 进程
  2. 开机启动 Zygote 进程
  3. 开机启动 SystemServer 进程
  4. Binder 驱动
  5. AMS 的启动过程
  6. PMS 的启动过程
  7. Launcher 的启动过程
  8. Android 四大组件
  9. Android 系统服务 - Input 事件的分发过程
  10. Android 底层渲染 - 屏幕刷新机制源码分析
  11. Android 源码分析实战

你可能感兴趣的:(Framework,Android,移动开发,移动开发,android,Framework,架构,安卓)