Android Handler解读

Handler通常都会面被问到这几个问题

- 1.一个线程有几个Handler?
- 2.一个线程有几个Looper?如何保证?
- 3.Handler内存泄漏原因?
- 4.子线程中可以new Handler吗?
- 5.子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?有什么用?主线程呢?
- 6.既然可以存在多个Handler往MessageQueue中添加数据(发消息时各个Handler可能处于不同线程),那它内部是如何确保线程安全的?取消息呢?
- 7.我们使用Message时应该如何创建它

Handler的总体框架

Handler的流程

这是我在网上看到的一张图,很形象的体现Handler的工作流程,也说明了Handler几个关键类之间的关系
Handler 只负责将message放到MessageQueue,然后再从MessageQueue取出message发送出去
MessageQueue 就是传送带,上面一直传送的许多message
Looper 就是传送带的轮子,他带动这MessageQueue一直跑动
Thread 就是动力,要是没有线程,整个传送都不会开始,并且Looper还提供了一个开关给Thread,开启才会传送

image.png

MessageQueue 和 Message

添加消息

只要你使用handler发送消息,最后都会走到handler#enqueueMessag
然后调用MessageQueue#enqueueMessage,可以看到方法需要传入一个Message的

handler#enqueueMessage

handler#enqueueMessag

MessageQueue#enqueueMessage

MessageQueue#enqueueMessage

而且MessageQueue里面还存放了一个mMessage变量,有什么作用呢,让我们先来看一下Message 是什么

image.png

Message就是我们所发送的一个个消息体,而在这个类中
可以看到,一个Message变量里,又存放一个Message叫做next,存放下一个Message的,这又有啥用呢

image.png

再次回到MessageQueue#enqueueMessage,看一看这些变量到底有什么作用

image.png

首先第一个msg1进入时,p = mMessage = null,所以进入第一个if语句
所以msg1.next = p = null,mMessage = msg1

而第二个msg2进入时,假设msg2的执行时间when是在msg1之后的,
此时p = mMessage = msg1,而when(msg2.when) > p.when(msg1.when)
则if语句就不成立了,会进入else语句的for循环


image.png

此时的prev = p = mMessage = msg1,
而p = p.next(p就是msg1,msg1.next = null),此时的p就为null
所以break出去后,for循环也结束了

最后两句就是做了下图的操作
msg2.next = p = null
prev.next(msg1.next) = msg2

image.png

结构就像这样,通过这样的赋值操作,这样就形成了一个链表结构
所以MessageQueue就相当于是一个仓库,里面存放着由许许多多的Message组成的链条


image.png

取消息

取消息的方法是MessageQueue#next()方法,里面的代码先不做分析,
我们知道发送消息是handler调用的
那么取消息是谁调用的呢

image.png

根据一开始的图很容易知道,是Loop#loop()调用了该方法
而在这个方法拿到msg后 会调用 msg.target.dispatchMessage(msg)将消息发送出去,这里的msg.target 就是 handler

image.png

image.png

所以他们形成了这样一种模式,一种生产者消费者模型

image.png

也就是说要调用Looper.loop()才会取出消息去分发,但是我们再主线程的时候,都是直接使用Handler,是哪里帮我们调用了Looper.loop()函数呢,直接看到主线程的main函数就能看到,也就是说app一启动,主线程就帮我们调用了Looper.loop()函数

image.png

知道流程后,回到一开始的问题

1.一个线程有几个Handler?

这个问题其实不用说都知道,难道主线程不能使用多个Handler吗

2.一个线程有几个Looper?如何保证?

答案很简单,一个线程只有一个Looper,但是怎么保证的呢?

我们先来看看Looper是怎么创建的,是谁创建的
可以看到,Looper的构造函数只在prepare这里使用过,而且系统也有提示我们,

image.png

但是Looper存放在了sThreadLocal变量中,所以先看看sThreadLocal是什么
查阅到就是Looper中的一个静态变量的ThreadLocal类,好像看不出什么

image.png

那就进入sThreadLocal.set(Looper)方法看一下

image.png
  • 1.可以看到set方法中,首先获取了当前线程,则prepare() --> set() --> 当前线程
    也就是说,Thread1调用prepare方法,获取的当前线程也就是Thread1,不可能为其他线程。

  • 2.然后通过getMap(当前线程)获得ThreadLocalMap,也就是说Thead和ThreadLocalMap有关系。也可以看到Thread中有ThreadLocalMap的变量

    image.png

  • 3.最后将this(当前ThreadLocal)与传入的Looper保存在ThreadLocalMap中

  • 4.ThreadLocalMap就是一个保存<key,value>键值对的

所以看一下 Thread,ThreadLocalMap,ThreadLocal,Looper的关系


image.png

所以这里保证了一个Thread对应一个ThreadLocalMap,而ThreadLocalMap又保存这该Thread的ThreadLocal。问题来了<key,vaule>中key是唯一的,但是value是可以代替的,怎么能做到<ThreadLocal,Looper>保存之后Looper不会被代替呢

再回到prepare函数,可以看到在new Looper之前,还有一个get()操作

image.png

get函数做了一个操作,就是查看当前Thread对应的ThreadLocal,在ThreadLocalMap有没有值,有值则在prepare抛出异常
也就是说,prepare在一个线程中,只能够调用一次,也就保证了Looper只能生成一次,也就是唯一的

image.png

3.Handler内存泄漏原因?

我们知道,handler不能作为内部类存在,不然有可能会导致内存泄漏。为什么其他内部类不会呢?

