注册

因面试提到 Handler 机制后,引发连环炮轰(我已承受不来~)

因业务繁忙,有段时间没有和大家进行技术分享了,今日特此抽出时间先来分享!!!

今日头条面试题:讲讲ThreadLocal底层原理和Handler的关系

竟然提到了Handler机制就不得不提到这几大将了:Handler,Looper,MessageQueue,Message。延伸重点ThreadLocal!!!

当UI的主线程在初始化第一个 Handler时,就会通过ThreadLocal创建一个Looper,该Looper与UI主线程一一对应。而使用ThreadLocal的目的是保证每一个线程只创建唯一一个Looper。

Looper初始化的时候会创建一个 消息队列MessageQueue。至此,主线程、消息循环、消息队列之间的关系是1:1:1。

Handler、Looper、MessageQueue的初始化流程如下图所示:Hander持有对UI主线程消息队列MessageQueue和消息循环Looper的引用,子线程可以通过 Handler 将消息发送到UI线程的消息队列MessageQueue中。

二问:主线程为啥不用初始化looper呢?

那是因为looper早在ActivityThread初始化的时候就声明好了,可以直接拿来用的。通过分析源码我们知道MessageQueue在Looper中,Looper初始化后作为对象丢给了Handler,并且又存在了 ThreadLocal 里面,ThreadLocal 对象的话作为Key又存在了ThreadLocalMap,ThreadLocalMap对象是Thread里面的一个属性值,也就是说Looper作为桥梁连接了Handler与Looper所在的线程。

三问:Handler机制有了解过没?跟我说说?

在理解Handler 机制前,我们需要先搞懂ThreadLocal。

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLocal的原理

想搞懂原理那就得先从源码入手开始分析。

我们先从set方法看起:

从上面的代码不难看出,ThreadLocal  set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map中属性不为空,则直接更新value值,如果map中找不到此ThreadLocal对象,则在threadLocalMap创建一个,并将value值初始化。显然ThreadLocal对象存的值是根据线程走的!

那么ThreadLocalMap又是什么呢,还有createMap又是怎么做的,我们继续往下看。

首先第一步我们得要知道这个东西

每个Thread有一个属性,类型是ThreadLocalMap,从代码不难看出ThreadLocalMap是ThreadLocal的内部静态类。它是与线程所绑定联系在一起的,可以看成一个线程只有一个ThreadLocalMap,知道这一点我们再往下看。

ThreadLocalMap的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal对象作为key,使用我们设置的value作为value。

get的话就比较简单了就是获取当前线程的ThreadLocalMap属性值,在获取Map中对应ThreadLocal对象的value值并返回。

 ThreadLocal总结一下就是:每个线程Thread自身有一个属性ThreadLocalMap,然后ThreadLocalMap是一个键值对,它的key值是ThreadLocal对象,它的value值则是我们想要保存处理的数据值。getMap是找到对应线程的ThreadLocalMap属性值,然后通过判断可以初始化或者更新数值。

ThreadLocal分析完了我们接着来看Handler吧。

因为主线程在ActivityThread的main方法中已经创建了Looper,所以主线程使用Handler时可以直接new;子线程使用Handler时需要调用Looper的prepare和loop方法才能进行使用,否则会抛出异常。所以我们从Looper的prepare来分析。

Looper 提供了 Looper.prepare() 方法来创建 Looper ,并且会借助 ThreadLocal 来实现与当前线程的绑定功能。Looper.loop() 则会开始不断尝试从 MessageQueue 中获取 Message , 并分发给对应的 Handler,也就是说 Handler 跟线程的关联是靠 Looper 来实现的。

Looper.loop() 负责对消息的分发,也是和prepare配套使用的方法,两者缺一不可。

msg.target是个啥呢,我们追到Message里面不难发现其实它就是我们发送消息的Handler,这写法是不是很聪明,当从MessageQueen中捞出Message后,我们就能直接调用Handler的dispatchMessage,然后就会走到我们的Handler的handleMessage了。直接上源码:

处理分析完了,我们看个简单点的,消息发送吧。Handler 提供了一些列的方法让我们来发送消息,如 send()系列 post()系列 。不过不管我们调用什么方法,最终都会走到 MessageQueue的enqueueMessage(Message,long) 方法。也就是实现了消息的发送,将Message插入到我们的MessageQueue中。

注意:dispatchMessage() 方法针对 Runnable 的方法做了特殊处理,如果是 ,则会直接执行 Runnable.run() 。(判断依据是上述msg.callback !=null这句)

MessageQueue是个单链表。

MessageQueue里消息按时间排序

MessageQueue的next()是个堵塞方法

总结分析:

Looper.loop() 是个死循环,会不断调用 MessageQueue.next() 获取 Message ,并调用 msg.target.dispatchMessage(msg) 回到了 Handler 来分发消息,以此来完成消息的回调。

四问:Handler什么会出现内存泄漏问题呢?

Handler使用是用来进行线程间通信的,所以新开启的线程是会持有Handler引用的,如果在Activity等中创建Handler,并且是非静态内部类的形式,就有可能造成内存泄漏。

非静态内部类是会隐式持有外部类的引用,所以当其他线程持有了该Handler,线程没有被销毁,则意味着Activity会一直被Handler持有引用而无法导致回收。

MessageQueue中如果存在未处理完的Message,Message的target也是对Activity等的持有引用,也会造成内存泄漏。

解决的办法:

使用静态内部类 + 弱引用的方式: 静态内部类不会持有外部类的的引用,当需要引用外部类相关操作时,可以通过弱引用还获取到外部类相关操作,弱引用是不会造成对象该回收回收不掉的问题,不清楚的可以查阅JAVA的几种引用方式的详细说明。

在外部类对象被销毁时,将MessageQueue中的消息清空。

五问:Looper死循环为什么不会导致应用卡死?

对于线程即是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,例如,binder线程也是采用死循环的方法,通过循环方式不同与Binder驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。但这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?通过创建新线程的方式。真正会卡死主线程的操作是在回调方法onCreate/onStart/onResume等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop本身不会导致应用卡死。

六问:主线程的死循环一直运行是不是特别消耗CPU资源呢?

其实不然,这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在Loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

好了,这轮面试中问道的Handler 就问了这么多了,大家可以好好的吸收一下,如有什么疑议欢迎在评论区进行讨论!!!


作者:冬日毛毛雨
链接:https://juejin.cn/post/7160681836291555365
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册