Android Handler机制探索及原理分析

        Handler在android开发中占有举足轻重的位置,相信大家都熟悉其用法及基本使用。

Handler是什么?

        Handler是安卓提供的一种消息机制。通常用于接受子线程发送的数据,并用此数据配合主线程更新UI。

为什么要用Handler?

        举个例子,我们点击一个按钮去服务器请求数据。如果直接在主线程(UI线程)做请求操作,界面会出现假死现象, 如果长时间还没有完成的话,会收到Android系统的一个错误提示  "应用程序无响应(ANR)"。为什么呢?因为在Android里, App的响应能力是由Activity Manager和Window Manager系统服务来监控的. 通常在如下三种情况下会弹出ANR对话框:

1:KeyDispatchTimeout(谷歌default 5s,MTK平台上是8s) --主要类型  按键或触摸事件在特定时间内无响应

2:BroadcastTimeout(10s)  BroadcastReceiver在特定时间内无法处理完成

3:ServiceTimeout(20s) --小概率类型  Service在特定的时间内无法处理完成。

        既然这样,那我们就把这些耗时操作放在子线程中执行好了。 可问题来了,需要把数据填充到相关控件中展示。但Android中更新UI只能在主线程中更新,子线程中操作是危险的。那怎么走出这个困境,用Handler!

1.试想一下:如果在一个Activity中,有多个线程去更新UI,并且都没有加锁机制,那么会产生什么样的问题?——更新界面混乱。

2.是不是在Android中子线程真的不能更新UI?这也不一定,之所以子线程不能更新UI界面,是因为Android在线程的方法里面采用checkThread进行判断是否是主线程,而这个方法是在ViewRootImpl中的,这个类是在onResume里面才生成的。因此,如果这个时候子线程在onCreate方法里面生成更新UI,而且没有做耗时多的操作,还是可更新UI的。你可以验证一下(打开注释和注释那行代码比较下):

protected void onCreate(@Nullable Bundle savedInstanceState) {

        ......

      new Thread( new Runnable() {   

                                @Override            public void run() {

                                    //步骤A try{ Thread.sleep(200)}catch(...){...};

                                    tv.setText("子线程中访问");

                                }

                        }).start();

        ......

}

怎么使用Handler?

        我们模拟子线程网络请求,完成后更新UI操作。

step1:

创建Handler对象 ,重写 handleMessage 方法

step2:

子线程耗时操作 发送消息

注:代码中Message为Handler接收与处理的消息对象。 

看看运行后打印结果:

运行结果

根据上面打印日志可以看出handlerMessage运行在main线程,故可以在这里更新ui。而且发送的what成功正确接收到为1000.

当然Handler还有很多其他用法比如:

1.post(Runnable)  ,postDelayed(Runnable ,long); 

2. sendMessage(Message),sendMessageDelayed(Message,long)

3.sendEmptyMessage(int),sendEmptyMessageDelayed(int,long)

....

这里就不一一列举了。我们在开发中常用的延时操作往往借助上面 handler.xxxDelayed方法来轻松实现。    其实使用上述不管哪种方法都调用了sendMessageDelayed函数,所以实质都是一致的。我们随便拿一个看看其内部实现(如:post(Runnable) ):

handler.post(runnable)方法源码

上面我们在主线程创建并使用了Handler对象,可以正常在子线程发送消息并成功接收。那么就有好事者说了“我要在子线程创建并使用Handler可不可以,没别的意思,就是任性想玩玩”。好吧,闲话少说,那就我们就来试试...

子线程创建并使用Handler

我们在子线程中试试Handler呀

实际上写出这个还没运行,我掐指一算就觉得这样会有问题(就不告诉你我提前已经偷偷了解了,哈哈)。

子线程 · handler · error

看红框部位的报错信息,知道是线程里创建Handler没调用Looper.prepare()方法.这是什么鬼?我们定位到创建Handler时使用的构造函数处:

构造函数-Handler(Callback callback, boolean async)

