前言
网上总是有很多阐述Android消息机制的文章,基本上大同小异,都是讲Handle,Message,Looper,MessageQueue这四个类会如何协同工作的。但是动脑筋的童鞋们可能总是会有如下的一些疑问,我翻阅了数多微博,很多年了,也没有看到相关比较完整的解释,所以这些天自己深刻阅读了一下源码,并且为自己解答了心中一直存在的疑惑,记录在此,希望也能帮助有同样疑问的小伙伴。
文章目录
Handler消息机制的诸多疑问
1、主线程中的Looper死循环取message,为什么不会卡死主线程?
我觉得这个问题困扰着每一个“了解”handler的人,我们经常看到一些讲解消息机制的博客,说了一大堆,ActivityThread启动之后,就Looper.loop(),之后这个方法里就通过死循环去取MessageQueue的Message,可是真正思考过的小伙伴一定想过,那么主线程岂不是一个死循环卡死在那里,那还怎么工作?那么请看如下代码:
public static void loop() {
//取出本线程的looper,没有looper的话就抛异常,这也解释了为何在子线程使用handler得先去Looper.prepare()
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
//取出本线程的MessageQ
final MessageQueue queue = me.mQueue;
.............
//开始死循环
for (;;) {
//从MQ里面next来取消息,我们从源码注释了也可以看到 这个next()方法可能会阻塞,我们去看看。
Message msg = queue.next(); // might block
//如果从MQ取出null来,说明没有消息了,就结束返回,这也代表着looper的loop循环结束退出。
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
.......
msg.recycleUnchecked();
}
}
接下来我们去看next()方法,我贴的代码会尽可能少,只贴重点,每一行都会标明注释。
Message next() {
//这个ptr变量 保存的是一个内存地址,对应本地方法中的指针!,很重要,消息循环要结束,就是将它置为0,当然还要销毁本地指针。
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
//这两个变量很重要,先注意一下
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
//死循环开始从自己的链表里取消息
for (;;) {
.......
//看这个方法,很重要,从名字上看它就知道是一个native方法,这个方法用的是linux的一种poll机制,
就是你去读取一个玩意(ptr),然后第二个参数是超时时间,大概就是,如果有数据的话就会返回,
如果没数据的话,就等待nextPollTimeoutMillis这么时间,而重要的是它在等待的过程中是不占用CPU的,
是一种休眠状态,放弃CPU,让CPU可以去干其他的事情。所以我们是不是觉得已经找到了答案?其实还没有,
因为我们可以看到上面的变量,nextPollTimeoutMillis = 0,所以他没有设置超时,是一种随取随用的状态,所以
这个方法暂时像普通方法一样走。
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// 取消息
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
.......
//消息不为空
if (msg != null) {
//如果没到消息该执行的时间,就nextPollTimeoutMillis 赋值为还需要等待多久,因为这个变量是给nativePollOnce方法
使用的,所以我们猜想得到,遇到消息时延时消息的话,我们会通过这种不占CPU的方法去睡眠这么久,醒来刚好
执行延时消息
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
//如果不为空的就返回消息给Looper.
//这个变量很重要,留意一下,如果有消息并且返回 它是等于false的
mBlocked = false;
..........
return msg;
}
}
//消息为空的情况下,nextPollTimeoutMillis = -1,我们同样知道它是给nativePollOnce方法用的,传-1进去,代表着永久
等待,除非有人通过某个方法去唤醒。
else {
// No more messages.
nextPollTimeoutMillis = -1;
}
// 如果退出了,就dispose去释放ptr,ptr = 0,然后返回null,我们知道loop就结束了。
if (mQuitting) {
dispose();
return null;
}
//还记得一开始那两个变量吗,一个nextPollTimeoutMillis ,一个pendingIdleHandlerCount = -1,
我们知道Looper刚开始循环是没有消息的,所以mMessages也等于空,所以如下条件是成立的,然后
pendingIdleHandlerCount = mIdleHandlers.size(),而mIdleHandlers是一个成员变量,一个List,可以通过MQ的
addIdleHander方法来为这个列表添加对象,但是我查看过,系统没有去添加的相关代码,所以这个size就为0。
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
//如果size为0了,这个条件就成立,那么mBlocked = true,然后continue就执行下一次循环了,
我们知道这个时候nextPollTimeoutMillis 已经等于了 -1,所以循环的下一次就会放弃CPU进入无限等待的状态。
这下我们就知道了,为什么这个死循环不会造成卡死了吧? 那么问题又来了,无限等待的话,那谁来唤醒他?
请看下一节,嘻嘻。
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}
. ........ 这个中间有很长一段过于IdleHandler的代码,它与上面的代码逻辑是独立的 ,不影响上面的执行,所以我把
这里删除了,为了避免给大家产生疑惑,觉得看不懂。其实没关系,看不懂没关系,我们从代码结构和变量上也可以
看的出来,我下面会贴出一片讲这段代码的博客。
}
.........
//这一步一般执行不到,如果要执行到,mIdleHandlers这个list的size应该是大于0的,而这个列表的相关作业,
不在这个讨论范围。
nextPollTimeoutMillis = 0;
}
}
虽然我秉承着尽量不贴代码的方式,因为我怕大家看到代码太多会头晕眼花,犯困瞌睡,但是如果没有关键代码,光靠我说,大家如何信服我说的呢?所以我总结一下这一节的问题:
为什么死循环不会卡死主线程? 因为他通过native方法用到了Linux的Poll机制,这是一种放弃CPU进入睡眠状态的等待机制,还有重要的一点是超时时间设置为负数的话,就代表无限等待。
所以这就是一个Looper开始工作时的状态,死循环了一次,在第二次的时候便进入了不占CPU无限等待的状态。那么它什么时候被唤醒呢啊?看下节
2、当线程的Looper收到消息的时候,如何唤醒阻塞?
带着上一节的疑问,我们来到的这一节,能坚持到第二件就说明大家很有毅力了,我现在开始讲解谁唤醒了那个无限等待。我们可以先猜想一下,当我们发送一个消息的时候,是不是进是它该醒来的时候了?我们去看一下消息入队的方法(怎么从Handler走入到MessageQueue的过程我就不再重复的阐述了,我们直接看MessageQueue的enqueueMessage方法:
boolean enqueueMessage(Message msg, long when) {
.......
synchronized (this) {
//如果发消息的时候,Looper退出的话,直接回收消息并且返回
if (mQuitting) {
msg.recycle();
return false;
}
//标记消息为正在使用
msg.markInUse();
msg.when = when;
Message p = mMessages;
//看到这个重要的变量没?这个就是唤醒的关键。
boolean needWake;
if (p == null || when == 0 || when < p.when) {
//如果队列里面没消息,就让新进来的消息成为队列头
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
//从上面一节的分析,我们知道 ,没消息的时候,这个mBlocked被赋值为了true.不信的回去看。
needWake = mBlocked;
} else
//如果有消息的话,needWake的值需要如下三个条件共同成立。为什么呢?根据原来英文注释的解释应该是这样的:
通常不需要唤醒事件队列,除非这个是一个最早的异步消息,或者在头部阻塞的情况下。所以在我看来:
enqueue的这个入队方法,在第一行就对message.target判空了,如果为空的话就抛出异常,说明这个p.target == null几乎
在任何时候不成立,而这里的needWake一般也是false,为什么要这样设计呢?我个人感觉是,你看这个if else,如果没有
消息的话,它在添加message为队列头的时候,就会给needWake = mBlocked = true。所以它本意是只在队列头部唤醒
一次消息队列,然后就一直取,直到取完消息队列之后,再会进入-1的那种无限等待状态。
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
.....取合适的队列消息
}
}
// We can assume mPtr != 0 because mQuitting is false.
//看这里,如果是true的话,就通过nativeWake去把ptr这个东西唤醒,而这个东西就是我们在next中nativePollOnce时传入的。
所以我们可以知道,当消息入队时,并且是一个消息队列的头部时,一般会去唤醒阻塞的next()方法。
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
所以我们来总结一下:
当Handler发送消息到MessageQueue的时候,在MessageQueue的enqueueMessage入队方法中,我们会判断是否需要唤醒阻塞在那边的next()方法。而唤醒的条件,一般是在队列的头部来唤醒,然后一直取,直到取完这个消息队列,然后再进入-1的那种无限等待状态。
可能会有细心的同学发现,如果是那种延时消息呢?其实我们已经知道,如果是延时消息的话,它会将nextPollTimeoutMillis 置为需要休眠的时间,然后去休眠这么久,不占用CPU,让CPU继续去处理下一条消息,然后等着nextPollTimeoutMillis 时间之后,自动唤醒。
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
}
3、线程中的Looper开启循环之后,会自动退出吗?何时退出?
这个问题已经不涉及到源码了,本来这一节的标题是“子线程中的Looper会自动退出吗?合适退出“,但是当我对源码深入的了解之后,已经经过自己实验与查证之后,可以确信。无论是主线程还是子线程,Looper都不会自动退出。除非你去调用它的quit方法。因为在我脑子中,一直相信子线程中使用完handler或者looper不用管他,他会自动退出,但是我错了,根本没有自动退出的,你需要手动调用Looper.quit()方法。所以也给了我们一个警示,在子线程中创建了looper之后,记得quit释放,要不会引发内存泄露等各种问题。以下是我的实验数据:
在activity启动的时候,开启一个线程创建Looper,开启循环,然后点击按钮可以发送消息:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
handler = new Handler(){
@Override
public void handleMessage(Message msg) {
Log.i("mydata","fuck everyone");
}
};
Looper.loop();
}
}).start();
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.bt_show:
handler.sendEmptyMessage(1);
break;
}
}
11-07 00:06:58.188 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
11-07 00:07:01.836 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
11-07 00:07:02.484 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
11-07 00:07:02.676 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
11-07 00:07:02.876 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
11-07 00:07:03.056 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
11-07 00:07:03.236 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
11-07 00:07:03.428 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
11-07 00:07:03.612 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
11-07 00:07:03.780 2642-2674/com.ryrj.testgit I/mydata: fuck everyone
当然我们也可以用Linux命令来查看存在多少线程,以及可以看到,我们子线程并不会自己销毁,因为Looper一直在loop。
注意:这也为我们提了个醒,这就是我们的主线程,为什么可以一直存在,不会因为没有消息就执行结束了。
4、主线程的looper与子线程的looper有何区别?
他两主要的区别就在于quit了。因为他们在创建时候有这样的区别:
public static void prepareMainLooper() { //主线程用来创建looper的方法
prepare(false);
........
}
public static void prepare() { //普通线程
prepare(true);
}
private static void prepare(boolean quitAllowed) { //都会调到此方法,不同的是变量的值
.......
sThreadLocal.set(new Looper(quitAllowed));
}
所以我们可以知道,主线程的looper是quitAllowed = false,子线程的是quitAllowed = true。就是主线程不允许退出,子线程允许退出。其他无恙,所以我们子线程使用完Looper一定要记得自己退出,避免产生麻烦。
5、遗留问题,主线程Looper会退出吗?如何退出?
解决完上述问题之后,我又发现和思考了一个有趣的问题,那么主线程的looper是何时退出?它在创建的时候已经表明了quitAllowed = false,不允许退出,我们也可以试着自己手动调用Looper.getMainLooper().quit(),可以发现会抛异常。那么主线程的looper到底何时停止,如果不停止那岂不是代表着程序一直会存在吗?我猜想主线的问题肯定在ActivityThread.java类中可以找到答案。所以我去找了这个类,并且在H这个handler中发现了一个很显眼的一条消息:
case EXIT_APPLICATION:
if (mInitialApplication != null) {
mInitialApplication.onTerminate();
}
Looper.myLooper().quit();
break;
我的天啊,我们知道开始一个应用的时候 先发的一个消息叫BIND_APPLICATION,那么这个EXIT_APPLICATION是否就标志着退出应用呢?它是否是退出应用的标志,我还没有确定,但可以确定的是,收到这个消息之后,主线程会退出Looper循环。但是又有一个奇怪的问题,就是我手动去调这行代码,会抛异常,因为主线程的looper是不允许退出的,但是在这里由系统调就不会抛出异常,我很是郁闷,我同时也看了层层调用,并没有try catch相关代码,这里算是一个小小的疑问吧,有明白的同学也可以给我解释一下。
太不容易了,写这一篇 看了一天源码,写了一天博客,反复斟酌,喜欢能够帮到一些人,有什么意见可以留下,我们共同讨论,共勉。