Android 面试中常问到的那些 Handler 面试题
Posted 冬天的毛毛雨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 面试中常问到的那些 Handler 面试题相关的知识,希望对你有一定的参考价值。
Handler通常都会面被问到这几个问题
- 1.一个线程有几个Handler?
- 2.一个线程有几个Looper?如何保证?
- 3.Handler内存泄漏原因?
- 4.子线程中可以new Handler吗?
- 5.子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?有什么用?主线程呢?
- 6.既然可以存在多个Handler往MessageQueue中添加数据(发消息时各个Handler可能处于不同线程),那它内部是如何确保线程安全的?取消息呢?
- 7.我们使用Message时应该如何创建它
Handler的流程
这是我在网上看到的一张图,很形象的体现Handler的工作流程,也说明了Handler几个关键类之间的关系
Handler
只负责将message放到MessageQueue,然后再从MessageQueue取出message发送出去
MessageQueue
就是传送带,上面一直传送的许多message
Looper
就是传送带的轮子,他带动这MessageQueue一直跑动
Thread
就是动力,要是没有线程,整个传送都不会开始,并且Looper还提供了一个开关给Thread,开启才会传送
MessageQueue 和 Message
添加消息
只要你使用handler发送消息,最后都会走到handler#enqueueMessag
然后调用MessageQueue#enqueueMessage
,可以看到方法需要传入一个Message的
handler#enqueueMessage
MessageQueue#enqueueMessage
而且MessageQueue里面还存放了一个mMessage变量,有什么作用呢,让我们先来看一下Message
是什么
Message
就是我们所发送的一个个消息体,而在这个类中
可以看到,一个Message变量里,又存放一个Message叫做next
,存放下一个Message的,这又有啥用呢
再次回到MessageQueue#enqueueMessage
,看一看这些变量到底有什么作用
首先第一个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循环
此时的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
结构就像这样,通过这样的赋值操作,这样就形成了一个链表结构
所以MessageQueue就相当于是一个仓库,里面存放着由许许多多的Message组成的链条
取消息
取消息的方法是MessageQueue#next(
)方法,里面的代码先不做分析,
我们知道发送消息是handler调用的
那么取消息是谁调用的呢
根据一开始的图很容易知道,是Loop#loop()
调用了该方法
而在这个方法拿到msg后 会调用 msg.target.dispatchMessage(msg)
将消息发送出去,这里的msg.target 就是 handler
所以他们形成了这样一种模式,一种生产者消费者模型
也就是说要调用Looper.loop()才会取出消息去分发,但是我们再主线程的时候,都是直接使用Handler,是哪里帮我们调用了Looper.loop()函数呢,直接看到主线程的main函数就能看到,也就是说app一启动,主线程就帮我们调用了Looper.loop()函数
知道流程后,回到一开始的问题
1.一个线程有几个Handler?
这个问题其实不用说都知道,难道主线程不能使用多个Handler吗
2.一个线程有几个Looper?如何保证?
答案很简单,一个线程只有一个Looper,但是怎么保证的呢?
我们先来看看Looper是怎么创建的,是谁创建的
可以看到,Looper的构造函数只在prepare
这里使用过,而且系统也有提示我们,
但是Looper存放在了sThreadLocal变量中,所以先看看sThreadLocal是什么
查阅到就是Looper中的一个静态变量的ThreadLocal类,好像看不出什么
那就进入sThreadLocal.set(Looper)
方法看一下
-
1.可以看到set方法中,首先获取了当前线程,则prepare() --> set() --> 当前线程
也就是说,Thread1
调用prepare方法,获取的当前线程也就是Thread1
,不可能为其他线程。 -
2.然后
通过getMap(当前线程)获得ThreadLocalMap
,也就是说Thead和ThreadLocalMap有关系。也可以看到Thread中有ThreadLocalMap的变量
-
3.最后将
this(当前ThreadLocal)
与传入的Looper保存在ThreadLocalMap中 -
4.ThreadLocalMap就是一个保存<key,value>键值对的
所以看一下 Thread,ThreadLocalMap,ThreadLocal,Looper的关系
所以这里保证了一个Thread对应一个ThreadLocalMap,而ThreadLocalMap又保存这该Thread的ThreadLocal。问题来了<key,vaule>中key是唯一的,但是value是可以代替的,怎么能做到<ThreadLocal,Looper>保存之后Looper不会被代替呢
再回到prepare
函数,可以看到在new Looper之前,还有一个get()
操作
get函数做了一个操作,就是查看当前Thread对应的ThreadLocal,在ThreadLocalMap有没有值,有值则在prepare抛出异常
也就是说,prepare在一个线程中,只能够调用一次,也就保证了Looper只能生成一次,也就是唯一的
3.Handler内存泄漏原因?
我们知道,handler不能作为内部类存在,不然有可能会导致内存泄漏。为什么其他内部类不会呢?
通过java语法我们知道:匿名内部类持有外部类的对象
比如这个,handler是持有HandlerActivity的,不然也不能够调用到其中的方法,而系统是直接帮我们省略了HandlerActivity.this部分的
这就表示**Handler ---持有--> this.Activity --持有--> Activity的一切内容 = 大量内存
**
首先我们知道,一个message是通过handler发送的,然后MessageQueue会保存
也就是说 MessageQueue ---持有--> message
接着我们再看看handler#enqueueMessage
,我认为红框就是造成内存泄漏的最主要原因,我们通过代码可以看到 message.traget = this
这就意味着 message ---持有--> Handler对象
将三条链路拼接在一起 MessageQueue ---持有--> message ---持有--> Handler对象 ---持有--> this.Activity --持有--> Activity的一切内容 = 大量内存
当Handler发送了一个延迟10s的message。但是5s的时候,Activity销毁了。
此时的message是没有人处理的,即使他已经从MessageQueue扔出去了,但是Activity销毁了没人接收,也就是说这个message一只存在,则上面的这条链路是一只存在的。所以这持有的大量内存一直没人处理,虚拟机也会认为你这块内存是被持有的,他不会回收,就这样造成了内存泄漏。
所以说,Handler的内存泄漏,是说是因为匿名内部类是不够全面的
4.子线程中可以new Handler吗?
答案是可以的。
主线程和子线程都是线程,凭啥子线程不行呢,而且看了这么多代码也没看到什么地方必须要做主线程执行的方法。
下面用一段代码演示一下怎么在子线程创建Handler
首先要自定义自己的线程,在线程中创建出自己的Looper
然后再将子线程的Looper传给Handler,这样创建的Handler就是子线程的了
但是这样写会有问题吗,显然是有的
我们知道子线程是异步的,而在子线程生成和获取Looper,你怎么知道他什么时候能创建好,怎么知道在Handler创建时,Looper是有值的呢?这一下变成了线程同步问题了,很简单,线程同步就加锁呗。实际上,系统已经写好了一个能在子线程创建Handler的 HandlerThread
可以看到总体还是和我们自己写的差不多的,不过在自己获取Looper和暴露给外界获取Looper加上了锁
也就是说,如果我们在looper还没创建出来时调用getLooper会执行wait(),释放锁且等待
直到run方法拿到锁之后,获取到Looper后去notiftAll()唤醒他
这样就能保证在Handler创建时,Looper是一定有的
5.子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?有什么用?主线程呢?
我们知道Looper会帮我们在MessageQueue里面取消息,当MessageQueue没有消息了,Looper会做什么呢
首先看到获取消息的next()
方法,他会调用到native层的方法nativePollOnce,当nativePollOnce取不到消息时,他就会让线程等待
所以此时的Looper.loop()方法中,系统也提示我们,会在这里阻塞住
而Looper.loop()是在子线程的run中运行的,要是一直没消息,他就会一直阻塞,run方法一直没办法结束,线程也没办法释放,就会造成内存泄露了
所以Looper给我们提供了一个方法quitSafely
,而他会调用到MessageQueue的方法
他会让mQuitting = true;
,接着清除message,接着nativeWake
, 这与nativePollOnce
是一对的,他会唤醒nativePollOnce继续执行
所以quitSafely
后,next()方法会继续,因为msg = null,mQuitting = true,导致next()直接返回 null
然后再看调用next()方法的Looper.loop(),msg为null后直接return,for循环退出,loop方法也结束了。这样线程也能得到释放了
6.既然可以存在多个Handler往MessageQueue中添加数据(发消息时各个Handler可能处于不同线程),那它内部是如何确保线程安全的?取消息呢?
我们知道Looper创建时,会创建一个MessageQueue,且是唯一对应的
这也就说明一个Thread,Looper,MessageQueue都是唯一对应的关系
那么在添加消息时,synchronized (this)
的this 就是MessageQueue,而根据对应关系,这里加锁,其实就等于锁住了当前线程。就一个线程内算多个Handler同时添加消息,他们也会被锁限制,从而保证了消息添加的有序性,取消息同理
7.我们使用Message时应该如何创建它
不知道你们有没有人使用new Message()
去创建消息。虽然是可以的,但是如果疯狂的new Message,你每new一个,就占用一块内存,会占用大量的内存和内存碎片
系统也提供了新建Message的方法,发现还是new Message(),那又有什么不同呢。
不同的就是sPool,他也是一个Message变量
我们回到Looper,没处理完一个消息后,他会调用Message的方法
而这个方法就是将当前的Message的所有参数清空,变成一个空的Message对象,然后放到sPool中去。等你一下需要Message变量时,他就可以重复里面
想要了解更多 Android 底层知识点,如Handler、Binder 等相关知识点,大家可以直接点击下方卡片进行访问查阅。
以上是关于Android 面试中常问到的那些 Handler 面试题的主要内容,如果未能解决你的问题,请参考以下文章