Android的消息机制--Handler

一、四大组件概述

Android的消息机制是由Handler、Message、MessageQueue,Looper四个类支撑,撑起了Android的消息通讯机制,Android是一个消息驱动系统,由这几个类来驱动消息与事件的执行

Handler:

  • 用来发送消息和处理消息
  • 无论使用的post ,还是send,都会执行enqueueMessage 方法,将消息加到队列中
  • 发送的消息并不是立刻得到执行的,所以必须有个地方把它存起来,也就是MessageQueue

MessageQueue

  • 基于单向链表,以触发时间的顺序排放在队列中,链表头部信息被触发的时间是最接近的
  • 队列中的消息怎样让它在必要的时间得到执行,就需要依靠Lopper

有三种消息类型:

  • BarrierMessage:屏障消息

  • AsyncMessage:异步消息

  • Message:同步消息

Lopper

  • 无限循环驱动器
  • 在它内部有个loop 方法,开启无限循环,从头遍历这个队列,检查满足条件的消息,有就把它取出来进行分发执行,没有或者消息为空时,当前线程就会进入阻塞状态,从而释放掉CPU 的资源占用,当有新消息进来的时候就会唤醒当前线程,从而继续遍历队列中是否有满足条件的消息,所以Looper并不会真的一直无限循环下去

Message

消息

  • long when:该消息被执行的时间戳,这个时间戳是它在队列中排队的唯一依据
  • Message next:消息队列是一条单向链表,每一条消息都会包含下一条消息的引用关系,从而形成单向链表
  • Handler target:代表着该message是由哪一个handler发送的,在消费这条消息时也由这个target来消费
  • 一个线程最多存在一个Lopper
  • 一个Looper对应一个MessageQueue
  • 一个MessageQueue中可以存在多个Message对象
  • 一个MessageQueue 或者一个Looper 对应多个handler

二、消息分发的优先级

  1. Message的回调方法:message.callback.run() 优先级最高
  2. Handler的回调方法:Handler.mCallback.handleMessage(msg)
  3. Handler的默认方法:handler.handleMessage(msg)
Kotlin 复制代码
       //1.直接在Runnable中处理任务
        handler.post(runable = Runnable {
            //这条消息在消费时,首先回调给Message中的callback,也就是runable对象
        })


        //2.使用Handler.callback来接收处理消息
      val handler = object :Handler(callback{
          //在创建handler的时候是可以传递一个callback 的,在消息分发的时候首先把消息分发给callback 来处理
          return@callback true
      })

        //3.最常用的handler的handlerMessage方法
        

三、疑问点

1.在使用handler的时候并没有指定Looper ,这三个类是怎么关联起来的

在创建实例对象时虽然没有传递Looper对象,但是在构造函数的重载里会调用Looper.myLooper来获取当前线程绑定的Looper对象

2.主线程的Looper是在哪里创建的?

在ActivityThread的main方法中调用Looper.prepareMainLooper()创建了主线程的Looper对象,然后调用loop()开启消息队列循环,所以在主线程中创建Handler不用给他创建Looper

如果一个线程的Looper对象没有调用prepare 方法,它的Looper是为空的

3.Looper.myLooper是如何保证获取到的Looper是当前线程的Looper 对象?

Kotlin 复制代码
    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 还是子线程的Looper都只能调用prepare()方法或者prepareMain()方法,在prepare()方法中首先判断在这个当前线程这个Looper对象是否已经被创建过了,如果是,再次调用该方法就会报错。

这就是一个线程最多只能存在一个Looper 对象的保证

接下来把Looper 对象保存到了ThreadLocal中,在获取Looper 时也是从ThreadLocal 这个对象中得到的

ThreadLocal:

是用来存储数据的,使其成为线程的私有局部变量,通过它提供的get、set来访问

好处:可以在线程的任意地方去访问这个局部变量,不用传来传去

4.如何让子线程拥有消息分发的能力?如何在子线程中弹出Toast?

默认子线程是没有Looper 对象的,要主动调用Looper.prepare 方法以及 Looper.loop方法,在这两个方法中间就可以弹出Toast 了,在接着给它创建一个handler,在主线程中拿到这个handler对象,就可以处理在子线程中处理主线程发送过来的消息了

Toast在没有显示出来之前,它还没有被添加到窗口上,对它的操作就不会触发线程的检查

实现方式跟ActivityThread方式一样