也就是说Looper.myLooper()得到的对象为空,就抛出了"Can't create handler inside thread that has not called Looper.prepare()"这个异常。进入Looper.myLooper()一探究竟:

Looper.myLooper()

sThreadLocal.get()返回一个空对象。那sThreadLocal又是什么?在Looper类源码中可以看到:

sThreadLocal

sThreadLocal是一个ThreadLocal 静态变量。那ThreadLocal是什么有什么作用?

ThreadLocal是什么?

        ThreadLocal 是线程的局部变量, 是每一个线程所单独持有的。当使用ThreadLocal维护变量的时候 为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个该变量,这样同时多个线程访问该变量并不会彼此相互影响,因此他们使用的都是自己从内存中拷贝过来的变量的副本, 这样就不存在线程安全问题。

什么意思?做个实例来体验一下以便于理解。在此之前先介绍下ThreadLocal常用方法--->

public T get() { } // 获取ThreadLocal在当前线程中保存的变量副本               

public void set(T value) { } //set()设置当前线程中变量的副本                           

public void remove() { } //移除当前线程中变量的副本               

protected T initialValue() { } //一般是用来在使用时进行重写的

ThreadLocal使用实例
TheadLocal 日志

通过上面例子应该可以检验“为每一个使用该变量的线程提供一个独立的变量副本”的含义了吧。。。

如果想要了解ThreadLocal内部原理移步:Java多线程编程-(8)-多图深入分析ThreadLocal原理

现在我们清楚了ThreadLocal的作用,继续回到上面对Looper的分析。因sThreadLocal.get()返回null,导致异常发生。根据错误日志,需要先调用 Looper.prepare()方法才行。那我们可以推测,prepare()方法里面应该做了sThreadLocal.set(looper)操作。

Looper<--->MessageQueue

那么我们具体看看Looper.prepare()了什么?

Looper.prepare()

果不其然,同我们上面推测的一致,sThreadLocal调用set方法保存了一个looper变量,同时可以知道对于每一个线程只能有一个Looper。接下来看看Looper(boolean) 构造函数:

Looper构造函数

