6.4 Android的消息机制
深入解析Android 5.0系统
消息驱动是一种进程或线程的运行模式。内部、外部的各种事件都可以放到消息队列中按序处理。这种模式特别适合处理大量的交互事件。Android应用的UI线程,同样采用了消息驱动模式,所有外部来的按键消息、触屏消息、各种系统Intent、广播等都会转化为内部的消息,然后在主线程中分发处理。
6.4.1 消息模型
现在的操作系统普遍采用消息驱动模式。Windows操作系统就是典型的消息驱动类型。但是,Android的消息处理机制和Windows的消息处理机制不太相同。下面用图6.1来分别表示Windows和Android的消息机制模型。
在Windows的消息处理模型中,存在一个系统消息队列,这个队列是整个进程的核心,几乎所有动作都要转换成消息,然后放到这个队列中,消息的处理只能在主线程来完成。
Android的消息处理则不一样。Android没有全局的消息队列,消息队列是和某个线程关联在一起的。每个线程最多有一个消息队列,消息的取出和处理在线程中完成。
比较而言,Windows的消息模型较简单,消息的发送也简单方便。Android的消息模型相对复杂很多,使用前必须为线程构造消息队列,发送消息也必须先得到消息队列的Handler对象。但是Windows的全局消息队列很容易成为程序的瓶颈,如果某个消息处理不能及时完成,整个进程都会挂起,而且因为是全局队列,所以线程间频繁地同步也会带来更大系统开销。Android的消息机制则避免了这种情况,消息队列在各个线程中,线程内部的消息发送完全没有额外的开销。在程序设计时,可以根据需要在合适的线程中设置消息队列,线程内部的消息在本线程的消息队列中循环,除非必要才向另外的线程发送消息,因此,最大程度地减少了因线程同步带来的系统开销。Android的消息处理方式非常灵活,消息处理的代码可以集中在一起,也可以分散在各个Handler对象中,甚至每条Message都能有自己的消息处理方法。
Android中与消息机制相关的类主要是Looper、Handler、Message和MessageQueue。
1.Looper类
Looper对象是线程的消息循环处理器,每个线程只能有一个Looper对象。Looper内部有一个消息队列MessageQueue,所有线程的消息都存放在这个队列中。新创建一个线程时,系统并不会马上为这个线程创建一个Looper对象,需要程序自己创建。Android在启动时,为主线程(UI线程)创建了一个Looper对象。
2.Handler类
Handler对象是Message的接收者和处理者。用户使用Handler对象把Message添加到消息队列中;同时通过Handler的回调方法handleMessage()来对消息队列中的Message进行处理。Handler对象在构造时和某个Looper对象关联在一起。Handler和Looper是多对一的关系,多个Handler对象可以和一个Looper对象关联起来,反之则不行。
3.Message类
Message是消息的载体。Message设计成为Parcelable类的派生类,这表明Message可以通过binder来跨进程发送。
6.4.2 理解Looper类
Looper类的主要成员变量和方法如下:
public final class Looper {
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static Looper sMainLooper;
final MessageQueue mQueue;
final Thread mThread;
private static void prepare(boolean quitAllowed){...}
public static void prepareMainLooper(){...}
public static Looper getMainLooper(){...}
public static void loop(){...}
}
每个线程只能有一个Looper类的实例对象,Looper类的实例必须通过方法prepare()创建。prepare()方法会创建一个Looper的对象,并把它保存在静态变量sThreadLocal中。一个线程中多次调用prepare()方法将会抛出异常。
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对象后,通过方法myLooper()可以获得Looper对象。
public static Looper myLooper() {
return sThreadLocal.get();
}
静态变量sThreadLocal的类型是模板类ThreadLocal,它通过将需要保存的对象和线程id关联在一起的方式实现了线程本地存储的功能,这样放入sThreadLocal对象中的Looper对象就和创建它的线程关联在一起了。
Looper类的getMainLooper()方法将返回主线程的Looper对象。Android应用启动时会创建主线程,同时会创建一个Looper对象和主线程相关联。但是创建主线程的代码在Framework中,应用层不能直接取得主线程的Looper对象。因此,Android将获得主线程Looper对象的方法放在了Looper类中,如下所示:
public static Looper getMainLooper() {
synchronized (Looper.class) {
return sMainLooper;
}
}
有了Looper类的对象后,可以调用Looper类的loop()方法来进入消息循环。loop()是一个静态的方法,它里面有一个无限for循环,对loop()方法的调用一般在线程的run()方法中。Looper类的典型用法如下:
class myThread extends Thread
{
public void run() {
Looper.prepare();
Looper.loop();
}
}
loop()方法的主要作用是分发消息队列中的消息,函数的代码如下:
public static void loop() {
final Looper me = myLooper();
......
final MessageQueue queue = me.mQueue;
......
for (;;) {
Message msg = queue.next(); // 取一条消息,没有消息会阻塞
if (msg == null) {
return; // msg等于null表示接到了退出的请求
}
......
msg.target.dispatchMessage(msg); // 分发消息
......
msg.recycleUnchecked ();
}
}
loop()方法会循环从MessagQueue队列中取出消息,然后把消息分发出去。消息分发是通过Message对象中的target变量完成了,这个变量的类型是Handler,前面已经介绍了,一个Looper对象可以对应多个Handler对象,线程的Looper对象并不是只和一个Handler对象相关联。
Message是消息的载体,发送者把需要传递的消息放在Message对象中,Message对象创建的时候就需要指定它的处理对象。Handler主要用来处理消息,一个Handler对象可以处理多种消息。
6.4.3 理解Handler类
Handler主要负责消息的发送和处理。在一个线程中可以只用一个Handler对象来处理所有消息,也可以使用多个。构造一个Handler对象需要两个参数,线程的Looper对象和消息的处理函数。对于Handler对象而言,参数Looper是必须的,因为它只能给某个线程的Looper对象发送消息,如果构造方法不指定特定的Looper对象,它会使用当前线程的Looper对象。但是参数callback并不是必须的,应用程序可以通过这个callback方法来实现对消息的集中处理。也可以把处理消息的callback方法直接放在消息对象中。
Handler类是消息框架的一部分,消息的定义和响应还需要在应用层的代码中完成,在这一点上Android设计的非常灵活。传统的消息模型中,某个线程能处理的消息的种类必须预先定义好,使用者只能使用它们来给某个线程发送消息。但是Android把消息的定义和处理完全独立出来了,线程只是提供了一个消息队列和消息响应代码的运行环境。例如,Android主线程的实现都是在Framework中,但是我们可以使用下面的方法来构造一个带有callback方法的消息发送给主线程。
public static Message obtain(Handler h, Runnable callback)
这样,这个callback方法将在主线程中执行。
这也是面向过程和面向对象设计的区别。从面向过程的设计思路出发,我们需要给每种Message定义不同的消息Id,然后在一个函数里用switch语句来处理它们。而在面向对象的概念中,消息对象本身就是独一无二的,因此,不需要使用消息Id来相互区分,定义一种消息时只需要关心最核心的东西:消息的响应代码。同样,线程也独立出来了,负责消息处理的线程此时更像一个平台,各种消息都可以使用它,而不再局限于处理一些预定义的消息。
如果使用消息的目的只是希望处理函数能在一个不同的线程中执行,或者延时一段时间执行,而并不关心它在哪个线程中执行,我们可以让一个公共线程来承担执行的任务,而不用再为每种消息来定义一个线程类。
Android的消息框架中对上面介绍的两种方式都支持,只要明白了设计思路,看懂这些接口就很容易了。
Handler类的另外一个功能是发送消息。Handler类用来发送消息的接口也是很丰富的。从总体来讲,Handler类的消息发送接口分成两大类:一类是“send”类,另一类是“post”类。
1.“send”类的接口如下
public final boolean sendMessage(Message msg)
public final boolean sendEmptyMessage(int what)
public final boolean sendEmptyMessageDelayed(int what, long delayMillis)
public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis)
public final boolean sendMessageDelayed(Message msg, long delayMillis)
public boolean sendMessageAtTime(Message msg, long uptimeMillis)
public final boolean sendMessageAtFrontOfQueue(Message msg)
所谓的“(send)发送”消息只是把消息插入到了消息队列中,同时指定消息处理的时间。如果指定的时间为0,表示要立即处理,MessageQueue会把这条消息插到队列的头部。MessageQueue类中接收消息的接口如下:
boolean enqueueMessage(Message msg, long when)
enqueueMessage()除了消息参数外,只有一个时间参数,因此,Handler类里面发送消息的接口虽然多,但是都是在时间上玩花样,让应用方便使用而已。“send”类方法总结如下:
如果希望马上处理,但是不打算插队,使用sendMessage();
如果非常紧急,希望尽快处理,使用sendMessageAtFrontOfQueue();
如果希望延时一段时间处理,使用sendMessageDelayed();
如果希望在指定时间处理,使用sendMessageAtTime()。
如果定义的消息只有消息Id,不用附加参数,使用sendEmptyMessage()方法将会更加方便。
2.“Post”类型的方法定义如下:
public final boolean post(Runnable r)
{
return sendMessageDelayed(getPostMessage(r), 0);
}
public final boolean postAtTime(Runnable r, long uptimeMillis)
{
return sendMessageAtTime(getPostMessage(r), uptimeMillis);
}
public final boolean postAtTime(Runnable r, Object token, long uptimeMillis)
{
return sendMessageAtTime(getPostMessage(r, token), uptimeMillis);
}
public final boolean postDelayed(Runnable r, long delayMillis)
{
return sendMessageDelayed(getPostMessage(r), delayMillis);
}
public final boolean postAtFrontOfQueue(Runnable r)
{
return sendMessageAtFrontOfQueue(getPostMessage(r));
}
从代码的实现上看,这些“Post”方法也是在使用“send”类的方法在发送消息,只是它们的参数要求是Runnable类的对象,然后在方法中调用getPostMessage()获取了一个Message对象来发送。
看到这里就很好理解了:“post”类型的方法用来发送带有处理方法的消息,“send”类型的方法则用于发送传统的带有消息Id的消息。
最后,看看Handler类的dispatchMessage()方法,代码如下:
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
从dispatchMessage()方法的代码来看,消息会优先分发给消息中自带的回调方法;否则,如果Handler定义了回调方法,先调用这个方法处理,如果这个方法没有处理,还会调用Handler自己的HandleMessage()方法,这个方法缺省不做任何事情。如果实现了一个Handler类的继承类,也可以通过重载这个HandleMessage()方法来达到处理消息的目的。
6.4.4 消息的同步——Message类的setAsynchronous()方法
在Android的Message类中,有一个setAsynchronous(boolean async)方法,从字面上理解,它给一条消息加上异步标志。难道消息不都是异步完成的吗?我们使用这个方法会把消息的执行模式变成同步的吗,还是不调用它,消息就是以同步的方式在执行?
在回答上面的问题前,我们先谈论编程中经常需要处理的一种情况:收到一条广播,或者在Activity刚启动时,可能需要完成一个比较耗时的操作,但是,如果消息处理函数长时间不返回,很容易发生ANR。常用的解决办法是发送一个消息,然后在消息处理函数中完成这个耗时操作。如果说发送消息有同步和异步之分,那么我们使用这种方法来解决ANR的问题是否在任何情况下都有效?
要理解setAsynchronous()方法的作用,需要仔细研究MessageQueue类的代码,在此之前,我们先说说这个问题的答案。如果我们希望用消息来避免ANR,使用普通消息就可以解决问题。因为从代码上看,所有消息都会先放到队列中,然后再在本线程的处理函数中处理。这样的机制能解决我们的问题,让接收广播或Activity的函数很快返回,避免ANR。
那么setAsynchronous()方法的作用又是什么呢? 在MessageQueue类中有一个方法enqueueSyncBarrier(long when),调用这个方法会在消息队列中插入一条没有Handler对象的消息,这条不带Handler对象的消息称为“SyncBarrier”,MessageQueue将暂停处理队列中“SyncBarrier”以后的消息。这好比一群人在排队买票,有人过来在队列中放了一块牌子:“从这开始,暂停销售”。但是这时如果还有消息需要处理,可以使用setAsynchronous()方法来给一条消息做上标志,MessageQueue检测到消息中的标志后,会正常处理这条消息,但是别的消息还是暂停处理,直到调用removeSyncBarrier()方法移走了挡在消息队列前面的“SyncBarrier”。
6.4.5 分析MessageQueue类
研究MessageQueue时,如果带着问题去分析,可以理解地更加透彻。先考虑下面的问题。
不同线程间发送消息,有同步保护吗?如何实现的?
消息队列不同于普通队列,每条消息都有时间,如何实现按时间分发消息?
没有消息时消息队列会挂起吗?来了新的消息又是如何唤醒的。
消息队列是如何组织的?新消息是如何插入的,都在队尾吗?
“SyncBarrier”是如何工作的?
下面我们将从对象构造、消息处理、发送消息3个方面来分析MessageQueue。
1.MessageQueue的构造
MessageQueue对象的构造是调用本地方法nativeInit()完成的。nativeInit()方法对应的JNI函数是native层的android_os_MessageQueue_nativeInit()函数,代码如下:
static jint android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
......
}
android_os_MessageQueue_nativeInit()函数最主要的功能是新创建了一个本地的Native MessageQueue对象。NativeMessageQueue的构造函数如下:
NativeMessageQueue::NativeMessageQueue() : mInCallback(false), mExceptionObj(NULL) {
mLooper = Looper::getForThread();
if (mLooper == NULL) {
mLooper = new Looper(false);
Looper::setForThread(mLooper);
}
}
NativeMessageQueue的构造函数只是创建一个本地的Looper类对象。从NativeMessageQueue类的代码看,它本质上是一个代理类。它把Java层的调用转变为对native层Looper类的函数调用,native层的Looper类才是关键所在。
native层的Looper类也实现了一套完整的消息处理机制。但是Java层的Looper类和native层的Looper类并没有直接关系。MessageQueue虽然使用了Native层的Looper类,但也只使用了它的等待/唤醒机制,其余的如消息队列的实现还是在Java层。因此,如果再看到MessageQueue有中从Java到native层之间的调用,可以略去中间过程,直接分析native层Looper类中的函数。
下面是本地Looper类的构造函数的代码:
Looper::Looper(bool allowNonCallbacks) :
mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
int wakeFds[2];
int result = pipe(wakeFds); //创建匿名管道 .
mWakeReadPipeFd = wakeFds[0];
mWakeWritePipeFd = wakeFds[1];
//把读写管道都设成非阻塞式
result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);
result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK);
mIdling = false;
mEpollFd = epoll_create(EPOLL_SIZE_HINT); //创建和初始化epoll对象
struct epoll_event eventItem;
memset(& eventItem, 0, sizeof(epoll_event));
eventItem.events = EPOLLIN;
eventItem.data.fd = mWakeReadPipeFd;
//把“读管道”加入到epoll监听中
result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, & eventItem);
}
在上面的代码中,Looper类的构造函数做了两件事情,一是创建了管道,二是使用epoll来监听读管道。epoll的作用是监听管道上的数据,管道则用于线程间通信。
2.MessageQueue中的消息处理过程
MessageQueue中的消息循环在方法next()中,为了能让读者更好地理解next()方法,笔者在代码中做了比较详细的注释,如下所示:
Message next() {
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
// 调用本地方法等待nextPollTimeoutMillis毫秒, -1表示要永远阻塞
nativePollOnce(ptr, nextPollTimeoutMillis);
// 这里使用了针对this对象同步,因此只要next方法还没退出
// 再调用本对象的任何方法都将导致调用线程挂起
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages; // mMessages指向队列头
if (msg != null && msg.target == null) {
// “SyncBarrier”的标志就是其消息的target为null
// 如果队列的第一条消息是“SyncBarrier”,忽略普通消息,查找第一条“异步消息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) { // 找到了第一条消息
if (now < msg.when) {
//如果还没有到处理这条消息的时间,计算需要等待的时长
nextPollTimeoutMillis = (int) Math.min(msg.when - now,
Integer.MAX_VALUE);
} else {
mBlocked = false; // 取消“阻塞”标志
// 将要处理的消息从队列中断开
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
//返回消息
if (false) Log.v("MessageQueue", "Returning message: " + msg);
return msg;
}
} else { // 表示队列中没有现在必须处理的消息了
nextPollTimeoutMillis = -1;
}
if (mQuitting) { // 如果退出标志设置了,则销毁native对象,然后返回
dispose();
return null;
}
// 第一次进入idle会检查是否安装了idle handler
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
mBlocked = true;
continue; // 没有安装idle handler则继续for循环
}
// idle handler放入数组mPendingIdleHandlers中
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(
pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
// 处理所有idle handler,如果回调结果为false,表示不再继续处理
// 则 从idle handler的列表中移除该handler
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null;
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf("MessageQueue", "IdleHandler threw exception", t);
}
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
pendingIdleHandlerCount = 0;
//如果有idle handler存在,把nextPollTimeoutMillis设为0,让循环继续,而不是阻塞
nextPollTimeoutMillis = 0;
}
}
下面总结next()方法的功能。
(1)检查队列中的第一条消息是否是“SyncBarrier”消息,如果是,寻找队列中的第一条“异步消息”,找到后设为当前处理的消息;如果不是“SyncBarrier”消息,把第一条消息设为当前处理的消息。
(2)如果当前消息不为NULL,检查该消息的处理时间是否已经超时,如果没有,计算等待时间。如果处理的时间到了,next()方法将返回该消息并退出。
(3)如果当前消息为NULL,表示队列中没有可以处理的消息,设置等待时间为-1(表示永久等待)。
(4)检查队列中的退出标志,如果检测到退出标志则销毁native层中创建的对象,然后next()方法退出。
(5)检查是否已经安装了处理idle状态的回调函数。如果没有安装,回到循环的开始,调用nativePollOnce()方法挂起线程并等待新消息的到来。
(6)如果安装了idle状态的回调函数则调用所有回调函数,同时把epoll的等待时间设为0,这表明在安装了idle处理函数的情况下消息队列的循环处理是不会阻塞的, 这样idle处理函数将会不停地被调用,直到处理函数的值为false。
next()方法中会调用nativePollOnce()方法。nativePollOnce()方法最后通过调用epoll_wait()来执行等待操作,如下面代码所示:
epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
上面代码中的mEpoolFd对象已经在Looper的构造函数里加入了对“读管道”的监听。如果wait的时间到,或者向“写管道”中写数据,epoll_wait()就会返回,阻塞结束。
3.向MessageQueue发消息
向MessageQueue发消息使用的是enqueueMessage方法,代码如下:
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) { // 如果消息的target为NULL,抛出异常
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) { // 如果加入的是正在处理的消息对象,抛出异常
throw new IllegalArgumentException(msg + " This message is already in use.");
}
synchronized (this) { // 使用this对象来同步
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w("MessageQueue", e.getMessage(), e);
msg.recycle();
return false;
}
msg.markInUse();
msg.when = when;
Message p = mMessages; // p指向消息队列头
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// 队列中没有消息,或者消息需要插到队列头
msg.next = p;
mMessages = msg; // 把消息插到队列头
needWake = mBlocked; // 这时如果处理线程阻塞了,则需要唤醒
} else {
// 如果设置了“SyncBarrier”,只有插入了“异步消息”才需要唤醒
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
// 比较消息时间,找到合适插入消息的地方
if (p == null || when < p.when) {
break;
}
// 如果已经有一条“异步消息”在队列了,而且在本条消息前处理,则不需要唤醒
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
if (needWake) { // 如果需要唤醒,则唤醒线程
nativeWake(mPtr);
}
}
return true;
}
enqueueMessage()方法插入消息时根据时间来排序,时间早的插在前面。
消息队列的组织也非常简单,利用了Message类中的next指针形成一个从头指向尾的单向链表。插入时计算是否需要唤醒处理线程。enqueueMessage()方法会尽量地避免唤醒处理线程,只有插入了一条马上要处理的消息,或者在暂停处理消息的情况下,又插入了“异步消息”的情况下才会去唤醒处理线程。
为什么其余情况下插入消息不需要唤醒处理线程呢?其余的情况都是把消息放到队列的中部或尾部(时间未到)。如果前面还有消息没处理,这条消息就更不着急去处理了。
最后,让我们看看nativeWake()如何唤醒处理线程。nativeWake()最终会调用到native层的Looper类的wake()方法,代码如下:
void Looper::wake() {
ssize_t nWrite;
do {
nWrite = write(mWakeWritePipeFd, "W", 1);
} while (nWrite == -1 && errno == EINTR);
......
}
从上面的代码可以看到,wake()方法通过向管道中写入数据来唤醒消息处理线程。处理线程通过epoll监听管道上的数据,一旦有数据到来,线程就会被唤醒,next()方法会继续处理消息。
分析到这里,应该已经能够回答本节前面提出的问题了。而且从代码中我们也了解到,通过安装idle handler可以得到消息队列进入idle的通知。因此,一旦消息队列为空,应用就可以运行一些优先级较低但是耗时的代码。这些代码将在不影响程序消息处理的情况下,利用空闲时间来运行。``