Android消息机制(三)Handler、Looper、MessageQueue原理探索

331 阅读14分钟

结合此前的内容,我们知道,ActivityThread的main函数中,实际上是做了几件事情:

  1. 初始化Looper
  2. Looper开始循环
  3. messageQueue.next()返回或者阻塞
  4. 处理消息

流程图.jpg

3 ~ 4之间是一个循环,一旦退出这个Looper的循环,那么意味着App也就退出了。

其中messageQueue.next()阻塞,阻塞的正是线程,准确的说应该是用户线程。当ActivityThread对应的主线程执行到这一行时,如果消息队列中没有消息,主线程就会被阻塞在此处,后续如果有新的消息到来了,就会唤醒,继续走4的处理消息的步骤,然后又进入3开始阻塞或者处理下一个消息。

显然,messageQueue.next()是IO,并且是阻塞式IO,它会卡住我们程序的执行。

我们知道,线程是处理机调度的基本单位,而可持续交互程序的本质,是利用一个循环不断地去读取消息,然后处理消息,换句话说,是一个线程在不断地去执行一个循环,读取消息,处理消息,

Looper也很纯粹,它主要的职责就是实现这么个循环,然后不断地去取消息,执行消息,但是它却是我们主线程的核心。我们的App的运行时,无论是从系统还是App里面,都会有源源不断的消息流入。

今天的内容,则主要聚焦于一次Looper将会发生什么:

image.png

1. Message: 消息和行为

我们知道,Looper会不断地进行循环,有则处理,无则阻塞在MessageQueue.next处。而Looper不断等待的内容,正是Message。

我们可以设置一个日志监听,最直观地感受一下不断地被Looper取出处理的Message:

Looper.getMainLooper().setMessageLogging { 
    println(it)
}

而Message包括下面的主要内容:

val message = Message.obtain()
    message.what
    message.arg1
    message.arg2
    message.obj
    message.callback

大致上能够分为两类首先是callback,即Message可以携带一个Runnable,一个可执行的单元,这也是runOnUiThread函数的实现方式。第二类则是使用一系列的标识:what、arg1、arg2等等来发送一些预定的行为。

什么是预定的行为?

例如你可以在ActivityThread.java中,看到class H,中的handleMessage就根据what字段的不同,有着各种各样的回调处理:

public void handleMessage(Message msg) {
  
    switch (msg.what) {
        case BIND_APPLICATION:
            // ......
            break;
        case EXIT_APPLICATION:
            // ......
            break;
        case RECEIVER:
            // ......
}

说白了,就是写死在ActivityThread.java中的回调,这些内容都是系统预置的,如果你通过绑定在主线程Looper上的Handler发送一个what = EXIT_APPLICATION的Message,程序是会被直接退出的,当然,你需要用一点手段才能拿到这样的Handler或者调用对应的方法。

比较早的版本是可以直接通过反射拿到Activity基类的mHandler变量的,但是比较新的版本已经被打上了@UnsupportedAppUsage标记,通过反射一般是拿不到该对象的。

总而言之,Message可以传递两种内容,消息(what/arg1/arg2/obj),也可以利用callback变量传递一段代码Runnable。虽然不存在二者都有的情况,但是逻辑上是Runnable会优先被执行的。

此外Message通过target属性来标记目标Handler

并不是所有Message都会被压在队列中执行,也不会所有的Message都会切线程,具体的细节取决于你对Handler的使用,详见#5。

2.MessageQueue.next()是如何取消息的

上面我们提到了,程序可能会阻塞在messageQueue.next()处,那么Looper是如何阻塞并且获取Message的呢?

Message next() {
    // ……
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }

        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
           // ……
        }

        // run idlehandler
    }
}

大家对上述的代码应该比较熟悉,又是一个循环,然后调用nativePollOnce(ptr,nextPollTimeoutMillis(),我们可以猜测阻塞和它相关,而上面的Binder.flushPendingCommands(),我们可以看看它的注释:

将当前线程中挂起的任何Binder命令刷新到内核驱动程序。这对于在执行可能会阻塞很长时间的操作之前进行调用非常有用,以确保已释放任何挂起的对象引用,以防止进程对对象的占用时间超过所需的时间。

关键在于,在这之后的代码可能会发生长时间的阻塞。 总之,想要知其所以然,我们需要看看nativePollOnce的源码究竟做了些什么,我们快速定位到源码的位置:

android_os_MessageQueue.cpp - OpenGrok cross reference for /frameworks/…

static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,189          jlong ptr, jint timeoutMillis) 
{
    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}

然后是pollOnce

