这种设计思想很巧妙,首先,容器是各自线程对象的成员变量,也就是数据其实就是交由各自线程维护,那么不同线程即使调用了同一 ThreadLocal 对象的同一方法,取的数据也是各自线程的数据副本,这样自然就可以达到维护不同线程各自相互独立的数据副本,且以线程为作用域的效果了。
同时,在将数据存储到各自容器中是以当前 ThreadLocal 对象实例为 key 存储,这样,即使在同一线程中调用了不同的 ThreadLocal 对象的 get() 方法,所获取到的数据也是不同的,达到同一线程中不同 ThreadLocal 虽然共用一个容器,但却可以相互独立运作的效果。
(特别佩服 Google 工程师!)
set()get() 方法我们已经梳理完了,其实到这里,ThreadLocal 的原理基本上算是理清了,而且有一点,梳理到现在,其实 ThreadLocal 该如何使用我们也可以猜测出来了。
你问我为什么可以猜测出来了?
忘了我们上面梳理的 get() 方法了么,内部会一直先去取线程的容器,然后再从容器中取最后的值,取不到就会一直返回初始值,会有哪种应用场景是需要一直返回初始值的么?肯定没有,既然如此,就要保证在容器中可以取到值,那么,自然就是要先 set() 将数据存到容器中,get() 的时候才会有值啊。
所以,用法很简单,实例化 ThreadLocal 对象后,直接调用 set() 存值,调用 get() 取值,两个方法内部会自动根据当前线程选择相对应的容器存取。
我们来看看 set() 是不是这样:
//ThreadLocal#set() public void set(T value) { //1. 取当前线程对象 Thread t = Thread.currentThread(); //2. 取当前线程的数据存储容器 ThreadLocalMap map = getMap(t); if (map != null) //3. 以当前ThreadLocal实例对象为key,存值 map.set(this, value); else //4. 新建个当前线程的数据存储容器,并以当前ThreadLocal实例对象为key,存值 createMap(t, value); }是吧,set() 方法里都是调用已经分析过的方法了,那么就不继续分析了,注释里也写得很详细了。
那么,最后来回答下开头的两个问题:
Q1:在不同线程中调用 Looper.myLooper() 为什么可以返回各自线程的 Looper 对象呢?明明我们没有传入任何线程信息,内部是如何找到当前线程对应的 Looper 对象呢?
A:因为 Looper.myLooper() 内部其实是调用了 ThreadLocal 的 get() 方法,ThreadLocal 内部会自己去获取当前线程的成员变量 threadLocals,该变量作用是线程自己的数据存储容器,作用域自然也就仅限线程而已,以此来实现可以自动根据不同线程返回各自线程的 Looper 对象。
毕竟,数据本来就只是存在各自线程中,自然互不影响,ThreadLocal 只是内部自动先去获取当前线程对象,再去取对象的数据存储容器,最后取值返回而已。
但取值之前要先存值,而在 Looper 类中,对 ThreadLocal 的 set() 方法调用只有一个地方: prepare(),该方法只有主线程系统已经帮忙调用了。这其实也就是说,主线程的 Looper 消息循环机制是默认开启的,其他线程默认关闭,如果想要使用,则需要自己手动调用,不调用的话,线程的 Looper 对象一直为空。
Q2:ThreadLocal 是如何做到同一个对象,却维护着不同线程的数据副本呢?
A:梳理清楚,其实好像也不是很难,是吧。无外乎就是将数据保存在各自的线程中,这样不同线程的数据自然相互不影响。然后存值时再以当前 ThreadLocal 实例对象为 key,这样即使同一线程中,不同 ThreadLocal 虽然使用同一个容器,但 key 不一样,取值时也就不会相互影响。
小彩蛋说是小彩蛋,其实是 Android 的一个小 bug,尽管这个 bug 并不会有任何影响,但发现了 Google 工程师居然也写了 bug,就异常的兴奋有没有。
另外,先说明下,该 bug 并不是我发现的,我以前在写一篇博客分析 View.post 源码时,期间有个问题卡住,然后阅读其他大神的文章时发现他提了这点,bug 是他发现并不是由我发现,只是刚好,我看的源码版本比他的新,然后发现在我看的源码版本上,这个 bug 居然被修复了,那么也就是说, Google 的这一点行为也就表示这确实是一个 bug,所以异常兴奋,特别佩服那个大神。
是这样的,不清楚 View.post() 流程原理的可以先去我那篇博客过过,不过也么事,我简单来说下:
通过 View.post(Runnable action) 传进来的 Runnable,如果此时 View 还没 attachToWindow,那么这个 Runnable 是会先被缓存起来,直到 View 被 attachToWindow 时才取出来执行。
而在版本 android-24 之前,缓存是交由 ViewRootImpl 来做的,如下:
//View#post() public boolean post(Runnable action) { //1. mAttachInfo 是当 View 被 attachToWindow 时才会被赋值 final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.post(action); } //2. 所以,如果 View 还没被 attachToWindow 时,这些 Runnable 会先被缓存起来 ViewRootImpl.getRunQueue().post(action); return true; }