通过java语法我们知道:匿名内部类持有外部类的对象
比如这个,handler是持有HandlerActivity的,不然也不能够调用到其中的方法,而系统是直接帮我们省略了HandlerActivity.this部分的
这就表示Handler ---持有--> this.Activity --持有--> Activity的一切内容 = 大量内存

image.png

首先我们知道,一个message是通过handler发送的,然后MessageQueue会保存
也就是说 MessageQueue ---持有--> message

接着我们再看看handler#enqueueMessage,我认为红框就是造成内存泄漏的最主要原因,我们通过代码可以看到 message.traget = this
这就意味着 message ---持有--> Handler对象

image.png

将三条链路拼接在一起 MessageQueue ---持有--> message ---持有--> Handler对象 ---持有--> this.Activity --持有--> Activity的一切内容 = 大量内存

当Handler发送了一个延迟10s的message。但是5s的时候,Activity销毁了。
此时的message是没有人处理的,即使他已经从MessageQueue扔出去了,但是Activity销毁了没人接收,也就是说这个message一只存在,则上面的这条链路是一只存在的。所以这持有的大量内存一直没人处理,虚拟机也会认为你这块内存是被持有的,他不会回收,就这样造成了内存泄漏。

所以说,Handler的内存泄漏,是说是因为匿名内部类是不够全面的

4.子线程中可以new Handler吗?

答案是可以的。
主线程和子线程都是线程,凭啥子线程不行呢,而且看了这么多代码也没看到什么地方必须要做主线程执行的方法。

下面用一段代码演示一下怎么在子线程创建Handler
首先要自定义自己的线程,在线程中创建出自己的Looper

image.png

然后再将子线程的Looper传给Handler,这样创建的Handler就是子线程的了

image.png

但是这样写会有问题吗,显然是有的
我们知道子线程是异步的,而在子线程生成和获取Looper,你怎么知道他什么时候能创建好,怎么知道在Handler创建时,Looper是有值的呢?这一下变成了线程同步问题了,很简单,线程同步就加锁呗。实际上,系统已经写好了一个能在子线程创建Handler的 HandlerThread

可以看到总体还是和我们自己写的差不多的,不过在自己获取Looper和暴露给外界获取Looper加上了锁
也就是说,如果我们在looper还没创建出来时调用getLooper会执行wait(),释放锁且等待
直到run方法拿到锁之后,获取到Looper后去notiftAll()唤醒他
这样就能保证在Handler创建时,Looper是一定有的

image.png

5.子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?有什么用?主线程呢?

我们知道Looper会帮我们在MessageQueue里面取消息,当MessageQueue没有消息了,Looper会做什么呢

首先看到获取消息的next()方法,他会调用到native层的方法nativePollOnce,当nativePollOnce取不到消息时,他就会让线程等待

image.png

所以此时的Looper.loop()方法中,系统也提示我们,会在这里阻塞住
而Looper.loop()是在子线程的run中运行的,要是一直没消息,他就会一直阻塞,run方法一直没办法结束,线程也没办法释放,就会造成内存泄露了

image.png

所以Looper给我们提供了一个方法quitSafely,而他会调用到MessageQueue的方法

image.png

他会让mQuitting = true;,接着清除message,接着nativeWake, 这与nativePollOnce是一对的,他会唤醒nativePollOnce继续执行

image.png

所以quitSafely后,next()方法会继续,因为msg = null,mQuitting = true,导致next()直接返回 null

image.png

然后再看调用next()方法的Looper.loop(),msg为null后直接return,for循环退出,loop方法也结束了。这样线程也能得到释放了

image.png

6.既然可以存在多个Handler往MessageQueue中添加数据(发消息时各个Handler可能处于不同线程),那它内部是如何确保线程安全的?取消息呢?

我们知道Looper创建时,会创建一个MessageQueue,且是唯一对应的
这也就说明一个Thread,Looper,MessageQueue都是唯一对应的关系


image.png

那么在添加消息时,synchronized (this) 的this 就是MessageQueue,而根据对应关系,这里加锁,其实就等于锁住了当前线程。就一个线程内算多个Handler同时添加消息,他们也会被锁限制,从而保证了消息添加的有序性,取消息同理

image.png

7.我们使用Message时应该如何创建它

不知道你们有没有人使用new Message()去创建消息。虽然是可以的,但是如果疯狂的new Message,你每new一个,就占用一块内存,会占用大量的内存和内存碎片

系统也提供了新建Message的方法,发现还是new Message(),那又有什么不同呢。
不同的就是sPool,他也是一个Message变量

image.png

我们回到Looper,没处理完一个消息后,他会调用Message的方法

image.png

而这个方法就是将当前的Message的所有参数清空,变成一个空的Message对象,然后放到sPool中去。等你一下需要Message变量时,他就可以重复里面

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

推荐阅读更多精彩内容

  • 有时我们需要在子线程中进行耗时的I/O操作,可能是读取文件或者访问网络等,当耗时操作完成后可能需要UI上做一些改变...
    zhuzhiqiang00阅读 1,909评论 0 2
  • 前言 1.要了解消息机制,首先要了解消息处理过程中的相关类。 ·Handler : 用来发送和处理消息·Messa...
    陈xinyu阅读 409评论 1 4
  • 在Android中,只有主线程才能更新UI,但是主线程不能进行耗时操作,否则会产生ANR异常,所以常常把耗时操作放...
    雷涛赛文阅读 904评论 1 2
  • 要分析Handler的原理,首先需要了解Message和Looper,所以我们先来分析一下Message和Loop...
    王小二的王阅读 473评论 0 0
  • 1、数据通信会带来什么开发中的问题?(1)线程间如何进行通信Handler通信实现的方案实际上是内存共享方案。(2...
    Boahui阅读 396评论 0 1