查看原文
其他

字节面试6连问:讲讲 ThreadLocal 与 Handler

AndroidPub 2023-02-21

作者:冬日毛毛雨
https://juejin.cn/post/7160681836291555365

一问:讲讲 ThreadLocal 和 Handler 的关系

竟然提到了 Handler 机制就不得不提到这几大将了:HandlerLooperMessageQueueMessage。延伸重点 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 和 Looper 作为 k,v 存在了 ThreadLocalMap,ThreadLocalMap 属于当前 Thread,也就是说 Looper 作为桥梁连接了 Handler 与 Looper 所在的线程。

可以理解为 Looper 关联了 Handler 和当前线程

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

在理解 Handler 机制前,我们需要先搞懂 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。

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

对 ThreadLocal 做一个总结:每个线程 Thread 自身有一个属性 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 的方法做了特殊处理,如果 msg.callback !=null 则会直接执行 Runnable#run() 

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 就问了这么多了,大家可以好好的吸收一下,如有什么疑议欢迎在留言讨论!!

-- END --

推荐阅读


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存