需要注意的是一旦给子线程执行了这两个方法,要在必要的时候调用Looper.quit 方法,让Looper 退出循环,否则会一直循环下去,这个线程就不会被销毁,不会被回收

四、消息入队

一条消息被插入到MessageQueue时做了哪些事情?

发送一条消息时主要有两种方法,一种是post开头的,一种是send开头的,无论使用的哪种方式发送,都会以Message的形式插入到队列中

对于post,发送一条消息时,都会通过getPostMessage,把runnable对象包装成Message对象,再次调用sendMessageDelayd方法把它加入到队列中

享元设计模式,共用已创建的对象 避免重复创建对象

getPostMessage方法中在获取Message对象时,使用的是Message.obtain()方法

Message提供了消息复用池的能力,最多会缓存50条Message对象,通过.obtain()方法来获取 Message对象是可以复用的,不需要每次都去创建一个新的

采用了链表的形式来管理消息池,链表的插入和删除比ArrayList快

还提供了消息回收能力,当消息被分发完了之后就会调用recycleUnchecked,将Message的对象进行重置,情况数据,并将其插入到链表的头节点中,它是一个队头复用机制

通过new Message出来的对象会出现大量的临时的Message对象,会导致内存占用率过高

消息入队,按照消息被执行的时间戳when插入队列

  • 无论是post、还是send,最终都会执行enqueueMessage,把消息插入到队列中
  • postSyncBarrier:发送一条屏障消息

新消息插入时按照消息插入的时间插入到队列中

enqueueMessage

Handler.enqueueMessage

java 复制代码
    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;
       ......
        return queue.enqueueMessage(msg, uptimeMillis);
    }

将Message的target赋值成当前的Handler,在下面会对这个target进行判空,空的话直接抛出异常

消息被消费的时候会通过handlerDispatchMessahe方法来完成

MessageQueue.enqueueMessage

java 复制代码
  boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }

        synchronized (this) {
            if (msg.isInUse()) {
                throw new IllegalStateException(msg + " This message is already in use.");
            }

            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;
            //mMessage始终是队头消息,拿到队头,这个队列才能执行增删改查的操作
            Message p = mMessages;
            boolean needWake;
            //p == null 说明队头为null,则队列为空
            //when == 0 或者新消息触发的时间戳为0
            // when < p.when 或者消息被触发的时间小于队头消息被触发的时间
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                //满足条件就把新消息插入到队列的头部
                //把新消息的next节点指向原来的头结点,然后将新信息赋值给头消息
                msg.next = p;
                mMessages = msg;
                //如果当前Looper处于休眠状态,则本次插入消息之后需要唤醒
                //mBlocked:是否处于阻塞状态,只有对列为空,队列当中没有可处理消息的时候,线程才会进入阻塞状态,mBlocked才会为true
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                //needWake:要不要唤醒线程,目的是让异步消息尽早的执行
                //mBlocked:当前线程是否处于休眠状态
                //p.target == null:队头消息是否为空,队头消息是异步同步屏障消息
                //msg.isAsynchronous:新消息是异步消息
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                //for循环:找到一个合适的位置来插入这条新消息
                for (;;) {
                    prev = p;
                    p = p.next;
                    //找到合适的位置退出for循环
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                //调整链表中节点的指向位置,实现消息插入队列的目的
                //msg:新消息  p:下一条消息  prev:上一条消息
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            //Looper被唤醒
            if (needWake) {
                //线程被唤醒,使用nativeWake native方法
                nativeWake(mPtr);
            }
        }
        return true;
    }

enqueueMessage在插入一条新消息时,主要是检查消息是否都具备target对象,否则消息是无法被处理的,还会选择性的决定唤醒当前的线程,继续轮询队列是否有符合条件的消息拿出来处理

postSyncBarrier 同步屏障消息

message.target == null ,这类消息不会真的执行,起到标记作用

MessageQueqe在遍历消息队列时,如果队头是同步屏障消息,那么会忽略同步消息,优先让异步消息得到执行

一般异步消息和同步屏障消息会一同使用

异步消息 & 同步屏障

使用场景:

  • ViewRootImpl接收屏幕垂直同步信息事件用于驱动UI测绘
  • ActivityThread接收AMS的事件驱动生命周期
  • InputMethodMessage分发软键盘输入事件
  • PhoneWindowManager分发电话页面各种事件

目的:

让重要的消息尽可能早的得到执行

注意:

  • 开发过程中无法使用,只能系统源码使用
  • 必须使用MessageQueue来发生,Handler是无法发送同步屏障消息的,只能用来发送异步消息和同步消息

MessageQueue#postSyncBarrier

java 复制代码
    public int postSyncBarrier() {
        //使用uptimeMillis,意思是:发送了这条消息,在这期间如果设备进入休眠状态,那么消息是不会执行的,设备被唤醒之后才会执行
        return postSyncBarrier(SystemClock.uptimeMillis());
    }
  • currentTimeMillis() 系统当前时间,即日期时间,可以被系统设置修改,如果设置系统时间,时间值也会发生改变
  • uptimeMillis() 自开机后,经过的时间,不包括深度休眠的时间
    sendMessageDelay.postDelay 也都使用了这个时间戳

问题:

如果使用handler发送一条消息,然后让设备进入休眠,也就是先熄屏,然后长时间不操作手机,这条消息会不会得到执行,为什么?

进入休眠之后,消息是不会被触发的,因为设备休眠之后uptimeMillis是不会被累加的

java 复制代码
 private int postSyncBarrier(long when) {
        // Enqueue a new sync barrier token.
        // We don't need to wake the queue because the purpose of a barrier is to stall it.
        synchronized (this) {
            final int token = mNextBarrierToken++;
            //从消息池复用,构建新消息体
            final Message msg = Message.obtain();
            msg.markInUse();
            //并没有给target赋值
            //区分是不是同步屏障,就看target是否等于null,等于null,就是同步屏障消息

            msg.when = when;
            msg.arg1 = token;

            Message prev = null;
            Message p = mMessages;
            if (when != 0) {
            //遍历队列所有消息,直到找到一个message.when > msg.when 的消息,决定新消息插入的位置
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            //如果找到了合适的位置则插入
            if (prev != null) { // invariant: p == prev.next
                msg.next = p;
                prev.next = msg;
            } else {
                //如果没有找到直接放队头
                msg.next = p;
                mMessages = msg;
            }
            return token;
        }
    }

屏障消息在插入队列时是没有主动唤醒线程的,因为屏障消息并不需要得到执行,也不需要唤醒这个线程去轮询它

屏障消息的移除,谁添加的就由谁来移除

比如ViewRootImpl,在接收到垂直同步信号的到达,发送一条异步消息,并发送了一条屏障消息,当接收到异步消息时,ViewRootImpl就会把同步屏障消息从队列中移除

问题:

ViewRootImpl是如何在UI测绘的工作优先得到执行的?

发送了同步屏障和异步消息

五、消息分发

Looper#loop()

队列中的消息之所以能得到分发,是由于Looper中的loop方法,会开启一个无限for循环消息的驱动器,在这个无限for循环中会调用MessageQueue的next方法,去获取一个可执行的msg对象

这个next方法可能使得当前线程进入一个阻塞的状态,此时这个方法不会有返回值,下面的分发代码就不会得到执行,所以这个无限for循环并不会一直空轮询下去,

目的:只是不让这个线程退出,因为一个线程任务执行完成,就会自动退出,如果想让它不退出,开启一个while循环等待一段时间,这里也是一样的道理

直到队列中有可处理的消息才会返回

java 复制代码
for(;;){
    //取出队列中的消息
    Message msg = me.mQueue.next(); // might block
    if (msg == null) {
        // No message indicates that the message queue is quitting.
        return false;
    }
    ......
    //分发消息
    msg.target.dispatchMessage(msg);
    ......
    //回收Message
    msg.recycleUnchecked();
}   

拿到消息之后调用**msg.target.dispatchMessage(msg)**去分发消息

当消息处理完成之后调用**msg.recycleUnchecked()**方法回收Message,留着复用

总结:

loop方法的作用是从队列中去取消息,然后分发,然后回收

MessageQueue#next()

首先也是开启一个for循环,在消息分发时可能存在消息的插入、删除,而且队列是一个单向链表无法确切知道消息插入时是有多少的,当这个next方法找到一个合适的消息时就会退出for循环

java 复制代码
    int nextPollTimeoutMillis = 0;
    for (;;) {
        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;
                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());
                }
                if (msg != null) {
                    //找到了一条消息,但是它的时间,还没到需要执行的时机
                    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);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        //prevMsg不为空说明需要删除的是中间的消息,只需要上一条消息的next指向msg的next,为空说明要删除的这条消息是队头消息
                        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 {
                    // No more messages.
                    //msg对象为空,说明队头对象为空,也就是当前队列为空,此时把nextPollTimeoutMillis置为-1,looper将进入永久休眠,直到有新消息到达
                    nextPollTimeoutMillis = -1;
                }
    }

