0%

(01)2021.12.9-Handler源码分析---Alvin老师(3)

线程的跨越是怎么实现的?

内存是不分线程的,可以在子线程和主线程都使用。

在子线程里面执行某个函数(比如调用 Handler 的 sendMessage 方法),这个函数就在子线程里面

事件变为内存了,MessageQueue.enqueueMessage(msg) 将这一块内存放入了 MessageQueue 了。

Looper.loop 是在主线程中,所以取出来的 Message 是通过 dispatchMessage 放在 主线程执行的

所以上述就是子线程切换到主线程的流程。

问题

有学员问存在以下几个条件:

  1. 所有 Looper 对象(不论主线程还是子线程的)中放 Looper 对象的 ThreadLocal 都是同一个对象

    这个没问题,因为 Looper 类中的 用于存放Looper 对象的 ThreadLocal 是 static final 的,整个 App 只有一个

  2. 那么看起来,所有线程也都公用同一个 Looper 对象了啊

看下 Looper 的源码:

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

所以学员说得没错, sThreadLocal 是整个 App 唯一的。但是,如果我们了解 ThreadLocal 的源码的话,就能得出如下的式子:

Thread1 -> 有自己的 ThreadLocalMap1 -> 存储的键值对样式 <sThreadLocal, Looper1>

Thread2 -> 有自己的 ThreadLocalMap2 -> 存储的键值对样式<sThreadLocal, Looper2>

能看到,在线程1中键值对存在ThreadLocalMap1 这个map 对象中, 样式是 <sThreadLocal, Looper1> ;线程2 中键值对存在 ThreadLocalMap2 这个 map 对象中,的样式是 <sThreadLocal, Looper2>

所以,根本原因在于: 每个线程都有自己的 ThreadLocalMap,虽然所有的线程中的不一样的 Looper 对象都有同一个 ThreadLocal 对象,但是这些线程有不一样的 ThreadLocalMap ,所以用同一个 ThreadLocal 对象作为 key 在不同的 ThreadLocalMap 中取值,取到的肯定是不同的 Looper ,这下明白了。

如何获取 Message

使用 Message.obtain() ,因为 Message 使用完成后,都会回收,所以我们可以一直使用,这是享元设计模式。这样就维持一个池子,避免内存抖动,防止 OOM。new 了必然会回收,回收了就可能有碎片。

在 Looper.loop 方法里面,msg.target.dispathcMessage(msg) 执行完后,在 for(; ;) 死循环最后一句的时候会调用 msg.recycleUnchecked(); 进行回收,将回收的msg 插入到池子的头部

Looper 死循环不会 ANR ?

不是同一个层面的东西。毫不相关的问题。点击 5秒没响应 ANR ,其实这个点击事件它也是一个 Message。

HandlerThread

HandlerThread 存在的意义:

  • 方便使用

  • 线程安全,getLooper 的线程安全问题

wait 会释放锁,这样其他的函数才能获取到锁执行 notify 操作。notify 不会释放锁, 要等 synchronized 代码全部执行完才释放锁(notify操作可能不在synchronized 代码块最后 )。

IntentService

处理后台耗时任务。一个任务分为多个子任务,子任务按照顺序执行完成后,任务才能算完成,这时候可以使用 IntentService ,这样可以保证所有的子任务在同一线程执行。关于要执行的多个任务,代码写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 请求1
Intent i = new Intent("cn.scu.finch");
Bundle bundle = new Bundle();
bundle.putString("taskName", "task1");
i.putExtras(bundle);
startService(i);

// 请求2
Intent i2 = new Intent("cn.scu.finch");
Bundle bundle2 = new Bundle();
bundle2.putString("taskName", "task2");
i2.putExtras(bundle2);
startService(i2);

startService(i); //多次启动

具体可以参考Android多线程:IntentService使用教程(含实例讲解)-腾讯云开发者社区-腾讯云 (tencent.com)

类似的思想也体现在 Fragment 的管理上,:

在 FragmentPagerAdapter 中,会有 instantiateItem 方法。里面会执行 mCurTrasaction.attach(fragment) ,去 attach fragment ,但是这个 attach 方法会立即执行么? 不会的,这时候只会加入list 中,等 commit 的时候才会一起执行。

并且可以保证 attach 一定是在 detach 后面执行,也是用 msg 来实现

具体得自己再去看下源码,老师讲得不详细。

glide 中的巧妙使用

假设有这样一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
fun with(){
val fragmentManager = supportFragmentManager
val f: MyFragment? = fragmentManager.findFragmentByTag("tag") as MyFragment?
if (f == null) {
fragmentManager.beginTransaction()
.add(MyFragment(), "tag")
.commitAllowingStateLoss()

//通过某种途径得到commit 时候用到的 handler
//handler.sendMessage(removeFragmentMessage)
}
}

如果连续调用 2 次,会怎样?可能你会想,第一次已经创建了 MyFragment 实例了,第二次直接通过 findFragmentByTag 就能找出来了。但是不是的,可能会创建 2 次 MyFragment 。这是因为最后在 commitAllowingStateLoss 的时候,最终是将这个操作作为一个 msg 扔到 Handler 里面。所以,第二次调用 with 的时候,第一次不一定执行完!所以,可能执行 2 次。 这个在 glide 的源码中有体现:

  1. 创建一个 HashMap ,以 tag 为key ,第一次的时候,首先从 hashmap中获取,如果没有,再从 fragmentManager 中获取,如果还没有,就创建一个,然后将其加入到 HashMap 中

  2. 第二次的时候,首先判断 HashMap 里面有没有,如果有的话,就忽略,因为这个 Fragment 最终肯定会被创建出来,此时应该是commit 的中提交到 handler 中的 msg 还没执行。

  3. 在上述代码的注释部分,可以看到最终通过handler 发送一个消息,这个消息用于将 fragment 从 HashMap 中移除 Fragment ,防止内存泄漏。

第三点为什么可以这么做呢?因为同一个handler ,commit 的时候发的消息,肯定在前面,后续用于移除 msg 的 msg 肯定在后面,这样很巧妙地保证了用完fragmengt 就从 HashMap 中移除。

上述代码其实就是 glide 中的一段代码设计思想,具体可以参考 Glide生命周期管理 - 简书 (jianshu.com) 中 2.0.1 创建RequestManagerFragment 这个章节。

谢谢你的鼓励