Handler-ThreadLocal分析

ThreadLocal 源码分析

在 Android 的 Handler 机制下,ThreadLocal 提供了对不同线程本地变量的副本的保存,并且实现了线程数据的隔离,不同线程的数据不会产生错乱。且在一个线程结束后,其对应在 ThreadLocal 内的数据会被释放,除非有其他地方对这部分数据的引用还存在。

基本介绍

ThreadLocal 提供了线程本地变量保存的功能。线程本地变量的修改由 ThreadLocalset() 实现,读取由 ThreadLocalget() 方法实现。ThreadLocal 实例通常会被定义成 static 字段,这些字段与一个线程的状态关联 ( 例如,用户ID 或 业务ID ) 。

依据 app 启动过程,主线程下 ThreadLocal 的使用进行对应的分析。

Android 主线程 ThreadLocal 的使用

在 Android 主线程中,ThreadLocal 对象被 Looper 所持有,在 Looper 类中被定义 static 字段。

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

在 app 进程开始执行时,先执行 ActivityThread.main() 方法:

// ActivityThread.java

public static void main(String[] args) {
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
    // ...
    Looper.prepareMainLooper(); // 主线程下,准备main looper。
    // ...
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
    // ...
    Looper.loop(); // looper开始循环

    throw new RuntimeException("Main thread loop unexpectedly exited");
}

主线程中,通过调用 Looper.prepareMainLooper() 创建并初始化 LooperThreadLocal 对象。调用到 Looper.prepareMainLooper() 方法:

// Looper.java

// sThreadLocal.get() 若是在 prepare() 方法调用之前返回,会返回 null。
@UnsupportedAppUsage
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

@UnsupportedAppUsage
private static Looper sMainLooper;  // 这个对象是static的,即它的生命周期跟app一致。

public static void prepare() {
    prepare(true);
}

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));  // 设置当前线程与looper关联
}

// 主线程的 main looper 由系统环境创建,因此不需要在 app 程序中调用。
public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}

// 返回与当前线程关联的 Looper 对象,若当前线程还没有关联的 Looper 对象,则返回 null。
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

在一个线程中,只能创建一个 Looper 对象,否则对应线程就发生crash,包含提示信息 "Only one Looper may be created per thread"。下面时序图是 app 进程中调用 prepareMainLooper() 的调用过程。

在这里插入图片描述

总结起来也就简单几部:

  1. 判断当前线程是否已经有与之关联的 Looper 对象了,若有则抛出运行时错误 ( throw new RuntimeException("Only one Looper may be created per thread");)。
  2. 通过 sThreadLocal.set() 将新建的 Looper 对象与当前线程关联起来。
  3. 判断 sMainLooper 若是不为 null,抛出状态异常 (throw new IllegalStateException("The main Looper has already been prepared."))。
  4. sThreadLocal 中获取与当前线程关联的 Looper 对象,作为主线程 Looper 赋给 sMainLooper

主线程的 looper 创建之后,即 sThreadLocal 中保存有 sMainLooper 引用的对象之后。looper 开始进行 loop() 操作 (后面专门讲 Looper)。

下面 TreadLocal 最常用的方法 set() get() 方法分析。

set()

在 app进程运行时,会创建 main looper,并将其与主线程关联。也就是调用 sThreadLocal.set(new Looper(quitAllowed)) 的过程。

set()方法的定义:

// ThreadLocal.java

private final int threadLocalHashCode = nextHashCode();

// 这个是 static 的,在线程中创建 ThreadLocal 并调用 set() 方法, nextHashCode 的值在每次调用后增长,
// 并被用于计算 Entry 在表中的索引位置值。
private static AtomicInteger nextHashCode = new AtomicInteger();

// hash值增量,每次增加后,与 0x0F(即15) 作且运算,每次计算产生的索引值不同,
// 在计算第16次时,索引值重新开始,与第一次计算的循环值相同。
private static final int HASH_INCREMENT = 0x61c88647;

