进阶之路 | 奇妙的Handler之旅
许朋友爱玩 人气:0
## 前言
> 本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍:
>
> [我的GIthub博客](https://lovelifeeveryday.github.io/)
## 需要已经具备的知识:
- `Handler`的基本概念及使用
## 学习导图:
![学习导图](https://cdn.jsdelivr.net/gh/LoveLifeEveryday/FigureBed@master/typora202003/06/000802-69705.png)
## 一.为什么要学习`Handler`?
在`Android`平台上,主要用到的通信机制有两种:`Handler`和`Binder`,前者用于进程内部的通信,后者主要用于跨进程通信。
在多线程的应用场景中,`Handler`**将工作线程中需更新`UI`的操作信息 传递到 `UI`主线程**,从而实现工作线程对`UI`的更新处理,最终实现异步消息的处理。
作为一个`Android`程序猿,**知其然而必须知其所以然**,理解其源码能更好地了解`Handler`机制的原理。下面,我就从消息机制入手,带大家畅游在`Handler`的世界中,体会`Google`工程师的智慧之光。
## 二.核心知识点归纳
### 2.1 消息机制概述
A.作用:**跨线程通信**
B.常用场景:当子线程中进行耗时操作后需要更新`UI`时,通过`Handler`将有关`UI`的操作切换到主线程中执行
> - 系统不建议在子线程访问`UI`的原因:`UI`控件**非线程安全**,在多线程中**并发**访问可能会导致`UI`控件处于不可预期的状态
>
> - 而不对`UI`控件的访问加上**锁**机制的原因有:
>
> 1.上锁会让UI控件变得复杂和低效
>
> 2.上锁后会阻塞某些进程的执行
C.四要素:
- `Message`:需要被传递的消息,其中包含了消息`ID`,消息处理对象以及处理的数据等,由`MessageQueue`统一列队,最终由`Handler`处理
- `MessageQueue`:用来存放`Handler`发送过来的消息,内部通过**单链表**的数据结构来维护消息列表,等待`Looper`的抽取。
- `Handler`:负责`Message`的发送及处理
> - `Handler.sendMessage()`:向消息队列**发送**各种消息事件
> - `Handler.handleMessage() `:**处理**相应的消息事件
- `Looper`:通过`Looper.loop()`不断地从`MessageQueue`中抽取`Message`,按分发机制将消息分发给目标处理者,可以看成是**消息泵**
>`Thread`:负责调度整个消息循环,即消息循环的执行场所
>
>存在关系:
>
>- 一个`Thread`只能有**一**个`Looper`,可以有**多**个`Handler`
>- `Looper`有**一**个`MessageQueue`,可以处理来自**多**个`Handler`的`Message`
>- `MessageQueue`有一组待处理的`Message`,这些`Message`可来自不同的`Handler`
>- `Message`中记录了负责发送和处理消息的`Handler`
>- `Handler`中有`Looper`和`MessageQueue`
![关系图](https://s2.ax1x.com/2020/03/05/37FxUI.png)
![数量关系](https://s2.ax1x.com/2020/03/05/37ke5q.png)
D.使用方法:
- 在`ActivityThread`主线程实例化一个全局的`Handler`对象
- 在需要执行`UI`操作的子线程里实例化一个`Message`并填充必要数据,调用`Handler.sendMessage(Message)`方法发送出去
- 重写`handleMessage()`方法,对不同`Message`执行相关操作
E.总体工作流程:
> 这里先总体地说明一下`Android`消息机制的工作流程,具体的`ThreadLocal`,`MessageQueue`,`Looper`,`Handler`的工作原理会在下文详细解析
- `Handler.sendMessage()`发送消息时,会通过`MessageQueue.enqueueMessage()`向`MessageQueue`中添加一条消息
- 通过`Looper.loop()`开启循环后,不断轮询调用`MessageQueue.next()`
- 调用目标`Handler.dispatchMessage()`去传递消息,目标`Handler`收到消息后调用`Handler.handleMessage()`处理消息
>简单来看,即`Handler`将`Message`发送到`Looper`的成员变量`MessageQueue`中,之后`Looper`不断循环遍历`MessageQueue`从中读取`Message`,最终回调给`Handler`处理。如图:
![总体工作流程](https://s2.ax1x.com/2020/03/05/37kLWV.png)
### 2.2 消息机制分析
#### 2.2.1 `ThreadLocal`
> 了解`ThreadLocal`,有助于我们后面对`Looper`的探究
Q1:`ThreadLocal`**是什么**
首先我们来看一下官方源码(`Android 9.0`)
> This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
大致意思:
>`ThreadLocal`是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,只有在**指定线程**中才能获取到存储的数据(也就是说,每个线程的一个变量,有自己的值)
Q2:`ThreadLocal`的**使用场景**:
- 当某些数据是以线程为作用域且每个**线程**有**特有**的**数据副本**
> - `Android`中具体的使用场景:`Looper`,`ActivityThread`,`AMS`
>
> - 如果不采用`ThreadLocal`的话,需要采取的措施:提供一个全局哈希表
- 复杂逻辑下的对象传递,比如:监听器的传递
> - 采用`ThreadLocal`让监听器作为线程中的全局对象,线程内部只有通过`get`方法即可得到监听器
>
> - 如果不采用`ThreadLocal`的方案:
>
> a.将监听器作为参数传递
>
> 缺点:当调用栈很深的时候,程序设计看起来不美观
>
> b.将监听器作为静态变量
>
> 缺点:状态不具有可扩充性
Q3:**`ThreadLocal`和`synchronized`的区别:**
>- 对于多线程资源共享的问题,`synchronized`机制采用了“**以时间换空间**”的方式
>- 而`ThreadLocal`采用了**“以空间换时间”**的方式
>- 前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响,所以`ThreadLocal`和`synchronized`都能保证线程安全,但是应用场景却大不一样。
Q4:原理
> `ThreadLocal`主要操作为`set`,`get`操作,下面分别介绍流程
A1:`set`的原理
![set流程图](https://cdn.jsdelivr.net/gh/LoveLifeEveryday/FigureBed@master/typora202003/05/155410-152067.png)
A2:`get`的原理
![get流程图](https://cdn.jsdelivr.net/gh/LoveLifeEveryday/FigureBed@master/typora202003/05/155653-993515.png)
综上所述,`ThreadLocal`之所以有这么奇妙的效果,是因为:
- 不同线程访问同一个`ThreadLocal.get()`,其内部会从各种线程中取出`table`数组,然后根据当前`ThreadLocal`的索引查找出对应的`values`值
> 想要了解`ThreadLocal`源码的读者,推荐一篇文章:[ThreadLocal详解](https://www.jianshu.com/p/411c40b09a81)
#### 2.2.2 `MessageQueue`
- 数据结构:`MessageQueue`的数据结构是单链表
- 操作:
A.`enqueueMessage`
主要操作是单链表的插入操作
B.`next`
是一个无限循环的方法,如果没有消息,会一直阻塞;当有消息的时候,`next`会返回消息并将其从单链表中移出
#### 2.2.3 `Looper`
Q1:**`Looper`的作用**
> - 作为消息循环的角色
> - 它会不停地从`MessageQueue`中查看是否有新消息,若有新消息则立即处理,否则一直阻塞(不是`ANR`)
> - `Handler`需要`Looper`,否则将报错
Q2:**`Looper`的使用**
a1:开启:
> `UI`线程会自动创建`Looper`,子线程需自行创建
```java
//子线程中需要自己创建一个Looper
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();//为子线程创建Looper
Handler handler = new Handler();
Looper.loop(); //开启消息轮询
}
}).start();
```
> - 除了`prepare()`,还提供`prepareMainLooper()`,本质也是通过`prepare()`
> - `getMainLooper()` 作用:获取主线程的`Looper`
a2:关闭:
- `quit`:直接退出
- `quitSafely`:设定退出标记,待`MessageQueue`中处理完所有消息再退出
> 退出`Looper`的话,子线程会立刻终止;因此:建议在不需要的时候终止`Looper`
Q3:原理:
![Looper原理](https://s2.ax1x.com/2020/03/06/3bcKLn.png)
#### 2.2.4 `Handler`
Q1:**`Handler`的两种使用方式:**
> **注意**:创建`Handler`实例之前必须先创建`Looper`实例,否则会抛`RuntimeException`(`UI`线程自动创建`Looper`)
- `send`方式
```java
//第一种:send方式的Handler创建
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//如UI操作
}
};
//send
mHandler.sendEmptyMessage(0);
```
- `post`方式
> 最终是通过一系列`send`方法来实现
```java
//实例化Handler
private Handler mHandler = new Handler();
//这里调用了post方法,和sendMessage一样达到了更新UI的目的
mHandler.post(new Runnable() {
@Override
public void run() {
mTextView.setText(new_str);
}
});
```
Q2:**`Handler`处理消息过程**
![Handler发送消息流程](https://cdn.jsdelivr.net/gh/LoveLifeEveryday/FigureBed@master/typora202003/05/203806-192172.png)
![一张图总结Handler](https://s2.ax1x.com/2020/03/05/3HGKI0.png)
### 2.3 `Handler` 的延伸
#### 2.3.1 内存泄露
> 在初学`Handler`的时候,往往会发现`AS`亮起一大块黄色,以警告可能会发生内存泄漏
![Handler警告](https://s2.ax1x.com/2020/03/05/3HYfxS.png)
- 发生场景:`Handler` 允许我们发送**延时消息**,如果在延时期间用户关闭了`Activity`,那么该`Activity`会泄露
- 原因:这个泄露是因为因为 **`Java` 的特性,内部类会持有外部类**, `Handler` 持有 `Activity` 的引用,`Message`持有`Handler`的引用,而`MessageQueue`会持有`Message`的引用,而`MessageQueue`是属于`TLS(ThreadLocalStorage)`线程,是与Activity不同的生命周期。所以当`Activity`的生命周期结束后,而`MessageQueue`中还存在未处理的消息,那么上面一连串的引用链就不允许`Activity`的对象被回收,就造成了内存泄漏
- 解决方式:
A.`Activity`销毁时,清空`Handler`中未执行或正在执行的`Callback`以及`Message`
```java
// 清空消息队列,移除对外部类的引用
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
```
B.**静态内部类+弱引用**
```java
private static class AppHandler extends Handler {
//弱引用,在垃圾回收时,被回收
WeakReference mActivityReference;
AppHandler(Activity activity){
mActivityReference=new WeakReference(activity);
}
public void handleMessage(Message message){
switch (message.what){
HandlerActivity activity=mActivityReference.get();
super.handleMessage(message);
if(activity!=null){
//执行业务逻辑
}
}
}
}
```
![Java各种引用](https://s2.ax1x.com/2020/03/05/3H09de.png)
#### 2.3.2 `Handler`里藏着的`Callback`
首先看下`Handler.dispatchMessage(msg)`
```java
public void dispatchMessage(Message msg) {
//这里的 callback 是 Runnable
if (msg.callback != null) {
handleCallback(msg);
} else {
//如果 callback 处理了该 msg 并且返回 true, 就不会再回调 handleMessage
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
```
可以看到 `Handler.Callback` 有**优先处理消息的权利**
- 当一条消息被 `Callback` 处理**并拦截(返回 `true`)**,那么 `Handler.handleMessage(Msg)` 方法就不会被调用了
- 如果`Callback`处理了消息,但是并没有拦截,那么就意味着**一个消息可以同时被`Callback`以及 `Handler` 处理**
这个就很有意思了,这有什么作用呢?
**我们可以利用 `Callback` 这个拦截机制来拦截 `Handler` 的消息!**
场景:`Hook` [ActivityThread.mH](http://activitythread.mh/) ,笔者在[进阶之路 | 奇妙的四大组件之旅](https://juejin.im/post/5e5c74c1e51d4526dc7be184)介绍过`ActivityThread`,在 `ActivityThread`中有个成员变量 `mH` ,它是个 `Handler`,又是个极其重要的类,几乎所有的**插件化**框架都使用了这个方法
> 限于当前知识水平,笔者尚未研究过插件化的知识,以后有机会的话希望能给大家介绍!
#### 2.3.3 创建 `Message` 的最佳方式
> 为了节省开销,尽量复用 `Message` ,减少内存消耗
法一:`Message msg=Message.obtain();`
法二:`Message msg=handler.obtainMessage();`
#### 2.3.4 妙用 `Looper` 机制
我们可以利用`Looper`的机制来帮助我们做一些事情:
- 将 `Runnable` `post` 到主线程执行
- 利用 `Looper` 判断当前线程是否是主线程
```java
public final class MainThread {
private MainThread() {
}
private static final Handler HANDLER = new Handler(Looper.getMainLooper());
//将 Runnable post 到主线程执行
public static void run(@NonNull Runnable runnable) {
if (isMainThread()) {
runnable.run();
}else{
HANDLER.post(runnable);
}
}
//判断当前线程是否是主线程
public static boolean isMainThread() {
return Looper.myLooper() == Looper.getMainLooper();
}
}
```
#### 2.3.5 `Android`中为什么主线程不会因`Looper.loop()`的死循环卡死?
> 这个是老生常谈的问题了,记得当初被学长问到这个问题的时候,一脸懵逼,然后胡说一通,实属羞愧
>
> 要弄清这个问题,我们可以通过几个问题来逐层深入剖析
Q1:**什么是线程?**
线程是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出
Q2:**进入死循环是不是说明一定会阻塞**?
前面也说到了线程既然是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?**简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出**
想到这就理解,主线程也是一个线程,它也要维持自己的周期,所以也是需要一个死循环的。所以死循环并不是那么让人担心。
Q3:**什么是`Looper`的阻塞?**
- `Looper`的阻塞,前提是没有输入事件,此时`MessageQueue`是空的,`Looper`进入空闲,线程进入阻塞,释放`CPU`,等待输入事件的唤醒
- `Looper`阻塞的时候,主线程大多数时候都是处于休眠状态,并不会消耗大量`CPU`资源
> `Looper`的阻塞涉及到`Linux pipe/epoll`机制,想了解的读者可自行`Google`
Q4:**聊聊`ANR`**
- 其实初学者很容易将`ANR`和`Looper的阻塞`二者相混淆
- `UI`耗时导致卡死,前提是要有输入事件,此时`MessageQueue`不是空的,`Looper`正常轮询,线程并没有阻塞,但是该事件执行时间过长(一般5秒),而且与此期间其他的事件(按键按下,屏幕点击..也是通过`Looper`处理的)都没办法处理(卡死),然后就`ANR`异常了
Q5:**卡死的真正原因:**
- 真正卡死的原因是:在回调方法`onCreate`/`onStart`/`onResume`等操作时间过长
## 三.课堂小测试
> 恭喜你!已经看完了前面的文章,相信你对`Handler`已经有一定深度的了解,下面,进行一下课堂小测试,验证一下自己的学习成果吧!PS:限于篇幅,笔者就不提供答案了,不过答案一搜就有了
Q1:如何将一个`Thread`线程变成`Looper`线程?`Looper`线程有哪些特点
Q2:简述下`Handler`、`Message`、`Looper`的作用,以及他们之间的关系
Q3: 简述消息机制的回调处理过程,怎么保证消息处理机制的唯一性
Q4:为什么发送消息在子线程,而处理消息就变成主线程了,在哪儿跳转的
------
如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力
本文参考链接:
- 《Android 开发艺术探索》
- [ThreadLocal详解](https://www.jianshu.com/p/411c40b09a81)
- [进阶之路 | 奇妙的四大组件之旅](https://juejin.im/post/5e5c74c1e51d4526dc7be184)
- [Handler运行机制中必须明白的几个问题](https://blog.csdn.net/fightingXia/articlehttps://img.qb5200.com/download-x/details/70494818)
- [Handler 都没搞懂,拿什么去跳槽啊?](https://juejin.im/post/5c74b64a6fb9a049be5e22fc#heading-18)
- [Android中为什么主线程不会因为Looper.loop()里的死循环卡死?](https://www.zhihu.com/question/34652589/answer/90344494)
- [为什么主线程不会因为Looper.loop()方法造成阻塞](http://wossoneri.github.io/2019/05/10/[Android]why-Looper-loop-will-not-block-main-thread/)
- [要点提炼|开发艺术之消息机制](https://www.jianshu.com/p/1c79fb5296b6)
- [Android消息机制浅析——面试总结](https://blog.csdn.net/Mr_dsw/articlehttps://img.qb5200.com/download-x/details/51074329)
- [Handler的sendMessage和post的区别](https://www.jianshu.com/p/43d6cd7b06f1)
加载全部内容