void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {108      mPollEnv = env;
      mPollObj = pollObj;
 mLooper->pollOnce(timeoutMillis); 
      mPollObj = NULL;
      mPollEnv = NULL;
      if (mExceptionObj) {
          env->Throw(mExceptionObj);
          env->DeleteLocalRef(mExceptionObj);
          mExceptionObj = NULL;
      }
}

然后又落到Looper的pollOnce之上,但是区别在于此时的mLooper不再是Java对象了,而是C++对象:

int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {174      int result = 0;
    for (;;) {
        // 省略号
        result = pollInner(timeoutMillis); 
    }
}

你会发现, 底层的代码,也是循环:

Looper.cpp - OpenGrok cross reference for /system/core…

int Looper::pollInner(int timeoutMillis) {
    // ……
    int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
    // ……
}

所以,native层的Handler机制,实际上是使用epoll来进行驱动的。

你并不必非常详细地知道epoll的细节,你暂时只需要知道epoll能够阻塞IO,并且实现唤醒即可,正如手册中说的那样:event notification facility,即事件通知设施。

epoll有一个参数是timeoutMillis,如果有数据则返回;没有数据则sleep,如果超过这个时间没有消息,此时也会返回,所以在倒数第二层的Looper::pollOnece中,仍然是使用了一个循环,用于轮询,也就是:

for(;;){
  // 有消息->返回;
  // 没有消息 -> 睡眠一段时间,继续循环
}

所以,你会发现只要和IO相关的地方,基本上都离不开循环。如果循环是在用户空间,例如ActivityThread中的代码,通常是通过系统调用,陷入内核;内核也是利用循环来读取消息,但是内核代码通常有更高的权限,它可以去请求调度的执行,让当前等待IO的程序让出CPU,在IO完成之后,再去响应等等,此前用户空间的代码在内核空间完成处理之后就会完成阻塞。

3.MessageQueue的next()和enqueue()

如果你仔细看了前面的内容,你会发现,MessageQueue.next的阻塞是调用native,利用epoll机制实现的消息获取。但是如果你简单地看一看MessageQueue的enqueueMessage,即添加消息的代码,你会发现,添加事件的代码几乎全部都发生在Java层,但是添加完了之后,根据needWake标记位去调用nativeWake。也就是说,消息是在Java层存储的,但是阻塞 + 唤醒的机制是利用Native层的。 之前我们已经看过了enqueue,现在来看看nativeWake做了什么:

void Looper::wake() {
    ALOGD("%p ~ wake", this);
    uint64_t inc = 1;
    ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd. get (), &inc, sizeof(uint64_t)) );
    if (nWrite != sizeof(uint64_t)) {
        if (errno != EAGAIN) {
            LOG_ALWAYS_FATAL("Could not write wake signal to fd %d (returned %zd): %s",
                 mWakeEventFd.get(), nWrite, strerror(errno));
        }
    }
}

就是往需要wake的fd中写入内容,然后唤醒对应的next,而在enqueue中会等待epoll_wait,epoll_wait返回时,阻塞完成,则继续向下执行,也就是说在IO完成之后,最终会根据不同的eventCount和eventItem去调用awoken方法,

int Looper::pollInner(int timeoutMillis) {#if DEBUG_POLL_AND_WAKEALOGD("%p ~ pollOnce - waiting: timeoutMillis=%d", this, timeoutMillis);
    // ……
    int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
    
    // ……
    for (int i = 0; i < eventCount; i++) {
        int fd = eventItems[i].data.fd;
        uint32_t epollEvents = eventItems[i].events;
        if (fd == mWakeEventFd.get()) {
            if (epollEvents & EPOLLIN) {
                awoken(); 
            } else {
                ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);
            }
        } else {
            // …… 
        }
    // ……
    return result;
}

而awoken的实现:

void Looper::awoken() {
    uint64_t counter;
    TEMP_FAILURE_RETRY(read(mWakeEventFd.get(), &counter, sizeof(uint64_t)));
}

显然,MessageQueue的休眠 + 唤醒就是一组搭配上epoll机制进行唤醒的,读空阻塞 + 写后唤醒,真正的事件是从上层Java代码里下发的,在Java中的MessageQueue里面进行enqueue,写完后调用native进行唤醒正在休眠的线程。

当然,当Looper对应的MessageQueue中没有消息导致阻塞了才需要唤醒,如果此刻的MessageQueue特别忙压根没时间休息也没必要去唤醒。

读到这里,你应该对MessageQueue和Looper有了新的的认识,无论是取消息时会在epoll机制的作用下阻塞,但是一旦有生产者向消息队列中发送消息,就会通过native层,唤醒被阻塞的线程。

