Android Handler消息机制中的诸多疑问

深入探讨Android中Looper、Handler和MessageQueue的工作原理,解释主线程为何不会因Looper的死循环而卡死,揭示消息唤醒机制及Looper退出条件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

网上总是有很多阐述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相关代码,这里算是一个小小的疑问吧,有明白的同学也可以给我解释一下。

太不容易了,写这一篇 看了一天源码,写了一天博客,反复斟酌,喜欢能够帮到一些人,有什么意见可以留下,我们共同讨论,共勉。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值