通过nativePollOnce这个native方法进行阻塞,传递的nextPollTimeoutMillis,如果这个值是大于0的,就会使得当前线程进入阻塞,并且释放掉对CPU资源的使用权,尽管Looper中有个无限for循环,但是不会造成CPU资源的过多占用

由于nextPollTimeoutMillis第一次for循环时是等于0的,所以第一次不会使得这个线程进入阻塞的,但是如果在接下来的循环中没有找到一个合适的、需要处理的消息,这个nextPollTimeoutMillis会被更新,在第二次循环的时候如果这个值不等于0,这个线程就会进入阻塞状态,如果这个值等于-1,当前线程就会进入无限阻塞状态,直到有新消息插入时,才会被唤醒

nextPollTimeoutMillis等于多少,这个线程就会被阻塞多少ms,超时之后就会继续执行以下代码

延迟消息是怎么得到保证的?

首先是通过uptimeMillis去计算出应该被执行的时间戳,然后借助nativePoll阻塞一段时间,超时之后自动恢复 ,然后继续往下检查是否有满足条件的消息,如果有就拿出来去执行

如何去取消息?

把队头消息赋值给一个临时变量msg,防止插入消息,队头可能被改变,然后判断msg是否是同步屏障消息(也就是判断msg.target == null),如果是通过do-while循环,在while中判断这个msg 不是一个异步消息,也就是说如果这个消息是异步消息,就会退出do-while循环

这个屏障消息唯一的作用就是:当它处于队头时,next方法在检索消息时会跳过同步消息,会优先检索出所有的异步消息,让它们优先执行

msg.target对象如果不为空,不会执行do-while循环,就会按照时间顺序来检索分发消息

判断消息msg是否为空,说明队头对象为空,也就是当前队列为空,此时把nextPollTimeoutMillis置为-1,looper将进入永久休眠,线程进入无限阻塞状态,直到有新消息到达

不为空时,也就是找到了一条消息,检查是否到达需要执行的时机

如果没有,去更新nextPollTimeoutMillis值,等于应该执行的时间 - 当前时间 ,计算出还应该延迟多久

否则说明找到了这条需要处理的消息,首先需要从队列中移除掉

preMsg不等于null,说明被删除的这条消息是队列中间的这条消息,preMsg代表被删除消息的前一条消息,删除这条消息,只需要将preMsg的next节点指向这个消息的next节点

preMsg等于null,说明被删除的这条消息是队头消息,需要将mMessage指向要删除消息的下一个节点

最后把msg对象返回回去,让它去分发,去执行

idler.queueIdle()

java 复制代码
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

判断队头Message是否为空,也就是队列中没有任务了,或者队头消息时间还没有到达可执行的时机,在第二次for循环中,线程即将进入阻塞状态,在进入阻塞状态之前,称之为空闲状态,判断有没有向MessageQueue中注册IdleHandler,用于监听这个状态

IdleHandler:

可以监听当前线程是否即将进入空闲状态,也就是说通过事件的监听来做一些延迟的初始化,以及数据加载、日志上报等工作,而不是有任务就提交,从而避免抢占重要的资源

如果pendingIdleHandlerCount大于0,就会去调用idler.queueIdle()方法

java 复制代码
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

最后会将nextPollTimeoutMillis延迟时间置为0,既然业务层监听了线程的空闲状态,在queueIdle这个方法里,就有可能再次产生新的消息,为了让新消息尽可能早的得到执行,此时不需要让线程进入休眠了

nextPollTimeoutMillis = 0;

在Android中存在两套消息机制,一套是Java的,一套是C++的,本质上是独立的,在Java中MessageQueue调用nativePollOnce的主要原因是借助native消息机制所实现的线程阻塞能力

六、问题解惑

http://t.csdnimg.cn/ZBfg7

相关推荐
拭心10 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王12 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡12 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道13 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库14 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道14 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe14 小时前
Android Hook - 动态加载so库
android
居居飒15 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He18 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗18 小时前
Android笔试面试题AI答之Android基础(1)
android