进阶之路 | 奇妙的Handler之旅
前言
本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍:
本文已授权公众号
顾林海
发布:https://mp.weixin.qq.com/s/HatMCXW-ErkPb4GWkMoBvA
需要已经具备的知识:
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详解
2.2.2 MessageQueue
数据结构:
MessageQueue
的数据结构是单链表操作:
A.
enqueueMessage
主要操作是单链表的插入操作
B.
next
是一个无限循环的方法,如果没有消息,会一直阻塞;当有消息的时候,
next
会返回消息并将其从单链表中移出
2.2.3 Looper
Q1:Looper
的作用
- 作为消息循环的角色
- 它会不停地从
MessageQueue
中查看是否有新消息,若有新消息则立即处理,否则一直阻塞(不是ANR
)Handler
需要Looper
,否则将报错Handler
内部通过ThreadLocal
获取到当前线程的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
的特性,内部类会持有外部类,Activity
被Handler
持有引用,Handler
被Message
持有引用,而Message
被MessageQueue
持有引用,而MessageQueue
是属于TLS(ThreadLocalStorage)
线程,是与Activity不同的生命周期。所以当Activity
的生命周期结束后,而MessageQueue
中还存在未处理的消息,那么上面一连串的引用链就不允许Activity
的对象被回收,就造成了内存泄漏
即两个关键条件:
- 存在
Activity
-->Handler
-->Message
-->MessageQueue
的一连串引用链Handler
的生命周期 > 外部类的生命周期
解决方式:
A.
Activity
销毁时,清空Handler
中未执行或正在执行的Callback
以及Message
// 清空消息队列,移除对外部类的引用
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
B.静态内部类+弱引用
- 为了保证不再持有当前
Activity
的引用,我们采用静态内部类
的方式- 为了让
Handler
在处理消息时调用外部类Activity
的方法,且能在GC
时回收其内存(换句话说:有短暂的生命周期),所以我们这里采用弱引用
的方式
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 ,笔者在进阶之路 | 奇妙的四大组件之旅介绍过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
机制,想了解的读者可自行
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详解
- 进阶之路 | 奇妙的四大组件之旅
- Handler运行机制中必须明白的几个问题
- Handler 都没搞懂,拿什么去跳槽啊?
- Android中为什么主线程不会因为Looper.loop()里的死循环卡死?
- 为什么主线程不会因为Looper.loop()方法造成阻塞
- 要点提炼|开发艺术之消息机制
- Android消息机制浅析——面试总结
- Handler的sendMessage和post的区别
进阶之路 | 奇妙的Handler之旅的更多相关文章
- 进阶之路 | 奇妙的Thread之旅
前言 本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍: 我的GIthub博客 需要已经具备的知识: Thread的基本概念及使用 AsyncTask的基本概念及使用 学习清单: 线程概述 ...
- 进阶之路 | 奇妙的Window之旅
前言 本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍: 我的GIthub博客 学习清单: Window&WindowManagerService Window&Window ...
- 进阶之路 | 奇妙的View之旅
前言 本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍: 我的GIthub博客 学习清单: View是什么 View的位置参数 View的触控 View的滑动 涉及以下各个知识点: View ...
- 进阶之路 | 奇妙的Animation之旅
前言 本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍: 我的GIthub博客 学习清单: 动画的种类 自定义View动画 View动画的特殊使用场景 属性动画 使用动画的注意事项 一.为什 ...
- 进阶之路 | 奇妙的IPC之旅
前言 本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍: 我的GIthub博客 学习清单: IPC的基础概念 多进程和多线程的概念 Android中的序列化机制和Binder Android ...
- 进阶之路 | 奇妙的Activity之旅
前言 本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍: 我的GIthub博客 本篇文章需要已经具备的知识: Activity的基本概念 AndroidManifest.xml的基本概念 学 ...
- 进阶之路 | 奇妙的Drawable之旅
前言 本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍: 我的GIthub博客 学习清单: Drawable简介 Drawable分类 自定义Drawable 一.为什么要学习Drawabl ...
- Android研发进阶之路
前言 移动研发火热不停,越来越多人开始学习android开发.但很多人感觉入门容易成长很难,对未来比较迷茫,不知道自己技能该怎么提升,到达下一阶段需要补充哪些内容.市面上也多是谈论知识图谱,缺少体系和 ...
- GO语言的进阶之路-初探GO语言
GO语言的进阶之路-初探GO语言 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.为什么我们需要一门新语言 Go语言官方自称,之所以开发Go 语言,是因为“近10年来开发程序之难 ...
随机推荐
- 2018 ACM-ICPC 宁夏 H.Fight Against Monsters(贪心)
It is my great honour to introduce myself to you here. My name is Aloysius Benjy Cobweb Dartagnan Eg ...
- sql的书写顺序
例:select t.* from (select * from t_user where isDelete = 1 limit 0,10) t order by t.qq select from ...
- BeagleboneBlack上u-boot的MLO文件是哪里来的
在玩BeagleboneBlack一段时间之后不可避免地接触到了u-boot,之前的玩耍过程大致上是这样的: 在MATLAB下耍,因为MATLAB提供了它的硬件支持,可以直接在命令行与之交互,也可在s ...
- BZOJ 2318: Spoj4060 game with probability Problem (概率dp)(博弈论)
2318: Spoj4060 game with probability Problem Description Alice和Bob在玩一个游戏.有n个石子在这里,Alice和Bob轮流投掷硬币,如果 ...
- VisualStudioAddin2016Setup.rar
本工具是用于Visual Studio 2010 /2012 的外接程序. 功能不太多,常用代码,引用管理等. 动态图: 下载地址: VisualStudioAddin2016Setup.rar
- Linux系统安装及配置ftp服务
1. 先用rpm -qa| grep vsftpd命令检查是否已经安装,如果ftp没有安装,使用yum -y install vsftpd 安装,(ubuntu 下使用apt-get instal ...
- 在服务器搭建git服务器
服务端操作 安装Git及创建用户组.用户 123 yum install gitgroupadd gitadduser git -g git 禁止用户git登录 修改/etc/passwd文件: 找到 ...
- iOS路由详解
本文如题,路由详解,注定是一篇详细解释iOS路由原理及使用的文章,由于此时正在外地出差,无法详细一一写出,只能不定时的补充. 一.什么是iOS路由 路由一词来源于路由器,可以实现层级之间消息转发的功能 ...
- 非参数检验|Sign test|Wilcoxon signed rank test|Wilcoxon rank sum test|Bootstrapping
非参数检验条件没有参数,因此就没有分布,利用数据等级之间的差距,依次赋值之后再用参数方法测试.将连续型变量转化为离散型变量,即顺序变量.与参数检验相比,正态分布较弱(p值有可能不显著,浪费信息,比如最 ...
- Doc: NetBeans
NetBeans的最新版本已经更新为Apache NetBeans. 安装JDK 在Mac OS X下,有".dmg"的安装包,可以直接安装.只要JDK的版本大于1.8.0就可以安 ...