个人博客:haichenyi.com。感谢关注
1. 目录
前言
??Android程序是一个以消息驱动的程序,页面的跟新,Activity生命周期的变化,点击事件等等都与消息息息相关。
简单总结
??简单的理解Handler发送消息的流程:Handler发送消息(message)到MessageQueue,然后,Looper通过loop()方法循环从MessageQueue里面读取消息。然后,发送给对应的target(Handler)。
我们带着问题来理解这个流程,最后,我们在重新总结一下。辣么问题就来了:
- handler都是一样的,为什么Looper会分Looper.getMainLooper()和普通的Looper?
- handler发送的消息过程是什么样子的?
- Looper怎么读取消息的?
- handler发送消息能发送延时消息,Looper读取到消息之后,怎么确定是立刻发送回去,还是隔多久发送回去?
- 怎么提升消息的优先级?
- 我们项目里面可能会用到的Looper.prepare(),Looper.loop(),这是做什么操作?
??我们结合源码一起来看一下这些问题:
Looper的区别:MainLooper和普通Looper
第一个问题,handler都是一样的,为什么Looper会分Looper.getMainLooper()和普通的Looper?我们都知道
Handler handler = new Handler(Looper.getMainLooper())
通过这个Looper.getMainLooper()方式得到得Handler,可以改变UI,其他的不行,这是为什么呢?我们都知道,UI线程才能改变UI。
ps:app的启动入口是在ActivityThread的main方法。
捡一些 (我看的懂的),呸,是主要的,跟我们聊的这个相关的位置贴出来,源码如下:
public static void main(String[] args) {
...
//loop调用了一个准备MainLooper方法(按照方法的名字意思翻译的)
Looper.prepareMainLooper();
...
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
...
//Looper调用loop()方法进入了循环模式
Looper.loop();
//如果走到这里,那就没有进入循环模式,就抛出异常了,异常字面意思很好理解,主线程的loop意外的退出了
throw new RuntimeException("Main thread loop unexpectedly exited");
}
注释应该写的比较清楚了吧?这里我想说的是,main启动的时候,这个线程就是UI线程,这个是系统给规定的,只有在这个线程里面才能改变UI。
我们再来看看这个Looper.prepareMainLooper()里面做了什么操作
@Deprecated
public static void prepareMainLooper() {
//调用prepare方法,传了一个false的Boolean值
prepare(false);
//锁
synchronized (Looper.class) {
//sMainLooper不等于null,就抛异常
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
//等于null,就把myLooper()方法的返回值赋值给sMainLooper
sMainLooper = myLooper();
}
}
//prepare,一个Boolean类型的参数,看名字意思应该是:是否允许退出
private static void prepare(boolean quitAllowed) {
//sThreadLocal.get()值不等于null,就抛异常
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
//sThreadLocal.get()值等于null,就new一个Looper,放到sThreadLocal中
sThreadLocal.set(new Looper(quitAllowed));
}
//Looper的构造方法,我们此时主线程new的时候传的是false
private Looper(boolean quitAllowed) {
//新建了一个消息队列,MsgQueue,并且把这个boolean传进去了,赋值给mQueue变量
mQueue = new MessageQueue(quitAllowed);
//把当前线程赋值给了mThread变量
mThread = Thread.currentThread();
}
//消息队列的构造方法,Boolean类型的参数,到这里就应该知道了,表示这个线程是否允许退出,true:允许退出。false:不允许退出
MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
//nativeInit():native方法,不知道是啥,应该是一些需要的初始化
mPtr = nativeInit();
}
//sThreadLocal.get()的值返回回去
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
注释都有了,最开始调用的prepare(false)方法,看完上面的注释,我大致串一下。
ps:说一下这个sThreadLocal变量,我也不知道怎么说,反正就是它的类型是:ThreadLocal,它就是一个普通的类,然后泛型是Looper,所以,这个类就是用来放Looper的。大致这么理解
我们再说回这个方法,主要就是,
- 先判断这个变量是不是空的,如果不是空的,就抛异常了,因为Looper是不允许我们自己手动创建的。
- 如果是空,就创建一个Looper,放进sThreadLocal变量里面;
- 然后,创建Looper的时候,顺道就创建了MessageQueue。主线程创建的MessageQueue是不允许主动退出的,如果消息队列退出了,退出app了。
- 并且,Looper的mQueue,mThread也都赋值好了,一个是消息队列,一个是当前线程(这两个变量用的也比较多)。
prepare()方法,到这里就说完了,我们再看剩下的代码,往上面翻一下,再看一下。
ps:sMainLooper变量,类型就是Looper
剩下的代码就是一个锁方法,
- 判断sMainLooper是不是不等于null,如果,不等于null就抛出了异常
- 如果等于null,就把上面创建的looper,赋值给sMainLooper
?我们Looper.getMainLooper()获取的Looper就是这个sMainLooper,也就是我们当前线程(UI线程)的Looper,我们只有绑定了这个looper的handler才能改变UI,因为,这个handler是在给UI线程传递消息。
??为什么不等于null就抛出异常了呢?因为sMainLooper只在系统的时候创建,不能在其他的时候创建,如果,在其他的时候创建,说明系统启动的时候没有创建Looper,那么,主线程就没法通信,这是有问题的。
第一个问题我说明白了吧?Looper.getMainLooper()获取到的是主线程的Looper,跟它绑定的handler能改变UI,没有跟它绑定的hanler都不能改变UI
handler发送的消息过程
第二个问题,handler发送的消息过程是什么样子的?
说到这里,我们先聊一下Message类
public final class Message implements Parcelable {
public int what;
public int arg1;
public int arg2;
public Object obj;
...
public long when;
@UnsupportedAppUsage
/*package*/ Handler target;
...
@UnsupportedAppUsage
/*package*/ Message next;
/** @hide */
public static final Object sPoolSync = new Object();
private static Message sPool;
private static int sPoolSize = 0;
private static final int MAX_POOL_SIZE = 50;
??写代码这么长时间,我们发了那么多消息,是不是都没有仔细看看Message的成员变量?看看上面这几个变量。
- what,arg1,arg2,obj可能我们用的比较多。
- 这个long 类型的when,很重要,是消息放在队列哪个位置的重要依据。是放在队头?还是队尾?(重点)
- Handler类型的target变量,我们之前没注意过吧?字面意思:目标。目标handler(重点)
- 下面还有两个Message类型的变量,一个next,一个sPool;next字面意思:下一条消息。pool:水池。类型又是Message;那么,sPool:池子的消息
- Object类型的sPoolSync:异步池子。根据经验来看,碰到过很多这种Object类型的东西,大部分都是加锁用的。synchronized(sPoolSync),一般都是这样用
- int类型的两个变量,sPoolSize值是0,再就是MAX_POOL_SIZE,值是50。字面的意思就是池子的大小是0,池子的最大值是50.
??什么池子啊,什么最大值啊。我相信很多人跟我的反应都是一样的,线程池,复用。所以这里就是消息池,消息能复用,消息池最大的消息个数是50个,异步。
??延申到这里,引出我想问的第一个问题,消息的创建,消息创建的两种方式:一种是new出来,一种是obtain的方式,它有一系列的重载方法。
//第一种:new的方式
Message msg1 = new Message();
msg1.what = 1;
msg1.arg1 = 20;
handler.sendMessage(msg1);
//第二种:obtain的方式
Message msg = Message.obtain();
msg.what =1;
msg1.arg1 = 20;
handler.sendMessage(msg);
第一种没啥好说的,我们看第二种:Message.obtain()
public static Message obtain() {
//加锁
synchronized (sPoolSync) {
//判断sPool是否为空
if (sPool != null) {
//sPool不为空,就复用sPool msg对象
Message m = sPool;
//然后,把m的next赋值给sPool
sPool = m.next;
//然后,把已经去出去的msg的额next置空
m.next = null;
m.flags = 0; // clear in-use flag
//这时候,消息池已经去出去了一条消息,消息池大小就减一
sPoolSize--;
return m;
}
}
//sPool为空,就new一个msg对象
return new Message();
}
简单的理解就是,不需要重新创建消息,从消息池里面取出一条消息,赋值给我们需要创建的msg对象。
这里为什么要加锁?什么情况下需要加锁?当然是防止并发呀,handler可以随时随地的发消息,所以,为了防止并发,加锁。
问题来了,这个sPool是什么时候赋值的呢?我们创建消息的时候没有赋值。创建的时候没有赋值,我们在Message类里面,检索sPool对象,我们找到如下方法:
@UnsupportedAppUsage
void recycleUnchecked() {
//重置一些列的成员变量
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = UID_NONE;
workSourceUid = UID_NONE;
when = 0;
target = null;
callback = null;
data = null;
//就是这里,加锁
synchronized (sPoolSync) {
//当前消息池子是否小于限制的最大值
if (sPoolSize < MAX_POOL_SIZE) {
//把sPool赋值给next
next = sPool;
//sPool赋值现在的消息
sPool = this;
//消息池子大小加1
sPoolSize++;
}
}
}
看这个方法名就应该能猜到,消息回收的时候调用的。所以,在消息回收的时候,就把这条消息重置,把这条回收的消息赋值给sPool,这里就是赋值的位置。在消息回收的时候赋值。
所以,只要你并发量不大,你每次都是obtain创建消息,基本上都是复用的,不会重新创建消息。
消息说完了,跑题了,跑题了,言归正传
handler发送消息的流程
欢迎来到走进科学之Android消息发送流程,我们来一步一步的剖析这条消息是怎么一步一步放进消息队列的。
Message msg = Message.obtain();
msg.what =1;
msg1.arg1 = 20;
handler.sendMessage(msg);
我们来看这个sendMessage的源码。
public final boolean sendMessage(@NonNull Message msg) {
//是不是眼熟,没错,它实际上调用的就是我们延时消息的方法,只不过,这个延时的时间是0
return sendMessageDelayed(msg, 0);
}
//我们再来看看这个sendMessageDelayed方法
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
//这里有个判断时间,小于0,就赋值给0,所以,发送延时消息的时候时间传递负数,会立马接收到消息,知道是为什么了吧?
if (delayMillis < 0) {
delayMillis = 0;
}
//这里又调用的sendMessageAtTime方法
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
//这里我想说的是SystemClock.uptimeMillis():表示系统开机时间
//我们再来看看这个sendMessageAtTime方法
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
//这里有一个消息队列的判断,这个消息队列是在Handler创建的时候赋值的。
//可以点进去看一下。Hander构造方法传递一个Looper,Looper构造方里面创建了msgQueue,就是这个。
MessageQueue queue = mQueue;
//如果是空,就抛异常,因为消息队列都没有,循环啥?
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
//这里又调用了enqueueMessage
return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
//这里,我们前面说消息的时候,说很重要,就是在handler发送消息的这里赋值,这个值也是后面Looper发送给哪个handler的依据。
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
//这个是判断你的这个消息是不是异步,提升消息优先级的位置。同步消息,同步屏障,异步消息。
if (mAsynchronous) {
msg.setAsynchronous(true);
}
//这里就开始进入到消息队列了
return queue.enqueueMessage(msg, uptimeMillis);
}
??msg.target是后面Looper拿到这条消息之后,发送的目的地,不然,Looper怎么知道要发送给谁(handler)?
??提升消息优先级的位置。同步消息,同步屏障,异步消息。也是比较重要,后面再细唠。
MessageQueue怎么把这条消息放进队列的
到这里handler的发送就完了,MessageQueue怎么把这条消息放进去的呢?方法如下:
boolean enqueueMessage(Message msg, long when) {
//判断有没有目标handler,如果没有,直接就抛异常,都没有这个目的地,我最后取出这条消息,我发给谁?所以,直接就抛异常
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
//加锁,不加锁,如果很多消息同时需要加紧队列就会出问题
synchronized (this) {
//判断这条消息是否正在使用,如果正在使用,那也抛异常。为什么消息会正在使用呢?
//我们前面说了obtain方式消息是复用的,发送消息会并发,所以,是吧?
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
//如果,当前msgQueue正在退出,把消息回收了。
//比方说,你新建线程请求网络,网络请求完,线程一般就会死掉了,线程都没有了,MsgQueue当然要退出了。
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
msg.recycle();
return false;
}
//走到这里,消息就准备放进消息队列了,就是放在那里的问题
//给消息加个标记,表示消息正在使用。跟前面那个判断正好对应
msg.markInUse();
//这个时间,还记不记得?系统开机时间+你延时的时间
msg.when = when;
//把消息队列的当前消息赋值给p
Message p = mMessages;
//是否需要唤醒线程,唤醒跟休眠都是native方法。
boolean needWake;
//当前消息是空,说明当前消息队列没有消息,就直接把我们传递的这条消息加进队列
//我加进来的这条消息的执行时间是0,时间是不会有负数的,如果传进来是负数,都被改成0了,所以,我加的这条消息应该是最先执行的,所以,要加进队列
//加进来的这条消息的执行时间小于当前线程的执行时间,我加进来的这条消息执行的时间,比你当前消息队列循环的时间小,说明,我要在它的前面执行,要加进队列
//上面这个时间小的问题,你可以理解成,消息队列循环的时间是延时10秒处理的,我新进的这条消息是要延时5秒,所以,要放在它的前面
if (p == null || when == 0 || when < p.when) {
//走到这里,说明新加消息需要放在队首 //我新加的消息放进来了之后,要把当前消息的后面,也就是我新加消息的next
//因为,我新加的消息要在它的前面执行
msg.next = p;
//然后,把我新加消息赋值给当前消息变量
mMessages = msg;
needWake = mBlocked;
} else {
//走到这个else里面,就说明当前消息不需要放到队首,就循环判断看它要被放在哪
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
//进入死循环
for (;;) {
prev = p;
p = p.next;
//如果当前msg的下一个消息是空,表示没有消息了,for循环就要中止了,需要把新加消息放进来了
//如果当前消息的下一条消息的执行时间在新加的执行时间的后面,说明,新加消息要在这条消息的前面执行。所以,for循环就要中止了,需要把新加消息放进来了
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
//把当前消息放在新加消息的后面
msg.next = p;
//把新加消息,放在当前执行消息的后面。此时,消息就插件队列了
prev.next = msg;
}
// We can assume mPtr != 0 because mQuitting is false.
//是否需要唤醒消息队列开始循环获取消息,是native层面做的事情。
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
总结下来就是:三个条件
- 根据当前队列是否空闲(p == null)
- 当前消息执行的时间when(when == 0)
- 当前队列执行的消息是否需要在新增消息的后面执行(when < p.when)
??来判断当前消息是否需要插到队首,只要满足上面的任意一个条件,就需要放进队首;否则,for循环判断当前消息需要放到消息队列的哪个位置。需要插队的话就记得把队列中后面的消息放到当前消息的后面。
再重复一遍,这个时间是SystemClock.uptimeMillis() + delayMillis,系统开机时间+你传递的延时时间。
到这里,消息就被插件消息队列了。代码基本上每行都有注释,一遍没有看懂的话就多看几遍。
Looper读取消息:loop()
消息已经放进队列了,第二个问题就结束了,接下来就是第三个问题:Looper怎么读取消息的?
Looper是通过loop()方法循环读取消息的。代码如下:
代码比较多,我把无关的(看不懂的)都去掉了
public static void loop() {
//获取当前线程的looper
final Looper me = myLooper();
//如果,等于null,就抛异常,看异常的消息就应该看的出来,说没有在当前线程调用Looper.prepare()方法
//Looper.prepare()这个方法就是创建Looper的
if (me == 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) {
// No message indicates that the message queue is quitting.
return;
}
...
//获取观察者模式的对象
final Observer observer = sObserver;
...
Object token = null;
if (observer != null) {
//这里应该是这个观察者对象发送了一个消息正在分发的消息
token = observer.messageDispatchStarting();
}
...
try {
//msg.target:是不是很眼熟?就是需要接收这条消息的handler //通过这个handler调用dispatchMessage方法,发送消息 msg.target.dispatchMessage(msg);
if (observer != null) {
//然后,观察者发送一个消息分发完成的消息 observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
if (observer != null) {
//如果出现了异常,这个观察者就发送一个消息分发异常的消息 observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
...
//眼熟不?就是前面说的,消息回收,重复利用,就是在消息分发完成之后触发
msg.recycleUnchecked();
}
}
我们先看一下这个handler的dispatchMessage方法:
public void dispatchMessage(@NonNull Message msg) {
//这个Message的callback是什么时候赋值的呢?就是创建Message的时候,可以回过头去看一下
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
//这个方法,眼熟吗?看下面,我们新建handler的时候,不就重写了这个方法吗?
handleMessage(msg);
}
}
Handler handler = new Handler(Looper.getMainLooper()){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
}
};
到此,消息的发送,入队,取消息,处理,就形成了闭环了。整个流程:
- 新建handler,发送消息sendMessage
- 此时消息的创建obtain复用模式,后面可能会造成正在使用的异常,所以,需要加锁同步一下
- 然后,消息进队,target(目的地的handler)和when(执行的时间系统开机时间+延时时间)
- 判断的三个条件,是放进队首(队列中是空的,时间是0,时间在队列消息时间的前面),还是队中(需要循环判断队列中是否还有消息和时间)
- 通过loop方法取出来消息,通过这个消息的target发送消息
我们接下来说第四个问题:handler发送消息能发送延时消息,Looper读取到消息之后,怎么确定是立刻发送回去,还是隔多久发送回去?
我们上面分析完,好像并没有看到这个延时消息的问题啊,Looper的loop方法是,只要queue.next()返回给它消息了,它就直接发送回去了,没有什么延时。
MessageQueue读取消息:next()
重点就在这里queue.next(),读取消息。这里也是提升消息优先级的位置(同步屏障,异步消息)。
@UnsupportedAppUsage
Message next() {
final long ptr = mPtr;
//通过注释翻译过来就是:loop已经退出了,或者应用正在尝试重启一个looper,就直接return null
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
//下一次循环的时间,单位是:秒
int nextPollTimeoutMillis = 0;
//开始进入死循环去读取消息
for (;;) {
if (nextPollTimeoutMillis != 0) {
//不知道啥意思。
Binder.flushPendingCommands();
}
//这是一个native方法,看名字,大概的意思应该就是循环一次,经过nextPollTimeoutMillis长的时间
//就是底层C/C++经过这么长时间之后,触发一次循环
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
//屏障消息的实质就是创建一条target为null的消息
//看这里的if条件,正常的消息target不等于null,这里的判断是不会进入的。
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
//再看这里的do...while循环,退出的条件是找到一条不为空的异步消息。
//msg.isAsynchronous():异步消息返回true,取反之后就是false,更前面&&,就是false,就退出do..while循环了
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
//消息不为空
if (msg != null) {
//当前时间小于消息需要执行的时间,说明是延时消息。
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
//修改这个下次循环的时间,前面说的native调用的时间,就是根据这个变量判断的。
//时间就是消息执行的时间-系统开机时间=延时时间
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
//否则就是一条实时消息,就是正常的赋值流程,返回这条消息给looper,然后发送出去
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
//消息是空,就表示没有更多消息了
nextPollTimeoutMillis = -1;
}
...
}
}
流程就是:
- 先判断looper有没有,系统有没有重启,如果重启了,looper没有,那就直接返回一个null对象,Looper接收到了一个null对象,会直接return
- 然后,判断是不是屏障消息(屏障消息消息的target等于null),如果是屏障消息,就进行do…while循环,直到取出一条异步消息为止
- 正常的判断消息,是同步消息还是延时消息,同步消息立刻返回,延时消息,提醒底层多长时间之后再调用我
??看到了吗?MessageQueue取消息的流程,通过msg的执行时间与当前系统的开机时间进行比较,延时消息就是判断了延时多长时间之后,告诉底层多长时间之后,你还要调用一次这个取消息的方法。这就是延时消息的实现。
如何提高消息的优先级?同步消息,屏障消息,异步消息
既然说到这里,我们就直接聊一下这个消息的优先级
ps:这里的异步消息,同步消息,并不是说多线程去处理消息。异步消息是有一个属性是true
//同步消息
Message msg = Message.obtain();
//异步消息,调用了一个setAsynchronous并且设置为true
Message msg = Message.obtain();
msg.setAsynchronous(true);
我们平时发消息是下面这个样子的:
Message msg1 = Message.obtain(handler,new Runnable(){
@Override
public void run() {
Log.v("hcy","这是一条延时3秒的消息");
}
});
handler.sendMessageDelayed(msg1,3*1000);
Message msg = Message.obtain(handler, new Runnable() {
@Override
public void run() {
Log.v("hcy","这是一条延时5秒的异步消息");
}
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
msg.setAsynchronous(true);
}
handler.sendMessageDelayed(msg,5*1000);
Log.v("hcy","两条消息都发送完了");
上面就是new了两条消息,一条同步消息,一条异步消息,如果没有屏障消息的情况下,同步消息和异步消息是一样的,没啥区别。程序运行完,过三秒钟同步消息回调,再过两秒打印异步消息回调,上面消息的打印结果如下:
2021-11-21 08:53:10.363 29452-29452/com.haichenyi.myapplication V/hcy: 两条消息都发送完了
2021-11-21 08:53:13.366 29452-29452/com.haichenyi.myapplication V/hcy: 这是一条延时3秒的消息
2021-11-21 08:53:15.365 29452-29452/com.haichenyi.myapplication V/hcy: 这是一条延时5秒的异步消息
那么,什么是屏障消息呢?怎么实现呢?我们先说怎么实现的。
Message msg1 = Message.obtain(handler,new Runnable(){
@Override
public void run() {
Log.v("hcy","这是一条延时3秒的消息");
}
});
handler.sendMessageDelayed(msg1,3*1000);
Message msg = Message.obtain(handler, new Runnable() {
@Override
public void run() {
Log.v("hcy","这是一条延时5秒的异步消息");
//异步消息处理完移除消息屏障
try {
Class<?> msgQueue = Class.forName("android.os.MessageQueue");
Method removeSyncBarrier = msgQueue.getDeclaredMethod("removeSyncBarrier", int.class);
removeSyncBarrier.invoke(Looper.myQueue(),token);
}catch (Exception e){
e.printStackTrace();
}
}
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
msg.setAsynchronous(true);
}
handler.sendMessageDelayed(msg,5*1000);
try {
//启动消息屏障
Class<?> msgQueue = Class.forName("android.os.MessageQueue");
Method postSyncBarrier = msgQueue.getDeclaredMethod("postSyncBarrier");
token = (int) postSyncBarrier.invoke(Looper.myQueue());
}catch (Exception e){
e.printStackTrace();
}
Log.v("hcy","两条消息都发送完了");
打印结果如下:
2021-11-21 09:05:36.915 29743-29743/com.haichenyi.myapplication V/hcy: 两条消息都发送完了
2021-11-21 09:05:41.922 29743-29743/com.haichenyi.myapplication V/hcy: 这是一条延时5秒的异步消息
2021-11-21 09:05:41.949 29743-29743/com.haichenyi.myapplication V/hcy: 这是一条延时3秒的消息
??代码执行完之后,先是过了5秒回调了异步消息,然后立刻回调了同步消息,为什么呢?因为,同步消息是延时3秒执行呀,异步消息是延时5秒执行,因为加了消息屏障,会把异步消息的优先级提到同步消息的前面,所以,执行完异步消息,同步消息的执行时间早就过了,肯定要立刻执行呀。
说了这么多,那么,这个提升优先级是怎么实现的呢?透过现象去看本质。两段代码的区别,就是通过反射,执行了两个方法postSyncBarrier,removeSyncBarrier。其中还有一个带参数的方法。
//执行消息屏障
try {
Class<?> msgQueue = Class.forName("android.os.MessageQueue");
Method postSyncBarrier = msgQueue.getDeclaredMethod("postSyncBarrier");
token = (int) postSyncBarrier.invoke(Looper.myQueue());
}catch (Exception e){
e.printStackTrace();
}
//移除消息屏障
try {
Class<?> msgQueue = Class.forName("android.os.MessageQueue");
Method removeSyncBarrier = msgQueue.getDeclaredMethod("removeSyncBarrier", int.class);
removeSyncBarrier.invoke(Looper.myQueue(),token);
}catch (Exception e){
e.printStackTrace();
}
我们先来看看这个消息屏障的方法:
@UnsupportedAppUsage
@TestApi
//我们反射调用的是这个方法,它最终执行的是一个同样名字的带参的重载方法
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
//最终执行到这里
private int postSyncBarrier(long when) {
//以来还是老规矩,加锁,防止多线程调用
synchronized (this) {
//token比较重要,是移除屏障的标记
final int token = mNextBarrierToken++;
//常规的msg的创建,msg执行的时间是系统的开机时间
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
//把这个token值赋值给了msg的arg1变量
msg.arg1 = token;
//说白了,下面就是链表操作了,不停的移动指针
//prev:上一条消息变量
Message prev = null;
//p:消息。
//mMessages:这个变量眼熟不?我们前面消息从队列中取的时候就是那个next()方法,
//在进入for循环里面,判断是否是屏障消息之前,是不是也同样是给一个成员变量赋值,赋值的值也是mMessages。
Message p = mMessages;
//执行时间不等于0,前面取的时候那三个条件,有一个时间等于0,就插进消息队首,这里我觉得也可以这样理解
if (when != 0) {
//这里是一个while循环,字面理解就是当前消息不等于null,当前消息的执行时间,小于屏障消息的执行时间,就继续循环。直到这两个条件不满足为止
//结合上下文的意思就是,我执行这个屏障消息的时候,如果发现队列里面还有消息的执行时间在我这个屏障消息的前面,就继续让它先执行。
while (p != null && p.when <= when) {
//把当前消息赋值给变量prev
prev = p;
//把当前消息的下一条消息,赋值给p变量(当前消息变量)
p = p.next;
//继续while循环
}
}
//经过上面的while循环之后,就找准了屏障消息该插入的为止了
if (prev != null) {
//如果prev不等于null,表示,消息队列里面还有需要在屏障消息前面执行的消息
//队列就变成了:prev-屏障消息-当前消息
msg.next = p;
prev.next = msg;
} else {
//如果prev等于null,就表示消息队列里面没有需要在屏障消息执行前面执行的消息了
//队列也就变成了:屏障消息-当前消息
msg.next = p;
mMessages = msg;
}
//返回这个token值。移除屏障消息的时候需要用到
return token;
}
}
上面这个执行消息屏障说的很清楚了吧?多看注释,多理解。
我们再来看看这个移除消息屏障
@UnsupportedAppUsage
@TestApi
public void removeSyncBarrier(int token) {
//加锁
synchronized (this) {
//两个变量赋值
Message prev = null;
Message p = mMessages;
//这个while循环,第一个条件:当前消息不等于null(当前消息队列还有消息)
//第二个条件:当前消息得目的地不为空(我们屏障消息这里是等于空的,这里应该是为了判断其他地方调用这个方法)
//然后就是第三个条件,我们传进来的token值,就是上面执行屏障消息时候的返回值,当时赋值给了arg1。这里比较,如果相同,那么,这条消息就是屏障消息
//第二个条件和第三个条件是或的关系,满足一条就行。
//屏障消息就要移除,链表的常规移除操作
while (p != null && (p.target != null || p.arg1 != token)) {
prev = p;
p = p.next;
//其实就是:原来是:上一条消息——屏障消息——下一条消息
//变成了:上一条消息——下一条消息
}
//到这里就移除完了
//上面while循环完,发现当前消息是空,说明消息队列中没有消息了,直接抛异常
if (p == null) {
throw new IllegalStateException("The specified message queue synchronization "
+ " barrier token has not been posted or has already been removed.");
}
final boolean needWake;
if (prev != null) {
prev.next = p.next;
needWake = false;
} else {
mMessages = p.next;
needWake = mMessages == null || mMessages.target != null;
}
//消息回收
p.recycleUnchecked();
// If the loop is quitting then it is already awake.
// We can assume mPtr != 0 when mQuitting is false.
//native层的是否需要唤醒服务
if (needWake && !mQuitting) {
nativeWake(mPtr);
}
}
}
总结一下这个提升消息优先级的方式就是:把你想发送的消息定义view异步消息发送,光这样还不行,还要发送一条屏障消息,具体流程:
- 往消息队列里面插入一条target为null的消息
- MessageQueue.next()方法读取的时候,会先判断这条消息是不是屏障消息,如果是,他就会执行do…while循环,直到找到一条异步消息为止。
- MessageQueue拿到消息之后,正常的取消息流程
- 在你执行完这条异步消息之后,记得要移除屏障消息,不然所有的异步消息都在同步消息前面执行。
其实有个更简单的方法,handler发消息的api都给出来了
//发送消息到队列前面
handler.sendMessageAtFrontOfQueue(msg1);
经过上面的整个流程之后,最后一个问题就比较简单了,自己看一下源码吧,我给出结论:
Looper.prepare():给当前线程创建Looper,MessageQueue的过程,这个MessageQueue是可退出的
Looper.loop():从头到尾都在说loop(),循环读取消息。
handler知识点总结
总结一下handler的东西:整理了一个流程图:
|