也就是说我们创建一个Looper对象,MessageQueue (按字面意思理解:消息队列就被创建了。也可以发现,默认情况下,这个MessageQueue的quiteAllow=true。

我们可以发现:在一个线程中如果存在Looper则 Looper和与之关联的MessageQueue都是唯一的。那MessageQueue是什么?它是不是与Message有些啥暧昧关系呢?看着像是Messa-geQueue包养了一队列的Message。(逃...)

MessageQueue 与 Message

前面我们用到并简单介绍了下Message,现在再对它做一下详细的分析...

Message截取部分代码

前面四个what,arg1,arg2,obj属性在用Handler经常会用到,应该很熟悉。另外还有几点值得注意:

Message 实现Parcelable 接口,也即实现了序列化,说明Message可用于进程间通信。                                                                                                                   

有一个Handler 对象 target(跟我们使用的Handler有没有关?)                                                                               

有个callback的Runnable 对象(是不是想到Handler.post(runnable) 这个runnable)                   

推荐使用obtain(),该方法可以从消息池中获取Message实例,不推荐直接调用构造方法。

好吧现在Message内部的一些重要特征我们都已打探清楚,那MessageQueue到底是什么呢?

MessageQueue构造函数

nativeInit()方法实现为android_os_MessageQueue_nativeInit();这是一个底部方法在此不做细究。有兴趣的自行深入分析。如果只想要了解MessageQueue的作用我们换种方式直接看看其注释,便会一目了然:

MessageQueue 注释

也就是说MessageQueue确实是用来存放消息(Message对象)的容器(可按字面意思理解为队列)。

其实MessageQueue数据结构,实质是一个单向链表,Message对象有个next字段保存列表中的下一个,MessageQueue中的mMessages保存链表的第一个元素。

既然是“队列”那它就有其常规操作:

入队 :  boolean enqueueMessage(Message msg, long when) {...}

出队:  Message next() {...}

这里先不详细介绍等我们下面按流程分析到在说。

到此为止我们大概搞清楚:

1.如果希望Handler正常工作,在当前线程中要有一个Looper对象

2.在初始化Looper对象时会创建一个与之关联的MessageQueue;

3.MessageQueue存放并管理Message

4.Message是Handler接收与处理的消息对象

上面一步步分析,都可谓是为了破解“在子线程创建Handler”所引发的这一"血案"。至于怎么解决“子线程创建Handler”报出error,相信通过以上讲解,很容易就大手一挥写出以下代码:

子线程·Handler ·+ looper.prepare

然后运行一下,当然没有报任何异常.但我们发现也没有任何日志打印出来。也就是说我们发送的消息(sendMessage)没有接收到。那是什么原因?现在该怎么办?此刻就有一个疑问浮现在脑海---“为什么主线程创建Handler可以正常工作?是不是它做了其他的操作?”

初遇Looper.loop()...

        我们找到android应用程序的入口ActivityThread中的main方法。ActivityThread就是应用程序的主线程,打开它的main方法可以看到:

ActivityThread  main()片段

Looper.prepareMainLooper() ?进去看看

Looper.prepareMainLooper()

确实,Looper.prepareMainLooper() 调用了prepare()方法(注意这里调用prepare时传递的参数值为false,和我们之前创建普通Looper时是不同的。因为这是主线程,不会被允许被外部代码终止),所以现在知道为什么在主线程直接创建Handler而不抛异常了吧。然后,后面还有个Looper.loop()?是不是就是少这步操作我们子线程Handler没有正常工作,那加上试一下:

子线程.Handler.can.use

真的可以正常工作,打印出log了!

log

那不禁要问,为什么加了个Looper.loop()就可以正常接收消息了呢?我们这里暂时不深究loop()内部到底什么原理,可以暂按得到的效果和其字面含义理解为一个获取消息的循环(轮询)。至此Handler消息已经可以正确接收,那我们不妨先看看消息是怎么发送的,因为有入才有出,我们就先从“入”这个源头扒起。

Handler发送消息实现原理

发送消息我们会调用handler.post(),handler.sendMessage()等方法,前文也已经分析到所有这些方法实质都调用的是sendMessageDelay():

sendMessageDelay

继续看sendMessageAtTime:

sendMessageAtTime

在前文Handler的构造函数中我们知道mQueue=mLooper.mQueue,也即是Looper中关联的MessageQueue对象。执行至equeueMessage:

equeueMessage

特别注意:msg.target = this 这句代码,该message的target赋值为当前的handler对象,这里Message就和当前Handler关联起来了,记住msg.target很重要,后面我们会用到。

我们可以看到最后执行的是MessageQueue的enqueueMessage方法,前面在介绍MessageQueue的时候我们就知道了这个enqueueMessage方法是用于"入队"操作的。看看源码也就清楚:

enqueueMessage部分源码

参数msg是由我们传进去Message对象,when时是执行时间,细心的朋友会发现mMessages这个对象,我们可以把它理解为是待执行的message队列,该队列是按照when的时间排序的且第一个消息是最先执行。

代码第4行中有三个条件:如果mMessages对象为空,或when为0也就是立刻执行,或者新消息的when时间比mMessages队列的when时间还要早,符合以上任一条件就把新的msg插到mMessages的前面 并把next指向它,也就是msg会插进队列的最前面,等待loop的轮询。

如果上面的条件都不符合就进入else中,我们可以看到17行是有个for的死循环遍历已有的message对象,其中第20行有个if语句when < p.when when是新消息的执行时间,p.when的是队列中message消息的执行时间,如果找到比新的message还要晚执行的消息,就执行

msg.next = p; 

prev.next = msg;

也就是把插到该消息的前面,优先执行新的消息。

现在我们搞清楚了sendMessage最终就是将消息(Message)放入MessageQueue里面,由其存放并管理,那接下来我们要明白的一点就是怎么取出消息了。那就回到了Looper.loop()的解释了。

消息接收:再续Looper.loop()

looper.loop()片段-1


looper.loop()片段-2

片段1好理解,我们重点分析片段2 ;

这里要给大家说一下,Linux的一个进程间通信机制:管道(pipe)。

原理:在内存中有一个特殊的文件,这个文件有两个句柄(引用),一个是读取句柄,一个是写入句柄

主线程Looper从消息队列读取消息,当读完所有消息时,进入睡眠,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。所以不会过多消耗性能。

looper里面是一个for死循环,看画红线的第一行代码,是从MessageQueue中提取Message,注释可能造成阻塞。我们进到MessageQueue的next里面看看:

MessageQueue · next 部分代码

简单分析一下,if(now < msg.when)  若当前时间还没到msg指定时间,设置一个timeout以到时用于唤醒;else msg执行出队操作。另外可以发现当msg=null,当也即消息队列为空,nextPollTimeoutMillis置为 -1 ,next就会阻塞。

回到loop()方法片段2,我们得到了Message对象,继续向下走到第二处红线位置,也就到了分发处理Message的时候了。

        msg.target.dispatchMessage(msg);

记不记得我们分析发送消息时提到的一个"特别注意"事项(msg.target = this 这句代码,该message的target赋值为当前的handler对象),在这里果然用到了。也就是说Handler发送的每个Message对象都存在有该Handler的句柄(target),所以这里实质就是调用了handler的dispatchMessage方法,那我们进入Handler.dispatch方法:

Handler.dispatch

现在该明白了为什么我们在使用Handler的时候复写其handlerMessage方法,可以在内部接收并处理消息了吧。是不是有种柳暗花明的感觉。

到此为止,我们已经把Handler的整个工作原理撸得差不多了,大致梳理一下。

当我们使用Handler时,在当前线程必须有且仅有一个Looper对象,Looper里面维护一个MessageQueue。所以需要提前调用Looper.prepare()方法将loop对象设置到内部静态ThreadLocal中(以保证线程安全)。(主线程已设置了)

我们用Handler主要是发送和接收消息。

对于发送消息:通过post,send等方法(实质都是send)发送消息Message(runnable最后也转化为message)实现。最终都是调用MessageQueue的enqueueMessage方法将Message插入MessageQueue(上面所说Looper中所维护的)这一单向链表中进行统一管理。

接收消息:我们必须开启Looper.loop()来轮询,通过调用MessageQueue中的next()方法移除并获取之前发送的Message对象(MessageQueue为空时,next阻塞)。获得的Message对象最终交由发送该消息的Handler对象(message.target)的dispatchMessage方法处理。而dispatchMessage方法最后会走到handlerMessage()方法去。所以我们在创建Handler时才能够在其handlerMessage()或callback.

handlerMessage方法中获取发送的消息并做出系列操作。

那么至此不经要问,主线程中的Looper.loop()一直无限循环为什么不会造成ANR

主线程-loop()-anr

首先,我们想一下,主线程要没有Looper.loop()会怎么样?

显而易见,如果应用入口main方法中没有looper进行循环,那么主线程一运行完毕就会退出。那我们打开一个app就过段时间直接关闭了。也就是说:ActivityThread的main方法主要就是做消息循环,一旦退出消息循环,那么你的应用也就退出了。

我们知道了looper.loop()是必须的再讨论这个死循环为啥不会造成ANR异常?

      因为Android 的是由事件驱动的,looper.loop() 会不断地接收/处理事件,每一个点击触摸或者说Activity的生命周期都是运行在 Looper.loop() 的控制之下,如果loop停止,那应用也就停止了。所以只能是某一个消息或者说对消息的处理时间超过系统规定时长(阻塞)才会导致ANR,而不是looper.loop()本身。


end...


因个人水平有限,有纰漏或错误处还望大家批评指正,谢谢!

参考:

终于明白了Handler的运行机制 - CSDN博客

主线程中的Looper.loop()一直无限循环为什么不会造成ANR? - CSDN博客

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容