线程机制还是消息机制
早年间Handler被称为线程间通信机制,现在这个概念已经被否定了,更准确的定义是消息机制。Android应用的启动,生命周期,view绘制,能想到的一切操作都是通过Handler消息机制运转处理的。
Android也是个Java程序 通过 static void main() 函数启动。阅读源码在ActivityThread.main()
函数的代码并不复杂,主要做了三件事
- 创建主线程Looper
Looper.prepareMainLooper();
- 创建ActivityThread 对象 调用 attach方法
ActivityThread thread = new ActivityThread(); thread.attach(false, startSeq);
- 调用
Looper.loop();
开启无限循环,从Message Queue中获取Message消息对象 。
关键在于这段无限循环代码,在线程里开启无限循环使线程一直运行不会退出,但是里面只是在取Message。
那么我们写了那么多代码是跑到哪里了怎么运行的呢?
在初学Java的时候,启动main() 函数,所有的代码都在main()函数中,按顺序执行完毕程序就结束了。按照Java程序的直观感受应该代码都放在main()函数中才对
所以上面的结论该怎么证明呢?
举个例子:view.requestLayout()
这个方法大家很熟悉,通知界面重绘。跟踪代码调用顺序:
view.requestLayout() —ViewRootImpl.requestLayout()—ViewRootImpl.scheduleTraversals()
通过源码可以看出,界面绘制是通过Handler机制实现的。
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//设置同步障碍
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//监听Vsync信号 内部通过Handler发送了异步消息,通知界面重绘
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
在ActivityThread类中,有内部类H 继承Handler,接收广播,创建服务,绑定服务很多逻辑都是在这里实现的。也可以证明Handler是消息机制,整个应用顺利运行都是通过消息驱动的
为什么是消息驱动机制
消息驱动的工作模型大概是:
- 有个消息队列
- 开启无限循环,从队列中消息 进行处理
- 系统想要作什么工作就往消息队列中投递一个消息
大概符合Android的工作机制,不仅仅符合Android,Windows,iOS许多GUI系统都是这样设计的。UI处理只能在主线程中进行,线程内通过消息队列处理任务。
又出现一个新问题:为什么GUI系统都选择了 差不多的单线程消息队列机制
大概就是:多线程处理UI,不仅不会使UI响应的更快还可能因为,线程锁导致的死锁和竞争,引发很多问题。
举个例子:
应用层修改图片的顺序:用户代码—GUI顶层—GUI底层—系统库—系统调用
系统底层发起的键盘或屏幕时间:系统调用—系统库—GUI底层—GUI顶层—用户代码
一个从上到下 ,一个从下到上,顺序完全相反 非常复杂 。设计出多线程GUI系统的麻烦远远大于收益,即便是设计系统的大佬,也觉得头疼,吃力不讨好。所以大部分GUI系统都不约而同的选择了 单线程消息队列机制。
单线程消息队列机制的特点:
Android、Swing、MFC等的GUI库都使用单线程消息队列机制来处理绘制界面、事件响应等消息,在这种设计中,每个待处理的任务都被封装成一个消息添加到消息队列中。消息队列是线程安全的(消息队列自己通过加锁等机制保证消息不会在多线程竞争中丢失),任何线程都可以添加消息到这个队列中,但是只有主线程(UI线程)从中取出消息,并执行消息的响应函数,这就保证了只有主线程才去执行这些操作。
单线程消息队列机制的问题:
单线程消息队列机制存在一个问题:消息响应函数中不能有耗时长的、计算密集型的操作,因为主线程在努力地处理这样的操作的时候就无法去处理其它的积压在消息队列中的绘制消息、事件消息了(一个消息处理完了主线程才会去队列中取下一个消息),这时候就会出现按键无响应、点击无反应的情况。
解决方案:
但这个问题有完美的解决方案,我们可以在消息响应函数中启动另一个工作线程(Worker Thread)来执行耗时操作,这样在线程启动起来后这个消息就算处理完了,主线程可以取下一个消息了,这时候主线程和还未执行完计算任务的工作线程就在操作系统的调度下并驾齐驱地狂奔了(调度算法会保证两个线程并发或并行地执行,不会专宠某个线程)
三篇好文:
(30条消息) GUI为什么不设计为多线程_liuqiaoyu080512的博客-CSDN博客_gui 线程
Android 中为什么需要 Handler? - 高爷的回答 - 知乎
基于消息的事件驱动机制(Message Based, Event Driven)
Handler工作流程
如图所示:
- 每一个线程持有唯一Looper
- Looper 中持有唯一MessageQueue
- MessageQueue中维护Message 单链表优先级队列
- Handler可以在任意位置创建 通过sendMessageI() 方法使消息入队
- Looper通过无限循环从MessageQueue中读取消息
- Message对象中 持有发送它的Handler引用,轮到处理当前消息时,通过这个Handler引用调用handleMessage() 执行消息分发。
Handler线程切换的本质—内存共享
handler经常被拿来做的事情就是切换线程,那么它是怎么实现的呢?
首先 只有函数调用才区分线程,A函数调用在主线程,B函数调用在子线程。 对象是不区分线程的,new一个新对象只是在内存中开辟了一块新的内存空间,任何一个线程都可以操作这块内存。
每一个线程都有一个唯一Looper,Looper持有MessageQueue,MessageQueue管理Message
队列。
Handler 在创建时需要绑定Looper,获取Looper持有的MessageQueue对象。
如果Handler初始化时绑定主线程Looper,即使是子线程调用Handler.sendMessage()
,最终会入队到主线程Looper的MessageQueue中,主线程Looper在分发消息的时候肯定是运行在主线程。
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
消息分发过程:
- Looper.loop() 内部实现无限循环,
- 调用MessageQueue的next()方法 取出Message
- Message内部 声明
target
属性,类型是Handler,在Handler.sendMessage()
赋值。表示哪一个handler发了当前消息 - Looper.loop() 中取出Message后 调用
msg.target.dispatchMessage(msg);
将消息分发给Handler,完成逻辑闭环 - 之前提过 函数调用才区分线程,在主线程looper中处理的消息,函数调用肯定也是在主线程。则完成线程切换
- 内存共享的本质就是,主线程和子线程可以操作同一个对象,子线程可以往主线程Looper发送消息,主线程也可以往子线程Looper发送消息
Looper为什么是唯一的
- ThreadLocal 线程上下文的存储变量,它本身并不存储数据,当调用
threadLocal.set()
时,是将数据添加到ThreadLocalMap
中 - 每个线程内部持有属性
ThreadLocalMap
map结构以key,value 键值对存储数据。key是ThreadLocal
对象,value是需要存储的值。 - Looper中有一个静态常量
ThreadLocal
保存当前looper,在looper构造时保存。static final ThreadLocal<Looper> *sThreadLocal* = new ThreadLocal<Looper>();
- 上面提到过
ThreadLocalMap
key 的类型是ThreadLocal
静态常量保证了key是唯一的 ThreadLocalMap
存储的数据当前线程内共享,因为Map特性,当key相同时,二次赋值,新值会覆盖旧值。 我们的需求Looper线程内唯一,Google在创建Looper时进行限制。添加非空判断,重复创建Looper会抛出运行时异常这样就保证不会出现新值覆盖旧值的情况- Key 是静态常量 唯一的,创建时又有判断 二者叠加保证Looper线程唯一
MessageQueue—单链表优先级队列
MeesageQueue 中持有一个Message对象, 每个Message中又持有一个 名为next 类型是Message对象的属性,以此构成单链表结构
MessageQueue—Message—next—next—...
每个Message在进入队列是会保存执行时间,在Message的when属性中。入队方法MessageQueue.enqueueMessage()
。通过循环新入队的Message会与已在队列中Message比较执行时间,时间越早排在队列的前面
MessageQueue.next()
取数据时,因为消息入队时经过时间排序,排在队首的消息肯定是最先要执行的消息,只需要取第一个就行了。但是会与当前时间进行比较,因为每个消息都在入队时指定了执行时间。
如果没有达到执行时间,队列会调用调用native层提供的方法暂时休眠
同步屏障机制
正常的消息 都是按照链表顺序一个一个执行的,同步屏障机制是可以让某些重要消息先执行。
在MessageQueue.next()
有一段逻辑,通过注释也可以看出。开启同步屏障,在队列中寻找下一个异步消息。
开启同步屏障的条件是msg.target == null
,找到队列中的异步消息。如果不开启同步屏障,异步消息与同步消息并没有什么不同,都会按照顺序执行。
Message msg = mMessages;
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
所以同步屏障机制有两部分构成:异步消息和同步屏障
异步消息的设置有两种方法:
1.设置message的 isAsynchronous属性
val message = handler.obtainMessage()
message.isAsynchronous= true
- 创建异步Handler,发送的消息都是异步的。但是异步Handler构造外部无法调用,提供了静态函数
Handler.createAsync()
sdk28以后才可以调用,约等于没有。
同步屏障本质上是一个 target==null的 Message对象。
但是呢 如果开发人员自行设置 target==null,在消息入队时会抛异常,直接保存。Message Queue提供了 postSyncBarrier(),``removeSyncBarrier()
设置,移除同步屏障的方法,但是不允许外界调用。
开发层用不了,那同步屏障机制什么时候发生做用呢? 答:用于界面重绘
ViewRootImpl.requestLayout()
通知界面界面重绘,但并不是调用requestLayout()方法后,立刻就重绘。而是先添加一个同步屏障,调用postSyncBarrier()
向主线程队列中添加一个msg.target==null的消息。
另一边呢 监听系统底层发出的VSYNC 信号,发送异步消息,执行重绘逻辑,任务完成后移除消息屏障。
同步屏障机制虽然可以使用反射调用,但是没必要呀。何必和页面重绘挤到一起呢,几乎永不到。API没有公开也侧面说明Google并不想让应用层使用。
参考
android Handler架构思考 - 掘金 (juejin.cn)
Android进程框架:线程通信的桥梁Handler - 掘金 (juejin.cn)
Android组件系列:Handler机制详解 - 掘金 (juejin.cn)