// 用于增长并非原来的值
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

// 将线程局部变量的当前线程福报设置给特定值。
public void set(T value) {
    Thread t = Thread.currentThread();  // 获取当前线程
    ThreadLocalMap map = getMap(t);  // 首次调用的时候,getMap(t)方法返回null。
    if (map != null)
        map.set(this, value); // 当 ThreadLocalMap 已经创建,则直接调用 set 方法。
    else
        createMap(t, value);  // 首次调用 getMap(t) 返回Null,因此会执行到这里,创建 ThreadLocalMap 对象。
}

// 获取当前线程的线程本地变量map。
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 获取当前线程的本地变量表。
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 创建当前线程的 ThreadLocalMap 对象。
void createMap(Thread t, T firstValue) {  // 这里主线程中,传入的 firstValue 是 Looper 对象。
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

// ThreadLocal 内部的 ThreadLocalMap,实际 保存线程本地变量的 实现。
static class ThreadLocalMap {
 	private static final int INITIAL_CAPACITY = 16;
    private Entry[] table;  // 封装了 (ThreadLocal, Looper) 对的结构类。
    private int size = 0;   // 存储有值的 entry 表大小。
    
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

    // 创建保存 (firstKey, firstValue) 键值对的 ThreadLocalMaps 对象,其中 firstKey 总是 ThreadLocal。
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 这个方式产生存放 Entry 的索引值。
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY); // 当 table 数组满时需要扩展表的大小。
    }

}

每次 首次 通过 当前线程对象 调用 getMap(t) 方法尝试获取线程本地变量 map 结果,该方法都会返回 null,因此会调用到 createMap(t, value); 若已经创建了,则调用到 ThreadLocalMap.set(ThreadLocal<?> key, Object value)

// ThreadLocalMap in ThreadLocal.java

// 快速计算下一个索引位置。
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

private void set(ThreadLocal<?> key, Object value) { // 主线程中,value 的值是 Looper 对象。

    Entry[] tab = table;
    int len = tab.length;  // 一般就是数组大小的 16,或扩展后的容量大小。
    int i = key.threadLocalHashCode & (len-1);

    // 在 Entry[] 中查找, 若搜索到了相同的 ThreadLocal 的 Entry,直接更新对应的 value。
    for (Entry e = tab[i];
         e != null;
         // 查找下一个搜索位置。
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) { // 搜索到了相同的 ThreadLocal 的 Entry,直接更新对应的 value。
            e.value = value;
            return;
        }

        if (k == null) {  // Entry 的 ThreadLcoal<?> 是 null,替换 Entry 对象。
            // 在当前运行中清除所有过期条目。
            // 主要目的是在插入新元素或者清除过期元素时,清除哈希表中的过期条目,以减少哈希表的大小并提高效率。
            replaceStaleEntry(key, value, i); 
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

这段代码是 ThreadLocalset(T) 方法的真正实现,在设置过程中,清理掉 ThreadLocalMap 中的过期条目,过期的判断条件就是 Entry 对象是否被 GC 回收了,即 e.get() == null

get()

get()方法定义:

// 返回当前线程局部变量表的副本。若表中没有值,则返回 setInitialValue() 方法中设置的初始值。
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

static class ThreadLocalMap {
    // 通过 hashcode 计算 Entry 对象的索引位置。 并获取对应值,若被GC回收了,则调用到 getEntryAfterMiss()。
    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key)
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }
    
    // 在直接计算得到的索引位置处无法找到对应的 Entry 时,计算下一个位置,尝试搜索。
    // 若 e.get() == null,表示该条目过期,会对该索引位置 Entry 清理。
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;

        while (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == key)
                return e;
            if (k == null)
                expungeStaleEntry(i);
            else
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }
}

上述的 get() set() 方法是 ThreadLocal 中最常用的两个方法。对于 ThreadLocal 的最常用分析先到这里。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

VoidHope

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值