作者:冬日毛毛雨 https:juejin。cnpost7160681836291555365一问:讲讲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和Looper作为k,v存在了ThreadLocalMap,ThreadLocalMap属于当前Thread,也就是说Looper作为桥梁连接了Handler与Looper所在的线程。 可以理解为Looper关联了Handler和当前线程三问:Handler机制有了解过没?跟我说说? 在理解Handler机制前,我们需要先搞懂ThreadLocal。 ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。 想搞懂原理那就得先从源码入手开始分析。我们先从set方法看起: 从上面的代码不难看出,ThreadLocalset赋值的时候首先会获取当前线程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则会直接执行Runnablerun() 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驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。 但这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?通过创建新线程的方式。真正会卡死主线程的操作是在回调方法onCreateonStartonResume等操作时间过长,会导致掉帧,甚至发生ANR,looper。loop本身不会导致应用卡死。六问:主线程的死循环一直运行是不是特别消耗CPU资源呢? 其实不然,这里就涉及到Linuxpipeepoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在Loop的queue。next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。 这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步IO,即读写是阻塞的。所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。 好了,这轮面试中问道的Handler就问了这么多了,大家可以好好的吸收一下