结合此前的内容,我们知道,ActivityThread的main函数中,实际上是做了几件事情:
- 初始化Looper
- Looper开始循环
- messageQueue.next()返回或者阻塞
- 处理消息
3 ~ 4之间是一个循环,一旦退出这个Looper的循环,那么意味着App也就退出了。
其中messageQueue.next()阻塞,阻塞的正是线程,准确的说应该是用户线程。当ActivityThread对应的主线程执行到这一行时,如果消息队列中没有消息,主线程就会被阻塞在此处,后续如果有新的消息到来了,就会唤醒,继续走4的处理消息的步骤,然后又进入3开始阻塞或者处理下一个消息。
显然,messageQueue.next()是IO,并且是阻塞式IO,它会卡住我们程序的执行。
我们知道,线程是处理机调度的基本单位,而可持续交互程序的本质,是利用一个循环不断地去读取消息,然后处理消息,换句话说,是一个线程在不断地去执行一个循环,读取消息,处理消息,
Looper也很纯粹,它主要的职责就是实现这么个循环,然后不断地去取消息,执行消息,但是它却是我们主线程的核心。我们的App的运行时,无论是从系统还是App里面,都会有源源不断的消息流入。
今天的内容,则主要聚焦于一次Looper将会发生什么:
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层,唤醒被阻塞的线程。
4. 发送消息的句柄:Handler
所谓的句柄,就是一个引用,更通俗地来说,就是一个“抓手”,让我们随时能够拿到发送消息的这么一个结构。
Handler的使用无非就是以下几个步骤:
-
声明Handler,最重要的是设置目标Looper,也就是确定目标循环,最终的目的是确定通过本Handler发送的消息所有执行的目标线程。
-
传递Handler引用。
-
使用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来派发一个方法,那么处理的先后顺序就是:
- msg的回调,即立即执行Runnable。
- 立即执行通过代码动态设置Handler的Callback。
UnsupportedAppUsage - 立即执行handleMessage的处理方案;
注意,通过dispatchMessage传递的消息是顺序执行的,将不会被压入MessageQueue队列中执行,不经过MessageQueue、Looper这一套流程就意味着不会被目标线程「捞起来」执行,所以dispatchMessage方法都是在调用线程中执行的,并不会切换线程。真正切换线程的时机在于消息进入队列之后,被目标线程的Looper取出这个动作,以实现工作线程的切换。换言之,如果希望切线程你必须使用post方法来发送Message。
4.2 post方法:压入队列执行(异步执行)
只有post系列的方法才会压入队列执行,但是最终Looper从队列取出消息之后,仍然调用的是dispatchMessage()方法去立即执行,post系列的方法自带一定的延迟(不多,几毫秒到几百毫秒都有可能,视主线程的任务数量、处理时长而定)。
- 既然直接调用dispatchMessage()是同步的,没有延迟,那么如果子线程调用主线程的Handler.dispatchMessage的时候,主线程已经有一个Message在执行了怎么办?
上面提到了,dispatchMessage并不会切线程,子线程调用dispatchMessage,那么无论是Message.callback还是Handler的handleMessage方法,都仍是在子线程执行的,并不会影响主线程的执行。
- **为什么Handler不直接去处理,而是要压到一个队列里面?
Handler有多个,但是线程只有一个。
以主线程为例,对应到主线程的Handler实例也是非常多的,但是一个线程同一时间只能处理一段代码,如果多个Message发出的Runnable发生并发请求执行,一个线程是无法同时处理的,必然要这么个结构来做消息的存储,按顺序执行。所以Handler的发送消息(postMessage)和执行消息(callback)是分开的。
而线程对应的Looper、MessageQueue也只有一个。
这就会出现MessageQueue中有非常多Message,但是可能各个MessageQueue对应的Handler都不一样,但是他们之间通常绝大多数时候都是有序的,对应的主线程就在不断地执行Handler的回调方法,无一例外的,所有的代码最终都是在主线程上执行的。
如果你熟悉一些基于事件队列的单线程编程语言,比如大家熟悉的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异常的栈帧了。