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 面试题的主要内容,如果未能解决你的问题,请参考以下文章

总结!Java面试中常被问到的几大技术难题

干货 | Java面试中常被问到的几大技术难题

求职季 | Java面试中常被问到的几大技术难题

面试中常被问到的(24)网络分层及协议

面试中常被问到的(12)函数调用执行过程

面试中常被问到的(19)常见几种锁