因业务繁忙,有段时刻没有和我们进行技术分享了,今天特此抽出时刻先来分享!!!

今天头条面试题:讲讲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中。

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

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

那是由于looper早在ActivityThread初始化的时分就声明好了,能够直接拿来用的。经过剖析源码咱们知道MessageQueue在Looper中,Looper初始化后作为目标丢给了Handler,而且又存在了 ThreadLocal 里边,ThreadLocal 目标的话作为Key又存在了ThreadLocalMap,ThreadLocalMap目标是Thread里边的一个特点值,也便是说Looper作为桥梁连接了Handler与Looper所在的线程。

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

在了解Handler 机制前,咱们需求先搞懂ThreadLocal。

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

ThreadLocal的原理

想搞懂原理那就得先从源码下手开端剖析。

咱们先从set办法看起:

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

从上面的代码不难看出,ThreadLocal set赋值的时分首先会获取当时线程thread,并获取thread线程中的ThreadLocalMap特点。假如map中特点不为空,则直接更新value值,假如map中找不到此ThreadLocal目标,则在threadLocalMap创立一个,并将value值初始化。明显ThreadLocal目标存的值是根据线程走的!

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

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

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

每个Thread有一个特点,类型是ThreadLocalMap,从代码不难看出ThreadLocalMap是ThreadLocal的内部静态类。它是与线程所绑定联络在一起的,能够看成一个线程只要一个ThreadLocalMap,知道这一点咱们再往下看。

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

ThreadLocalMap的构成主要是用Entry来保存数据 ,而且仍是继承的弱引证。在Entry内部运用ThreadLocal目标作为key,运用咱们设置的value作为value。

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

get的话就比较简略了便是获取当时线程的ThreadLocalMap特点值,在获取Map中对应ThreadLocal目标的value值并返回。

ThreadLocal总结一下便是:每个线程Thread自身有一个特点ThreadLocalMap,然后ThreadLocalMap是一个键值对,它的key值是ThreadLocal目标,它的value值则是咱们想要保存处理的数据值。getMap是找到对应线程的ThreadLocalMap特点值,然后经过判别能够初始化或许更新数值。

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

ThreadLocal剖析完了咱们接着来看Handler吧。

由于主线程在ActivityThread的main办法中已经创立了Looper,所以主线程运用Handler时能够直接new;子线程运用Handler时需求调用Looper的prepare和loop办法才能进行运用,否则会抛出异常。所以咱们从Looper的prepare来剖析。

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

Looper 供给了Looper.prepare()办法来创立 Looper ,而且会借助 ThreadLocal 来完成与当时线程的绑定功能。Looper.loop()则会开端不断尝试从 MessageQueue 中获取 Message , 并分发给对应的 Handler,也便是说 Handler 跟线程的关联是靠 Looper 来完成的。

Looper.loop()担任对音讯的分发,也是和prepare配套运用的办法,两者缺一不可。

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

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

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

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

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

处理剖析完了,咱们看个简略点的,音讯发送吧。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 就问了这么多了,我们能够好好的吸收一下,如有什么疑议欢迎在评论区进行评论!!!

最终

我归纳了以往面试中被问到的一些面试题进行了收拾,收拾成了下面这《Android 进阶面试集锦》,还有一些Android 开发所需求用到的一些学习文档供我们参阅,中心包括Framework +性能优化+Compose+Gradle +音视频+车载开发等。有需求的能够 直接点击此处↓↓↓进行参阅学习!

有需求的能够复制下方链接,传送直达!!!
https://qr21.cn/CaZQLo?BIZ=ECOMMERCE

经过一些真刀真枪地实战,能够帮助你迅速地调整面试的状况,记住,你的终究目标是你心仪的公司,不要被中心的一两个失利的战役影响到自己,找出问题所在,为下一个面试做好准备。