流程图 (2).jpg

4. 发送消息的句柄:Handler

所谓的句柄,就是一个引用,更通俗地来说,就是一个“抓手”,让我们随时能够拿到发送消息的这么一个结构。

Handler的使用无非就是以下几个步骤:

  1. 声明Handler,最重要的是设置目标Looper,也就是确定目标循环,最终的目的是确定通过本Handler发送的消息所有执行的目标线程

  2. 传递Handler引用。

  3. 使用Handler引用,调用post方法发送消息。

代码中,散落在各处的Handler绝大部分都指向主线程,比如Activity基类是自带一个Handler的,如果Handler的构造参数传空,会默认调用线程的静态方法Looper.myLoop获取Looper实例,最终就是一个指向Main线程Looper的Handler:

@UnsupportedAppUsage
final Handler mHandler = new Handler();

Activity中的runOnUiThread便借助了这个Handler:

public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        action.run();
    }
}

它帮我们将一个Handler抛向UI线程(主线程)执行,实质是抛向主线程的Looper,最终会在Looper循环中不断地将消息取出,之后执行或者压入队列。

对于线程来说,Looper也只能有一个,MessageQueue和线程是锁死的,只能一对一,而Looper和线程也是锁死的,所以Looper在初始化的时候就生成了自己的MessageQueue,MessageQueue实际上是Looper实例的一个变量。

具体是如何做到和线程绑定的,可以看看ThreadLocal相关的内容。在这里你只需要知道,通过Looper.myLooper()取出的一定是调用Looper.myLooper()对应的当前线程的Looper即可。

显然,我们通过Handler可以发送Message,但是Handler不光要管发送,还要管处理。前面提到了,Message的分类大致上可以分为两类:1是数据类,2是Runnable类。

  • 数据类主要传递一些数据,交给Handler的handleMessage回调方法去处理;

  • Runnable则主要能够传递一些动态的代码,让其在Handler指定的线程处理;

4.1 dispatchMessage方法:立即执行(同步执行)

二者使用的方法也不同,如果你使用Handler.dispatchMessage来派发一个方法,那么处理的先后顺序就是:

  1. msg的回调,即立即执行Runnable。
  2. 立即执行通过代码动态设置Handler的Callback。UnsupportedAppUsage
  3. 立即执行handleMessage的处理方案;

注意,通过dispatchMessage传递的消息是顺序执行的,将不会被压入MessageQueue队列中执行,不经过MessageQueue、Looper这一套流程就意味着不会被目标线程「捞起来」执行,所以dispatchMessage方法都是在调用线程中执行的,并不会切换线程。真正切换线程的时机在于消息进入队列之后,被目标线程的Looper取出这个动作,以实现工作线程的切换。换言之,如果希望切线程你必须使用post方法来发送Message。

4.2 post方法:压入队列执行(异步执行)

只有post系列的方法才会压入队列执行,但是最终Looper从队列取出消息之后,仍然调用的是dispatchMessage()方法去立即执行,post系列的方法自带一定的延迟(不多,几毫秒到几百毫秒都有可能,视主线程的任务数量、处理时长而定)。

  1. 既然直接调用dispatchMessage()是同步的,没有延迟,那么如果子线程调用主线程的Handler.dispatchMessage的时候,主线程已经有一个Message在执行了怎么办?

上面提到了,dispatchMessage并不会切线程,子线程调用dispatchMessage,那么无论是Message.callback还是Handler的handleMessage方法,都仍是在子线程执行的,并不会影响主线程的执行。

  1. **为什么Handler不直接去处理,而是要压到一个队列里面?

Handler有多个,但是线程只有一个。

以主线程为例,对应到主线程的Handler实例也是非常多的,但是一个线程同一时间只能处理一段代码,如果多个Message发出的Runnable发生并发请求执行,一个线程是无法同时处理的,必然要这么个结构来做消息的存储,按顺序执行。所以Handler的发送消息(postMessage)和执行消息(callback)是分开的。

而线程对应的Looper、MessageQueue也只有一个。

这就会出现MessageQueue中有非常多Message,但是可能各个MessageQueue对应的Handler都不一样,但是他们之间通常绝大多数时候都是有序的,对应的主线程就在不断地执行Handler的回调方法,无一例外的,所有的代码最终都是在主线程上执行的。

image.png 如果你熟悉一些基于事件队列的单线程编程语言,比如大家熟悉的Flutter、JavaScript等等。它们正是这样实现所谓的异步操作的,也被叫做伪异步,例如Flutter,它异步的本质上是往事件队列的末端加入一个新的Future,虽然JavaScript、Flutter这类的语言只提供了一个线程(Flutter中对应的ISolate)给我们操作,但是又不能影响绘制,依赖的就是这样的伪异步操作。两个伪异步操作之间仍然有先后顺序,并不是完全并行的。

