[TOC]
## 什么是 Handler?
Handler 是 Android 的一种消息处理机制,与 Looper,MessageQueue 绑定,可以用来进行线程的切换。常用于接收子线程发送的数据并在主线程中更新 UI
## Handler线程通信的原理
> 你刚说 Handler 可以切换线程,它是怎么实现的?
“切换线程”其实是“线程通信”的一种。为了保证主线程不被阻塞,我们常常需要在子线程执行一些耗时任务,执行完毕后通知主线程作出相应的反应,这个过程就是线程间通信。
Linux 里有一种进程间通信的方式叫消息队列,简单来说当两个进程想要通信时,一个进程将消息放入队列中,另一个进程从这个队列中读取消息,从而实现两个进程的通信。
![](https://img.kancloud.cn/3e/74/3e74c814cf7f372c13e568036bd90bf7_463x346.png)
Handler 就是基于这一设计而实现的。在 Android 的多线程中,每个线程都有一个自己的消息队列,线程可以开启一个死循环不断地从队列中读取消息。
当 B 线程要和 A 线程通信时,只需要往 A 的消息队列中发送消息,A 的事件循环就会读取这一消息从而实现线程间通信
![](https://img.kancloud.cn/2e/62/2e62c2579d6aaf9dfd52c5054af7aaee_628x448.png)
### 事件循环和消息队列的实现(Looper MessageQueue)
Android 的事件循环和消息队列是通过 Looper 类来实现的
Looper.prepare() 是一个静态方法。它会构建出一个 Looper,同时创建一个 MessageQueue 作为 Looper 的成员变量。MessageQueue 是存放消息的队列
当调用 Looper.loop() 方法时,会在线程内部开启一个死循环,不断地从 MessageQueue 中读取消息,这就是事件循环
每个 Handler 都与一个 Looper 绑定,Looper 包含 MessageQueue
![](https://img.kancloud.cn/ee/46/ee46cdf633f39c882e4cbf16d6d74963_328x283.png)
### Looper 被存放在ThreadLocal
Looper 是存放在线程中的。但如何把 Looper 存放在线程中就引入了 Android 消息机制的另一个重点 --- **ThreadLocal**
前面我们提到。Looper.prepare() 方法会创建出一个 Looper,它其实还做了一件事,就是将 Looper 放入线程的局部变量 ThreadLocal 中。
~~~
// Looper.java#private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
// sThreadLocal是一个静态对象,类型是ThreadLocal<Looper>
sThreadLocal.set(new Looper(quitAllowed));
}
~~~
### 什么是 ThreadLocal
ThreadLocal 又称线程的局部变量。它最大的神奇之处在于,**一个 ThreadLocal 实例在不同的线程中调用 get 方法可以取出不同的值。** 用一个例子来表示这种用法:
~~~
public void set(T value) {
// ① 获取当前线程对象
Thread t = Thread.currentThread();
// ② 获取线程的成员属性map
ThreadLocalMap map = getMap(t);
// ③ 将value放入map中,如果map为空则创建map
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
~~~
**ThreadLocal.set 可以将一个实例变成线程的成员变量**
因为 Looper 要放在线程中的,每个线程只需要一个事件循环,只需要一个 Looper。事件循环是个死循环,多余的事件循环毫无意义。ThreadLocal.set 可以将 Looper 设置为线程的成员变量
同时为了方便在不同线程中获取到 Looper,Android 提供了一个静态对象 Looper.sThreadLocal。这样在线程内部调用 sThreadLocal.get 就可以获取线程对应的 Looper 对象
综上所述,使用 ThreadLocal 作为 Looper 的设置和获取工具是十分方便合理哒
## Looper 的这个死循环会一直“空转”
当然不会!如果事件循环中没有消息要处理但仍然执行循环,相当于无意义的浪费 CPU 资源!Android 是不允许这样的
为了解决这个问题,在 MessageQueue 中,有两个 native 方法,`nativePollOnce` 和 `nativeWake`。
nativePollOnce 表示进行一次轮询,来查找是否有可以处理的消息,如果没有就阻塞线程,让出 CPU 资源
nativeWake 表示唤醒线程
所以这两个方法的调用时机也就显而易见了
~~~
// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
···
if (needWake) {
nativeWake(mPtr);
}
···
}
~~~
在 MessageQueue 类中,`enqueueMessage` 方法用来将消息入队,如果此时线程是阻塞的,调用 `nativeWake` 唤醒线程
~~~
// MessageQueue.java
Message next() {
···
nativePollOnce(ptr, nextPollTimeoutMillis);
···
}
复制代码
~~~
`next()` 方法用来取出消息。取之前调用 `nativePollOnce()` 查询是否有可以处理的消息,如果没有则阻塞线程。等待消息入队时唤醒。
### nativePollOnce 与 nativeWake
在linux新的内核中使用了epoll来替换它,相比于select,epoll最大的好处在于它不会随着监听文件描述符数目的增长而效率降低,select机制是采用轮询来处理的,轮询的fd数目越多,效率也就越低。epoll的接口非常简单就只有三个函数:
int epoll_create(int size);创建一个epoll句柄,当这个句柄创建完成之后,在/proc/进程id/fd中可以看到这个fd。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);注册事件函数。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);等待事件的发生,参数timeout是超时时间毫秒值,0会立即返回,-1将不确定,也就是说有可能永久阻塞。该函数返回需要处理的事件数目,如返回0表示已超时。
### Looper 是个死循环,为什么不会导致 ANR
首先要明确一下概念。**ANR 是应用在特定时间内无法响应一个事件时抛出的异常。**
典型例子的是在主线程中执行耗时任务。当一个触摸事件来临时,主线程忙于处理耗时任务而无法在 5s 内响应触摸事件,此时就会抛出 ANR。
但 Looper 死循环是事件循环的基石,本身就是 Android 用来处理一个个事件的。正常情况下,触摸事件会加入到这个循环中被处理。但如果前一个事件太过耗时,下一个事件等待时间太长超出特定时间,这时才会产生 ANR。所以 Looper 死循环并不是产生 ANR 的原因。
## 消息队列中的消息是如何进行排序
这个就要看 MessageQueue 的 enqueueMessage 方法了
enqueueMessage 是消息的入队方法。Handler 在进行线程间通信时,会调用 sendMessage 将消息发送到接收消息的线程的消息队列中,消息队列调用 enqueueMessage 将消息入队。
~~~
// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
// ① when是消息入队的时间
msg.when = when;
// ② mMessages是链表的头指针,p是哨兵指针
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
// ③ 遍历链表,比较when找到插入位置
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
// ④ 将msg插入到链表中
msg.next = p;
prev.next = msg;
}
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
~~~
消息入队分为 3 步:
① 将入队的时间绑定在 when 属性上
② 遍历链表,通过比较 when 找到插入位置
③ 将 msg 插入到链表中
这就是消息的排序方式
## 异步消息和同步屏障
在 Android 的消息机制中,消息分为**同步消息**、**异步消息**和**同步屏障**三种。(没错,同步屏障是 target 属性为 null 的特殊消息)。通常我们调用 sendMessage 方法发送的是同步消息。异步消息需要和同步屏障配合使用,来提升消息的优先级。
同步屏障理解起来其实很简单。刚才说同步屏障是一种特殊的消息,当事件循环检测到同步屏障时,之后的行为不再像之前那样根据 when 的值一个个取消息,而是遍历整个消息队列,查找到异步消息取出并执行。
这个特殊的消息在消息队列中像一个标志,事件循环探测到它时就改变原来的行为,转而去查找异步消息。表现上看起来像一个屏障一样拦住了同步消息。所以形象地称为同步屏障。
源码实现非常非常简单:
~~~
//MessageQueue.java
Message next() {
···
// ① target为null表明是同步屏障
if (msg != null && msg.target == null) {
// ② 取出异步消息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
···
}
~~~
### 同步屏障,不移除,会发生什么事呢?
同步屏障是用来“拦住”同步消息,处理异步消息的。如果同步屏障不移除,消息队列里的异步消息会一个一个被取出处理,直到异步消息被取完。如果此时队列中没有异步消息了,则线程会阻塞,队列中的同步消息永远不会执行。所以同步屏障要及时移除。
### 那你知道同步屏障的应用场景
同步屏障的核心作用是提高消息优先级,保证 Message 被优先处理。Android 为了避免卡顿,应用在了 view 绘制中。具体可以看之前关于 view 绘制的总结~
## Handler相关的内存泄漏问题
内存泄漏归根到底其实是生命周期“错位”导致的:**一个对象本来应该在一个短的生命周期中被回收,结果被一个长生命周期的对象引用,导致无法回收。** Handler 的内存泄漏其实是内部类持有外部类引用导致的。 形成方式有两种:
(1)匿名内部类持有外部类引用
~~~
class Activity {
var a = 10
fun postRunnable() {
Handler(Looper.getMainLooper()).post(object : Runnable {
override fun run() {
this@Activity.a = 20
}
})
}
}
~~~
Handler 在发送消息时,message.target 属性就是 handler 本身。message 被发送到消息队列中,被线程持有,线程是一个无比“长”生命周期的对象,导致 activity 无法被及时回收从而引起内存泄漏。
解决办法是在 activity destory 时及时移除 runnable
(2)非静态内部类持有外部类引用
~~~
//非静态内部类
protected class AppHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
}
}
}
~~~
解决方案是用静态内部类,并将外部引用改为弱引用
~~~
private static class AppHandler extends Handler {
//弱引用,在垃圾回收时,被回收
WeakReference<Activity> activity;
AppHandler(Activity activity){
this.activity = new WeakReference<Activity>(activity);
}
public void handleMessage(Message message){
switch (message.what){
}
}
}
~~~
## HandlerThread
HandlerThread 顾名思义就是 Handler+Thread 的结合体,它本质上是一个 Thread。
我们知道,子线程是需要我们通过 Looper.prepare()和 Looper.loop()手动开启事件循环的。HandlerThread 其实就帮我们做了这件事,它是一个实现了事件循环的线程。我们可以在这个线程中做一些 IO 耗时操作。
## IdleHandler
IdleHandler 虽然叫 Handler,其实和同步屏障一样是一种特殊的”消息"。不同于 Message,它是一个接口
~~~
public static interface IdleHandler{
boolean queueIdle();
}
复制代码
~~~
Idle 是空闲的意思。与同步屏障不同,同步屏障是提高异步消息的优先级使其优先执行,IdleHandler 是事件循环出现空闲的时候来执行。
这里的“空闲”主要指两种情况
(1)消息队列为空
(2)消息队列不为空但全部是延时消息,也就是 msg.when > now
利用这一特性,我们可以将一些不重要的初始化操作放在 IdleHandler 中执行,以此加快 app 启动速度;由于 View 的绘制是事件驱动的,我们也可以在主线程的事件循环中添加一个 IdleHandler 来作为 View 绘制完成的回调,等等。 但应该注意的是,如果主线程中一直有任务执行,IdleHandler 被执行的时机会无限延后,使用的时候要注意哦~
## 参考资料
[【面试官爸爸】继续给我讲讲Handler?](https://juejin.cn/post/6995341995015143432)
- Android
- 四大组件
- Activity
- Fragment
- Service
- 序列化
- Handler
- Hander介绍
- MessageQueue详细
- 启动流程
- 系统启动流程
- 应用启动流程
- Activity启动流程
- View
- view绘制
- view事件传递
- choreographer
- LayoutInflater
- UI渲染概念
- Binder
- Binder原理
- Binder最大数据
- Binder小结
- Android组件
- ListView原理
- RecyclerView原理
- SharePreferences
- AsyncTask
- Sqlite
- SQLCipher加密
- 迁移与修复
- Sqlite内核
- Sqlite优化v2
- sqlite索引
- sqlite之wal
- sqlite之锁机制
- 网络
- 基础
- TCP
- HTTP
- HTTP1.1
- HTTP2.0
- HTTPS
- HTTP3.0
- HTTP进化图
- HTTP小结
- 实践
- 网络优化
- Json
- ProtoBuffer
- 断点续传
- 性能
- 卡顿
- 卡顿监控
- ANR
- ANR监控
- 内存
- 内存问题与优化
- 图片内存优化
- 线下内存监控
- 线上内存监控
- 启动优化
- 死锁监控
- 崩溃监控
- 包体积优化
- UI渲染优化
- UI常规优化
- I/O监控
- 电量监控
- 第三方框架
- 网络框架
- Volley
- Okhttp
- 网络框架n问
- OkHttp原理N问
- 设计模式
- EventBus
- Rxjava
- 图片
- ImageWoker
- Gilde的优化
- APT
- 依赖注入
- APT
- ARouter
- ButterKnife
- MMKV
- Jetpack
- 协程
- MVI
- Startup
- DataBinder
- 黑科技
- hook
- 运行期Java-hook技术
- 编译期hook
- ASM
- Transform增量编译
- 运行期Native-hook技术
- 热修复
- 插件化
- AAB
- Shadow
- 虚拟机
- 其他
- UI自动化
- JavaParser
- Android Line
- 编译
- 疑难杂症
- Android11滑动异常
- 方案
- 工业化
- 模块化
- 隐私合规
- 动态化
- 项目管理
- 业务启动优化
- 业务架构设计
- 性能优化case
- 性能优化-排查思路
- 性能优化-现有方案
- 登录
- 搜索
- C++
- NDK入门
- 跨平台
- H5
- Flutter
- Flutter 性能优化
- 数据跨平台