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的规划就采用了出产者-顾客这一模型。

反思 Android 音讯机制的规划与完结

了解二

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

换个姿态,带着问题看Handler

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

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


2 传送带模型

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

假如直接从传送带开端,那必定很突兀,咱们有必要站在大局去考虑,那么,在这套体系内,“最大”的角色便是进程,因而,咱们从进程开端。

已然是出产者-顾客模型,出产,一般是在工厂里出产,那么咱们的整个场景,便是工厂的场景。

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)的吗?在这座工厂内,应该要有一个专门的取出货槽的点(就像超市的购物推车,它有一个专门的寄存点,然后咱们购物是去那里取一个出来用,而不是自己造一个出来,就算一开端没有任何推车,需求造,那也不是咱们去造,咱们只是取来用。)

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

好,那已然关于咱们来说,咱们只是取货槽来用的话,那么这个工厂就得给咱们供给一个“取”的办法(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的设计之传送带模型

那么以上便是以传送带模型的视角讲述的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层的思维,都是有借鉴的。

最终,假如想看源码的剖析,附上一篇自己的学习笔记。

希望这篇文章,对你能有协助,就写到这儿吧~