5.实践:利用ActivityThread下的Handler退出我们的App

首先,我们需要切到较低的Android版本,这里选择的是API23,即6.0,这样我们可以通过反射拿到被@UnsupportedAppUsage注解标记的变量,但是高版本上是行不通的,所以这并不能当成一种常规的手段在开发中使用。

  • 需要拿到ActivityThread下的Handler对象

但是ActivityThread的引用并不好获取,但是我们查找ActivityThread对象的创建时,我们可以看到:

ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);

if (sMainThreadHandler == null) {
    sMainThreadHandler = thread.getHandler();
}

@UnsupportedAppUsage
public Handler getHandler() {
    return mH;
}

即系统预置的这个Handler被挂在sMainThreadHandler之下,而thread.getHandler()中,对应的正是在ActivityThread对象中创建的主线程Handler对象:final H mH = new H();

  • 获取对应的MessageQueue

第一步中获取Handler的目的是此前提到的,使用Handler的第二步:获取Handler引用,通过引用发送Message,然后在Handler实现handleMessage的地方处理回调。

其实按理说我们只需要这样就可以完成这个实践了:

val message = Message.obtain()
message.what = 111; // public static final int EXIT_APPLICATION        = 111;
mInnerHandler.dispatchMessage(message)

但是直接跑起来是直接会抛出异常的,因为主线程是不允许被退出的:

Main thread not allowed to quit.

我们跟踪H类中EXIT_APPLICATION的实现,我们可以发现这个限制是被MessageQueue的mQuitAllowed变量控制,所以我们还需要反射修改MessageQueue的mQuitAllowed变量值为true。

因为线程、MessageQueue、Looper的关系是一一对应的,所以我们可以直接使用上面获取到的Handler拿到Looper再拿到MessageQuque,也可以直接使用Looper.getMainLooper()获取主线程的Looper然后再去拿MessageQueue,结果都是同一个MessageQueue对象,然后再反射调用,修改mQuitAllowed的值为true即可:

val mMessageQueue: MessageQueue =
    mInnerHandler.looper.fastGetDeclaredFieldDynamic("mQueue")
        ?: return@setOnClickListener
// 反射设置主线程MessageQueue允许退出
mMessageQueue.fastSetDeclaredFieldsDynamic("mQuitAllowed", true)
        
// 或者
Looper.getMainLooper().fastGetDeclaredFieldDynamic<MessageQueue>("mQueue")
    ?.fastSetDeclaredFieldsDynamic("mQuitAllowed", true)

其中反射相关的代码需要自己去实现,完整代码大概长这样:

// 获取activityThread中,静态的sMainThreadHandler实例
val activityThread = classLoader.loadClass("android.app.ActivityThread")
val mInnerHandler: Handler =
    activityThread.fastGetDeclaredFieldStatic("sMainThreadHandler")
        ?: return@setOnClickListener
// 反射获取InnerHandler对应的Looper的MessageQueue
val mMessageQueue: MessageQueue =
    mInnerHandler.looper.fastGetDeclaredFieldDynamic("mQueue")
        ?: return@setOnClickListener
// 反射设置主线程MessageQueue允许退出
mMessageQueue.fastSetDeclaredFieldsDynamic("mQuitAllowed", true)
val message = Message.obtain()
message.what = 111; // public static final int EXIT_APPLICATION        = 111;
mInnerHandler.dispatchMessage(message)

执行上述代码,实践是成功了,虽然最后程序还是崩溃了,但是是因为主线程循环被退出了,最后抛出了一个异常:

Main thread loop unexpectedly exited

因为ActivityThread.java中main函数就是这样写的,Looper.loop()完成之后自然而然就走到throw new Execption当中。

public static void main(String[] args) {
    // 省略号
    Looper.prepareMainLooper();
    // 省略号
    Looper.loop(); 
    // 省略号
    throw new RuntimeException("Main thread loop unexpectedly exited");
}

如果你在主线程的异常兜底里面将这个异常兜住会发生什么?

Thread.currentThread().setUncaughtExceptionHandler { t, e ->
    e.printStackTrace()
}

这样一来程序不会崩溃退出,但是界面就卡死在那里了,UI虽然还在,但是已经Debug中已经无法找到对应的程序了:

如果此时你再去看函数的堆栈,你会发现此前的两个main函数已经消失了,只剩下catch异常的栈帧了。

发送消息之前的栈帧

发送消息之后的栈帧