前言
本文已经收录到我的 GitHub 个人博客, 欢迎大佬们光临寒舍:
我的 GitHub 博客 https://lovelifeeveryday.github.io/
需要已经具备的知识:
Handler 的基本概念及使用
学习导图:
一. 为什么要学习 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
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 处理. 如图:
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 的原理
A2:get 的原理
综上所述, 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, 子线程需自行创建
- // 子线程中需要自己创建一个 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: 原理:
2.2.4 Handler
Q1:Handler 的两种使用方式:
注意: 创建 Handler 实例之前必须先创建 Looper 实例, 否则会抛 RuntimeException(UI 线程自动创建 Looper)
send 方式
- // 第一种: send 方式的 Handler 创建
- Handler mHandler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- // 如 UI 操作
- }
- };
- //send
- mHandler.sendEmptyMessage(0);
post 方式
最终是通过一系列 send 方法来实现
- // 实例化 Handler
- private Handler mHandler = new Handler();
- // 这里调用了 post 方法, 和 sendMessage 一样达到了更新 UI 的目的
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mTextView.setText(new_str);
- }
- });
Q2:Handler 处理消息过程
2.3 Handler 的延伸
2.3.1 内存泄露
在初学 Handler 的时候, 往往会发现 AS 亮起一大块黄色, 以警告可能会发生内存泄漏
发生场景: Handler 允许我们发送延时消息, 如果在延时期间用户关闭了 Activity, 那么该 Activity 会泄露
原因: 这个泄露是因为因为 Java 的特性, 内部类会持有外部类, Handler 持有 Activity 的引用, Message 持有 Handler 的引用, 而 MessageQueue 会持有 Message 的引用, 而 MessageQueue 是属于 TLS(ThreadLocalStorage)线程, 是与 Activity 不同的生命周期. 所以当 Activity 的生命周期结束后, 而 MessageQueue 中还存在未处理的消息, 那么上面一连串的引用链就不允许 Activity 的对象被回收, 就造成了内存泄漏
解决方式:
A.Activity 销毁时, 清空 Handler 中未执行或正在执行的 Callback 以及 Message
- // 清空消息队列, 移除对外部类的引用
- @Override
- protected void onDestroy() {
- super.onDestroy();
- mHandler.removeCallbacksAndMessages(null);
- }
B. 静态内部类 + 弱引用
- private static class AppHandler extends Handler {
- // 弱引用, 在垃圾回收时, 被回收
- WeakReference<Activity> mActivityReference;
- AppHandler(Activity activity){
- mActivityReference=new WeakReference<Activity>(activity);
- }
- public void handleMessage(Message message){
- switch (message.what){
- HandlerActivity activity=mActivityReference.get();
- super.handleMessage(message);
- if(activity!=null){
- // 执行业务逻辑
- }
- }
- }
- }
2.3.2 Handler 里藏着的 Callback
首先看下 Handler.dispatchMessage(msg)
- 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 判断当前线程是否是主线程
- 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 运行机制中必须明白的几个问题
Handler 都没搞懂, 拿什么去跳槽啊?
Android 中为什么主线程不会因为 Looper.loop()里的死循环卡死?
为什么主线程不会因为 Looper.loop()方法造成阻塞
要点提炼 | 开发艺术之消息机制 https://www.jianshu.com/p/1c79fb5296b6
Android 消息机制浅析 -- 面试总结
Handler 的 sendMessage 和 post 的区别 https://www.jianshu.com/p/43d6cd7b06f1
来源: https://www.cnblogs.com/xcynice/p/qi_miao_de_handler_zhi_lv.html