在这篇文章中,我们将会讨论 Android 的消息机制。提到 Handler,有过一些 Android 开发经验的都应该很清楚它的作用,通常我们使用它来通知主线程更新 UI。但是 Handler 需要底层的 MessageQueue 和 Looper 来支持才能运作。这篇文章中,我们将会讨论它们三个之间的关系以及实现原理。
在这篇文章中,因为涉及线程方面的东西,所以就避不开 ThreadLocal。笔者在之前的文章中有分析过该 API 的作用,你可以参考笔者的这篇文章来学习下它的作用和原理,本文中我们就不再专门讲解:《Java 并发编程:ThreadLocal 的使用及其源码实现》。
1、Handler 的作用
通常,当我们在非主线程当中做了异步的操作之后使用 Handler 来在主线程当中更新 UI。之所以这么设计无非就是因为 Android 中的 View 不是线程安全的。之所以将 View 设计成非线程安全的,是因为:1).对 View 进行加锁之后会增加控件使用的复杂度;2).加锁之后会降低控件执行的效率。但 Handler 并非只能用来在主线程当中更新 UI,确切来说它有两个作用:
- 任务调度:即通过 post() 和 send() 等方法来指定某个任务在某个时间执行;
- 线程切换:你也许用过 RxJava,但如果在 Android 中使用的话还要配合 RxAndroid,而这里的 RxAndroid 内部就使用 Handler 来实现线程切换。
下文中,我们就来分别看一下它的这两个功能的作用和原理。
1.1 任务调度
使用 Hanlder 可以让一个任务在某个时间点执行或者等待某段时间之后执行。Handler 为此提供了许多方法,从方法的命名上,我们可以将其分成 post() 和 sned() 两类方法。post() 类的用来指定某个 Runnable 在某个时间点执行,send() 类的用来指定某个 Message 在某个时间点执行。
这里的 Message
是 Android 中定义的一个类。它内部有多个字段,比如 what
、arg1
、arg2
、replyTo
和 sendingUid
来帮助我们指定该消息的内容和对象。同时, Message
还实现了 Parcelable
接口,这表明它可以被用来跨进程传输。此外,它内部还定义了一个 Message
类型的 next
字段,这表明 Message
可以被用作链表的结点。实际上 MessageQueue 里面只存放了一个 mMessage
,即链表的头结点。所以,MessageQueue
内部的消息队列,本质上是一个单链表,每个链表的结点就是 Message
。
当调用 post() 类型的方法来调度某个 Runnable 的时候,首先会将其包装成一个 Message,然后再使用 send() 类的方法进行任务分发。所以,不论是 post() 类的方法还是 send() 类的方法,最终都会使用 Handler
的 sendMessageAtTime()
方法来将其加入到队列中:
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
// ... 无关代码
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
使用 Handler 进行任务调度是非常简单的。下面的代码就实现了让一个 Runnable 在 500ms 之后执行的逻辑:
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// do something
}
}, 500);
上面的任务执行方式在主线程中执行不会出现任何问题,如果你在非主线程中执行的话就可能会出现异常。原因我们后面会讲解。
既然每个 Runnable 被 post() 发送之后还要被包装成 Message,那么 Message 的意义何在呢?
Runnable 被包装的过程依赖于 Handler 内部的 getPostMessage()
方法。下面是该方法的定义:
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
可见,我们的 Runnable 会被赋值给 Message 的 callback。这种类型的消息无法做更详细的处理。就是说,我们无法利用消息的 what
、arg1
等字段(本身我们也没有设置这些字段)。如果我们希望使用 Message 的这些字段信息,就需要:
- 首先,要使用 send() 类型的方法来传递我们的 Message 给 Handler;
- 然后,我们的 Handler 要覆写 handleMessage() 方法,并在该方法中获取每个 Message 并根据其内部的信息依次处理。
下面的一个例子用来演示 send() 类型的方法。首先,我们要定义 Handler 并覆写其 handleMessage() 方法来处理消息:
private final static int SAY_HELLO = 1;
private static Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SAY_HELLO:
LogUtils.d("Hello!");
break;
}
}
};
然后,我们向该 Handler 发送消息:
Message message = Message.obtain(handler);
message.what = SAY_HELLO;
message.sendToTarget();
这样,我们的 Handler 接收到了消息并根据其 what
得知要 SAY_HELLO
,于是就打印出了日志信息。除了调用 Message 的 sendToTarget() 方法,我们还可以直接调用 handler 的 sendMessage() 方法(sendToTarget() 内部调用了 handler 的 sendMessage())。
1.2 线程切换
下面我们用了一份示例代码,它会先在主线程当中实例化一个 Handler,然后在某个方法中,我们开启了一个线程,并执行了某个任务。2 秒之后任务结束,我们来更新 UI。
// 在主线程中获取 Handler
private static Handler handler = new Handler();
// 更新UI,会将消息发送到主线程当中
new Thread(() -> {
try {
Thread.sleep(2000);
handler.post(() -> getBinding().tv.setText("主线程更新UI"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
上面之所以能够在主线程当中更新 UI,主要是因为我们的 Handler 是在主线程当中进行获取的。随后,我们调用 handler
的 post()
方法之后,传入的 Runnable
会被包装成 Message
,然后加入到主线程对应的消息队列中去,并由主线程对应的 Looper 获取到并执行。所以,就使得该 Runnable 的操作最终在主线程中完成。
也许你会觉得先在主线程当中获取到 Handler 然后再使用比较麻烦。别担心,我们还有另一种方式来解决这个问题。我们可以直接使用 Looper 的 getMainLooper()
方法来获取主线程对应的 Looper,然后使用它来实例化一个 Handler
并使用该 Handler 来处理消息:
new Handler(Looper.getMainLooper())
.post(() -> getBinding().tv.setText("主线程更新UI"));
本质上,当我们调用 Handler
的无参构造方法,或者说不指定 Looper 的构造方法的时候,会直接使用当前线程对应的 Looper 来实例化 Handler。每个线程对应的 Looper 存储在该线程的局部变量 ThreadLocal 里。当某个线程的局部变量里面没有 Looper 的时候就会抛出一个异常。所以,我们之前说直接使用 new
来实例化一个 Handler
的时候可能出错就是这个原因。
主线程对应的 Looper 会在 ActivityThread 的静态方法 main()
中被创建,它会调用 Looper 的 prepareMainLooper()
静态方法来创建主线程对应的 Looper。然后会调用 Looper 的 loop()
静态方法来开启 Looper 循环以不断处理消息。这里的 ActivityThread 用来处理应用进程中的活动和广播的请求,会在应用启动的时候调用。ActivityThread 内部定义了一个内部类 H