一、前言
我们知道在Android中经常需要在不同的线程中进行消息传送。当线程A需要线程B来处理数据操作时,在后者处理完成后如何能将结果通知到线程A?这便引出消息机制的概念。其原理是通过一个MessageQueue消息队列来存储其他线程发送的消息,通过本线程的Looper轮询器不断地取消息队列中的消息并通知给当前线程。接下来我们将进入消息机制源码层去探索它的奥秘。
在阅读本篇文章之前,首先建议读者预览一下本文的目录结构,因为本人编写的思路是将消息机制的三大知识点进行分离单独讲解,所以了解清文章的结构能够帮助读者更好的理解本文的知识内容
二、关键知识及流程
本文主要涉及的内容有ThreadLocal、Looper、MessageQueue、Handler、消息屏障、闲置时间处理等等。在分析源码前先来了解一下消息机制的工作流程。请看图
1、消息机制流程
- 线程A中调用
Looper.prepare()
初始化Looper - 创建Handler对象并重写
handleMessage()
用于处理消息,这里有个优先顺序,在创建Handler之前必须先要初始化Looper,否则会抛出异常 - 调用
Looper.loop()
启动循环,对应图for(;;)
部分,之后Looper会不断地从Messagequeue队列中读取消息 - Messagequeue队列存放中以单链表的结构存放着一个个的message消息,mMessage代表着消息队列的头消息
- 当队列中有消息时,Looper会取出队列中的头消息,假设队列中不存在异步消息,简单引入一点:消息屏障.当有异步消息时会跳过所有的同步消息首先取出异步消息来执行,我们后文中会详细介绍,这里作简单代入
- Looper成功取出消息后,回调
msg.target.dispatchMessage(msg)
(target值为发送此条消息的handler对象),将取出的消息发送给handler - handler的
dispatchMessage()
中,调用handleMessage(msg)
执行最终的消息消费 - 在图中线程B充当这消息队列生产者的角色,在某一时间段线程B执行
mhandler.sendMessage(msg)
时,会将消息添加到消息队列中,最后有looper取出
大体流程基本就是这样,更多的细节我会在接下来的文章中详细介绍.
注意: 这里需要注意一点,收发消息的线程如果是非主线程,需要手动来创建一个Looper轮询,其次调用Looper.loop()来启动轮询.这里可能会有初学者会问了,在平常操作中经常仅重写Handler#handleMessage(),并未创建过仍可以使用。没错,的确是这样的,大多数情况下,handler的使用是在主线程中进行的,因为handler的主要目的就是为了接收消息,更新UI,同学们都知道UI的更新只能在主线程中进行,所以handler一般是被定义到主线程中执行的,而主线程中默认已经存在一个mainLooper(),也就是说其实源码层已经帮我们做了Looper的初始化操作,所以我们才可以直接使用它.
既然讲到了主线程的UI更新,那就再宽泛的说一下为什么UI的更新只能在主线程中?作为本文的一点小零食知识分享吧
2、主线程更新UI的原因
如果在子线程中也可以更新UI,那会是一个什么样的画面?我们来设想一下这种场景:
假如在主线程中创建出两个子线程用于网络数据请求,如果两个子线程处理数据后同时刷新UI,画面可能会在前一秒界面数据还是一,后一秒界面数据就发生了变化,这仅仅是两个线程,若存在多个线程,那么界面的显示将变得不可控.那加入同步锁呢?加锁本来就是一个很重的操作,同步锁只会是的界面的更新变得低效、复杂。总之若子线程可以访问UI,那么对于UI的刷新将会变得难以控制,界面闪动或延迟。
UI刷新并不是线程安全的,所以为了线程能够稳定展示,提高效率与安全性,规定只能统一在主线程更新UI。在ViewRootImpl中有这么样一个方法
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
// 只有创建视图结构的原始线程才能更新他的视图,而创建此视图的正是主线程
}
}
checkThread()
方法会检查当前线程是否与mThread线程相等,而mThread在构造方法中赋值,创建视图的正是主线程,凡是更新视图的行为如setText()
、setWidth()
、setHeight()
都会执行此方法检查线程.所以只能在主线程刷新UI。为什么有时候在子线程中执行setText()时不会报错呢?那是不是代表部分耗时短,轻量的绘制操作可以在主线程进行呢?NO,你可以在调用此方法前延迟一小会时间,照样报不允许子线程更新UI的异常,这种情况是因为此方法的调用早于ViewRootImpl的实例,逃过了它的checkThread()
,所以没有异常,原则上还是不允许的
扯了点题外话。我们回到正轨
// 我们以非主线程下使用消息机制,来分析它的原理
new Thread(() -> {
Looper.prepare();
Handler handlers = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
Message message = new Message();
System.out.println(message.what);
return false;
}
});
Looper.loop();
}, "线程A");
三、Looper
首先我们分析Looper的初始化,Looper相当于一个运输车,它的工作就是在消息队列中有消息的前提下,不断地从队列中拿到消息,运输到Handler中,交给Handler消费
1、Looper初始化
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
调用Looper.prepare()
后会执行到这里,quitAllowed
参数代表循环器是否是可退出的,默认为true.在主线程中创建时为false不可退出,意味着looper会随着进程的销毁而结束,无法通过中间过程停止轮询,大家可能都知道Android主线程其实就维护着一个大的Looper,消息的更新、事件点击等处理都是在这个大的消息机制下进行的,在ActivityThread内的main()方法中会执行Looper.prepareMainLooper()
创建属于主线程的Looper,参数为false,与贴出的方法基本相同,只不过是将判断条件换为了if (sMainLooper != null)
这里出现了一个变量sThreadLocal,顺着代码,我们来看ThreadLocal
1.1、ThreadLoacal
ThreadLocal可以被认为是一个本地变量管理器,其主要功能是为某线程创建一个"本地变量副本"并存储到此线程中,此值只有该线程可以使用,其他线程无法得到,实现了线程间的互不干涉,数据隔离问题.
ThreadLocal中有两个方法:
1.2 ThreadLocal#set()存储本地变量
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//当线程首次调用set()时,Thread中的threadLocals还未初始化,所以首先创建实例,再将value值存储为线程本地副本
//之后每次存储时,便会直接将数据存储到此map中
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap为ThreadLocal的内部类,由Thread使用,主要是将value存储的当前的线程中
1.2 ThreadLocal#get()读取本地变量
//获取线程本地副本数据
public T get() {
Thread t = Thread.currentThread(); //获取调用get()的线程
ThreadLocalMap map = getMap(t); //取到当前线程的threadLocals并将对应数据返回
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
//ThreadLocalMap的构造函数
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
//firstKey为调用set()方法的ThreadLocal对象,这里对该值进行hash计算被作为数组的索引
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//以当前的ThreadLocal对象为key,将数据存储
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
根据ThreadLocalMap的构造函数可以看出其内部创建了一个Entry数组,大小默认为16,而每个所谓本地副本变量其实就是一个个Entry对象以key : value形式存储到当前线程中.如果你还是没有理解它的原理,请不要着急,将它运用到消息机制中,自然而然地你就明白了
回到Looper.prepare()
中,因为该方法是在线程A下执行的,首先会将线程A中的threadLocals 进行初始化
t.threadLocals = new ThreadLocalMap(this, firstValue);
value是一个可中断Looper对象(quitAllowed=true),也就为线程A绑定了一个Looper循环器,若当前线程的的threadLocals不为null,意味着之前已经有过一次looper的存储,那么抛出异常,可以看出每个线程只能对Looper进行一次初始化
2、核心工作looper()方法
我们为什么要称Looper是一个循环器呢?通过looper()方法的分析将为您揭晓答案
public static void loop() {
final Looper me = myLooper();
if (me == null) { //当前线程没有初始化Looper,返回null
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next(); // 从消息队列中读取消息
if (msg == null) { //当queue的next返回null时,looper就正式结束了
return;
}
try {
....
msg.target.dispatchMessage(msg);
//Message的target值是发送此消息的Handler,回调这个Handler对象dispatchMessage()最后将执行handleMessage(),我们稍后分析
} catch (Exception exception) {
....
} finally {
....
}
上述代码中我们可以看出looper使用一个for的无条件循环,不断地执行queue.next()
方法获取消息.我们分析下一行msg判null?如果为null直接退出循环?要知道主线程维护的Looper循环也会执行这个方法,一旦队列返回null,将意味着结束.那么我们就需要好奇messageQueue中是如何管理消息的,那么随后我们将揭开MessageQueue的面纱
3、Looper退出quit()和quitSafely()
Looper的退出共有两个方法,其内部调用的都是一个函数 mQueue.quit(),区别在于
- quilt():
mQueue.quit(false)
,传递false,将会删除所有的消息,包括延迟消息,非延迟消息,官方解释意思不建议使用 - quitSafely():
mQueue.quit(true)
,见名知意,安全退出,但是此方法操作比较能接受,将所有及时(到该执行时间段)的消息全部执行完毕后才会退出,同时删除所有未到时间的延迟消息
执行quit()将不会再接收新的消息。OK,到此Looper这一小节就分析完成了,初始化、轮询、退出作了简单的分析,作为一个搬运工,他的工作也是比较地简单,其实具体的消息管理工作都是在MessageQueue中执行的,所以我们具体来看看MessageQueue都做了哪些工作
四、MessageQueue
MessageQueue在Looper实例化时进行的初始化,本小节我们主要分析MessageQueue的构造方法、enqueueMessage()
、next()
以及quit()
四个方法,工作分别是初始化参数、插入消息、得到消息、退出队列。通过分析这些方法我们便能深刻理解它的原理
1、MessageQueue的初始化
MessageQueue的初始化是在Looper的构造方法中执行的,构造方法包含一个boolean类型的形参
MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}
- quitAllowed: 表示是否可以退出,比如
Looper.prepareMainLooper()
的实参传递的为false,即不可退出,而平时使用中常常是允许退出 - mPtr: 一个long类型变量,用于标记当前阻塞队列的状态,当值为0时表示队列退出状态
2、消息插入enqueueMessage()
当调用handler.sendxxx(msg)时候执行的方法,将其他线程要发送的消息添加到消息队列当中
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
/*发送消息时,msg必须要有handler的引用,当然后面也有msg.target为空
的情况,此消息作为一个标识时,target为null,但是添加标识有单独的方
法,叫做消息屏障,我们之后会讲解到*/
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) { //消息正在被使用 异常
throw new IllegalStateException(msg + " This message is already in use.");
}
synchronized (this) {
if (mQuitting) { //队列退出时,mQuitting为true
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
msg.recycle(); //消息回收
return false; //消息添加失败
}
msg.markInUse(); // 更新消息状态
msg.when = when; //需要延时多久执行此消息 延时的时间
Message p = mMessages; //mMessages为当前消息头,因为队列中的消息是以单链表形式存储,mMessages为头结点
boolean needWake;
//是否需要唤醒当前线程,当队列中无消息处于空闲状态时,
//会进行阻塞以节省CPU资源,当有消息进来时,唤醒沉睡的线程执行消息
//p==null只有第一次添加时头结点才会为null
//when == 0 表示没有延迟时间,需要即刻执行的消息
//when < p.when如果队头有延时时间且大于当前添加的消息,那么当前消息作为头结点
if (p == null || when == 0 || when < p.when) {
//满足上述其中一个条件 更换新消息为队头
msg.next = p;
mMessages = msg;
needWake = mBlocked; //mBlocked为当前的队列状态,true表示阻塞状态,需要被唤醒
} else {//都不满足,说明此消息是一个延时消息,且比p的延时时间长
//后两个条件可以看做一个整体,上面说道p.target为null的情况也存在,当调用postSyncBarrier()时会向队列中追加一个消息屏障标识
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) { //此处循环的目的在于将当前消息插入到队列正确的位置
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) { //如果当前线程需要被唤醒,但是当前为异步消息时,取消唤醒
needWake = false;
}
}
// 找到应该存放的位置后,将消息插入队列中
msg.next = p;
prev.next = msg;
}
if (needWake) { //是否需要唤醒线程
nativeWake(mPtr); //唤醒 并更新队列状态值
}
}
return true;
}
消息填加方法做了一下几点:
- 被加入msg为即时消息或比头结点延时短,放到头结点
- 根据消息延时消息的时长由短到长的顺序进行连接,按时间延时长短将消息插入链表对应位置
- 队列处于阻塞状态,在需要的时候唤醒它
- 在发送消息结束后,会回收msg对象,我们在平时使用过程中也应该注意个这个细节
3、消息屏障
消息机制中可分为三种消息:普通消息、异步消息、同步屏障
同步屏障可以被看作一个标识,作用是拦截普通消息而使得异步消息的优先执行。通过postSyncBarrier()可以向队列中插入一个消息屏障
public int postSyncBarrier() {
//currentTimeMillis() 获取时间为自1970-1-01 到此刻
// uptimeMillis()为电脑开机到此刻,更加准确,有兴趣单独了解
return postSyncBarrier(SystemClock.uptimeMillis());
}
// when代表该屏障消息延迟时间
private int postSyncBarrier(long when) {
synchronized (this) {
final int token = mNextBarrierToken++;
//注意此消息是新创建的 且没有给msg.target赋值
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
//同样是按时间顺序插入队列中
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) {
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
//最终返回的token为该消息屏障的唯一标识,便于使用后删除
return token;
}
}
我们应该知道Android中生命周期、动画、绘制等都是msg消息,当向主线程发送一条UI绘制消息时,将会被插入到MessageQueue中,此时需要被处理的消息可能有很多,使得绘制消息不能最快处理而导致界面卡顿.为了保证绘制消息能够优先得到执行,同步屏障机制可以跳过这些普通消息优先执行绘制UI的消息.同步屏障就是屏蔽同步消息去执行异步消息的一种机制,所以叫同步屏障.
4、读取消息next()
final long ptr = mPtr;
if (ptr == 0) { //队列退出时ptr会等于0
return null;
}
int nextPollTimeoutMillis = 0; //消息延迟时间
for (;;) {
//更新线程状态,nextPollTimeoutMillis有3个值分别决定当前线程的状态
//-1 阻塞线程,直到有消息时被唤醒
//0 不阻塞
//>0 阻塞时间为该值,队列中仅有延时消息时,nextPollTimeoutMillis=msg.when
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages; //msg默认为队头消息
// 消息屏障的处理,凡target==null为消息屏障
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
//只要遇到异步消息,结束循环,即取到最近一条的异步消息
} while (msg != null && !msg.isAsynchronous());
}
//若有消息屏障,此时的msg会指向异步消息
if (now < msg.when) { //msg是当前需要被执行的消息,有延迟则阻塞剩余等待时间
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
//mBlocked表示当前线程阻塞情况 else为消息需要立即执行
mBlocked = false;
if (prevMsg != null) { //当msg处于链表中间部分时,执行msg后要连接该节点的前后节点
prevMsg.next = msg.next;
} else {
//消息头指向下一消息
mMessages = msg.next;
}
msg.next = null;
msg.markInUse(); //最后返回当前消息
return msg;
}
} else {// 若mMessages 返回null,队列中无队列,阻塞线程
nextPollTimeoutMillis = -1;
}
if (mQuitting) { //关闭队列
dispose();
return null;
}
//....
}
}
- mMessages 为头消息,如果是消息屏障,变量msg会寻找到最近的异步消息返回,并连接上下节点
- 若为普通消息,返回消息头,其mMessages 指向链表下一个msg节点
- 若msg消息不为null且是延时消息,线程睡眠
- 若msg消息为null,阻塞线程
- 分析looper时我们说过queue队列返回null,就意味着轮询结束,从next()看得出,返回null的情况很少,仅在队列退出状态中,会返回null
next()源码比较长只贴了部分,关于方法后半段是对消息队列的空闲时间的利用,客户端通过添加IdleHandler并重写queueIdle()来利用这些空闲时间执行一些相关操作,在queueIdle()返回false时回调一次会被移除,否则mIdleHandlers中下次空闲继续回调,具体可翻看源码详细了解,这里作简单介绍
5、队列退出quit(
void quit(boolean safe) {
if (!mQuitAllowed) { //主线程不允许退出
throw new IllegalStateException("Main thread not allowed to quit.");
}
synchronized (this) {
if (mQuitting) { //队列正在退出,则无需操作
return;
}
mQuitting = true;
if (safe) { //方法参数我们前面讲过,true为安全退出,仅删除when大于当前时刻的消息,即在执行完所有到时间的消息后,退出
removeAllFutureMessagesLocked();
} else {//非安全退出则将删除所有的消息
removeAllMessagesLocked();
}
//唤醒线程
nativeWake(mPtr);
}
}
looper.quit()
将执行此方法,默认为非安全退出,删除所有的消息,包括到时需要被执行的msg- quit()一旦被调用,外部线程便无法再向此队列发送消息
- 此时next()中的循环还未结束,唤醒线程后,进入下一次循环,非安全情况下,队列中无消息
return null
,looper退出,反之处理剩下消息后退出
6、总结
不难看出,消息队列是消息机制的仓库,负责消息的增删、线程的睡眠唤醒、消息的类型、队列的退出,其中有关消息屏障是比较难理解的,有兴趣的读者可以多研究研究
五、Handler
1、Handler的初始化
为什么说子线程中创建Handler前必须要先进行Looper的初始化呢?通过它的构造方法你就明白了
public Handler(@Nullable Callback callback, boolean async) {
mLooper = Looper.myLooper(); //return sThreadLocal.get(); 获取当前线程Looper对象
if (mLooper == null) {
//答案就在这里, 创建Handler时,会判断当前线程下是否有Looper实例,其创建是调用Looper.prepare()添加到ThreadLocal中
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue; //消息队列
mCallback = callback; //消息回调
mAsynchronous = async; //异步消息标记
}
- 每个线程仅有一个Looper且使用消息机制是,必须提前初始化
- handler有多个构造函数,读者可自己翻看,还是比较容易理解的
2、消息回调dispatchMessage()
当looper轮询取到消息后通过msg.target.dispatchMessage(msg);
便回调此方法
public void dispatchMessage(@NonNull Message msg) {
//此方法判断使用哪个回调接口
if (msg.callback != null) { //msg中携带的回调接口
handleCallback(msg);
} else {
if (mCallback != null) { //初始化handler传递的callback回调接口
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg); //回调重写的handleMessage()
}
}
handler有三种回调接口方式:
- msg.callback在发送消息同时携带回调接口,有消息时回调此接口
- callback 在初始化Handler时入参的一个回调接口,有消息则回调该接口handleMessage()
- 若没有传递回调接口,默认回调Handler的handleMessage()方法
3、Handler的消息发送
总得来说,Handler的发送消息有两种发送postxxx()
、sendxxx()
两者最后都是进入到了sendMessageDelayed()
所以区别不是很大
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
而最后的发送消息也非常的简单,若是延迟消息则赋值第二个参数延时时长,计算方式则累加当前系统的时间,反之为0
六、总结
OK,到这里有关Android消息机制我们就讲完,通篇文章讲解了Looper、MessageQueue、Handler的初始化,消息的插入与取出,主线程更新UI的原因(篇幅原因仅做了简单讲解)、ThreadLocal本地变量、以及比较绕脑的消息屏障,看似简单的消息机制其内部也有着你不知道的一面,比如Android主线程对Handler的使用,当然这个信息量是非常庞大的,只能告诉你Android所有的绘制、事件、生命周期等等,都在使用消息机制,本文仅带领读者去分析消息机制源码,更多的应用场景还需读者自己深挖