(译)Android 消息传递机制

目前为止,目前我们使用的线程通讯手段都是常规的 Java 应用所使用的. 管道、内存共享、可阻塞式队列可以应用到 Android 应用 ,但是会引起 UI 线程的问题, 因为这些手段都倾向于堵塞. UI 线程的响应性在使用带有阻塞行为的机制的时候是有风险的,因为这些机制通常可能会使线程挂起.

在 Android 中绝大部份线程之间的通讯是 UI 线程和 工作线程之间的通讯.因此,Android 平台定义了它自己的线程间通讯的方式. UI 线程能够以发送数据消息的方式让长时间运行的任务在后台线程被处理.消息传递机制是非阻塞式的 生产者-消费者模式,在这个模式下无论是生产者线程还是消费者线程,在消息处理的时候都不会被堵塞.

消息处理机制是 Android 平台的基石, API 位于 android.os 包下, 由下图 4-4 中所示的一系列类来实现功能.

android.os.Looper

     跟有且只有一个消费者线程关联的消息分发者

android.os.Handler

     消费线程消息处理者,生产者线程插入消息到队列的接口. 一个Looper能有多个相关联的Handler,但是它们都会被插入到相同的队列中.

android.os.MessageQueue

     在消费者线程被处理的无限的消息列表.每一个Looper以及线程至多有一个消息队列.

android.os.Message

     将被在消费者线程执行的消息

如图4-5所示消息由生产者线程插入并且由消费者线程处理.

  1. 插入: 生产者线程通过使用与消费者线程相关联的Handler往消息队列中插入消息
  2. 获取:运行在消息者线程的Looper按照序列的顺序从队列中获取消息
  3. 分发: handler负责在消费者线程处理消息.一个线程可能有多个Handler实例来处理消息,Looper确保了消息被分发到了正确的Handler.

图 4-5 ,多个生产者线程和单个消费者线程之间的消息传递机制.每个消息都指向队列中的下一个消息,图中以左向的箭头指示.

例子: 基本的消息传递

在我们进一步详细的分析组件之前,让我们先来看个基本的消息传递的例子来熟悉下:

以下代码实现可能是最常见的使用场景之一,用户按下屏幕上的一个能够触发长时间操作的按钮,例如网络操作.为了不拖慢UI的渲染,一个假的 代表长时间运行的操作doLongRunningOperation()方法 ,需要运行在工作线程.因此,需要仅仅需要对一个生产者线程(UI线程)以及一个工作线程(Looper线程)进行设置.

我们的代码建立了一个消息队列.它在click()回调中处理按钮点击事件,这是在UI线程执行的.在我们的实现中,回调插入了一条假的消息到消息队列当中.为简洁起见,布局和UI组件在我们的代码事例中被省略了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class LooperActivity extends Activity {

LooperThread mLooperThread;

private static class LooperThread extends Thread { //1

Handler mHandler;


@SuppressLint("HandlerLeak")
public void run() {
Looper.prepare();//2
mHandler = new Handler() {//3
@Override
public void handleMessage(@NonNull Message msg) {//4
if (msg.what == 0) {
doLongRunningOperation();
}
}
};
Looper.loop();//5
}

private void doLongRunningOperation() {
//Add long running operation here.
}

}

public void onCreate(Bundle saveInstanceState) {
super.onCreate(saveInstanceState);
mLooperThread = new LooperThread();//6
mLooperThread.start();
}

public void onClick(View v) {
if (mLooperThread.mHandler != null) {//7
Message msg = mLooperThread.mHandler.obtainMessage(0);//8
mLooperThread.mHandler.sendMessage(msg);//9
}
}


@Override
protected void onDestroy() {
super.onDestroy();
mLooperThread.mHandler.getLooper().quit();//10
}
}
  1. 定义工作线程,作为消息队列的消费者
  2. 将 Looper-隐式关联了消息队列与线程关联在一起
  3. 设置一个被生产者所使用的Handler,让生产者用来插入消息到队列中.这里我们使用的是默认的构造器,因而会与当前线程的 Looper 绑定在一起. 因此,Handler 只能在 Looper.prepare() 之后创建,不然它没有东西可以绑定了.
  4. 当消息被分发到工作线程的时候进入了回调.它会检查 what 参数并执行长时间运行的任务
  5. 开始从消息队列向消息者线程分发消息.这是一个阻塞的调用,因此工作现象不会终止.
  6. 启动工作线程,从而它能开始处理消息
  7. 后台工作线程上handler的设置和handler在UI线程上的使用存在竞态.因此需要验证mHandler是否可用.
  8. 初始化一个消息对象并将其what参数设置为0
  9. 将消息插入队列
  10. 终止后台线程.Looper.quit()的调用终止了 消息的分发并将 Looper.loop 从 阻塞中释放出来,从而run方法能终止,从而导致线程的终止.

消息传递所使用的类

现在,让我们更为具体地看看消息传递中所使用的具体组件,以及它们的用途.

MessageQueue

消息队列由 android.os.MessageQueue 所代表.它由链接的消息构建而成,形成了一个无限的单向链表.生产者线程插入到队列的消息,稍后会被分发到消息者.消息是基于事件戳排序的.队列中时间戳最小的消息,排在分发给消费者的队列的最前面.但是,消息只有在时间戳的值小于当前的时间值的时候才会被分发.如果时间没有到,分发这个动作会等到当前时间超过时间戳.

图4-6 显示了一个有三个等待的消息的消息队列,它们是以时间戳顺序排列的, t1 < t2< t3. 只有一个消息越过了分发栏栅,这个栏栅代表当前时间.能够被分发的消息的时间是小于当前时间的.

图 4-6 队列中的处于等待状态的消息.最右边的消息,是队列中最先被处理的.队列中的消息的箭头指向队列中的下一个消息.

如果没有消息被越过分发栏栅,而Looper以及做好获取下一个消息的转呗,消费者者线程会阻塞. 执行会在消息被发送到分发栏栅的时候恢复.

生产者能能够在任意时间以及队列的任意位置插入一个新的消息.队列中插入的位置是由时间戳决定的.如果跟等待的消息比,新的消息由最小的时间戳,那么它就会占据队列的第一个位置,将被下一个分发.插入总是遵循时间戳的顺序的.关于消息插入的部分会在 Hanlder这节进一步分析.

MessageQueue.IdleHandler

如果没有消息要处理,消费者线程就会有一些空闲时间.例如,图4-7,显示了当消费者线程空闲的时候,由一段时间槽.默认情况下,消费者线程只会在空闲时间期间等待新的消息,但是处了等待,在空闲槽期间线程能被分配去执行其它任务.这个特性能够让非重要的任务让出它们的执行时间,直到没有其它消息竞争执行时间.

图 4-7. 如果没有消息被传到分发栏栅,在下一个等待消息被执行之前,就存在一段时间槽能用来执行.

当等待的消息被分发之后,没有其它消息被传到分发栏栅,就会有一段时间槽,期间消费者线程能利用它来执行其它任务.应用通过 android.os.Message.IdleHandler 接口来获取着的时间槽. IdleHanlder 接口,值当线程空闲的时候的一个监听回调.监听被附加到消息队列上并以如下调用的方式从队列上脱离出来:

1
2
3
4
5
6
7
//Get the message queue of the current thread.
Message mq=Looper.myQueue();
//Create and register and idle listener
MessageQueue.IdleHandler idleHanlder=new MessageQueue.IldeHandler();
mq.addIdleHandler(idlehandler)
//Unregister an idle Handler
mq.removeIdleHanlder(idleHanlder)

idle hanlder 接口只包含了一个 callback 方法

1
2
3
interface IdleHandler {
boolean queueIdle();
}

当消息队列监测到消息队列的空闲时间的时候,它会调用所有注册过 IdleHanlder 实例地方的 queueIdle().实现callback的责任就交给了应用.你应该避免运行长时间运行的任务,因为在它们运行的时候,它们会推迟等待中的消息.

queue() 的实现必须返回一个布尔值,具体含义如下:

true

     idle hanlder 处于活跃态,它会继续在接下来的时间槽中接收回调.

false

     idle handler 已经不活跃了,就接下去又时间槽,他也不会接受到回调了.这个效用是更通过 MessageQueue.removeIdleHanlder()移除监听是一样的.

例子:使用 Idle Hanlder 来终止一个不再使用的线程

当线程又空闲时间槽的时候,所有在 MessageQueue 上注册过 IdleHanlders 的,都会被调用,期间他在等待要处理的新消息. 空闲时间槽能够发生在第一个消息之前,消息之前,以及最后一个消息之后.如果多个内容提供者应该在消费者线程上按序处理数据, IdleHandler 可以用来终止消费者线程,当消息都处理完了,线程在内存中就没有用处了.有了 IdleHanlder,没有必要记录最后一个插入的消息去得知什么时候线程能被终止.

这只在生产者线程没有延迟地插入消息到队列中才有效,因为这样直到最后一个消息插入之后消费者线程才会进入空闲状态.

ConsumeAndQuitThread 方法展示了 带有 Looper和消息队列的线程的结构,这个线程会在没有消息要处理的时候被终止:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private static class ConsumeAndQuitThread extends Thread implements MessageQueue.IdleHandler {

private static final String THREAD_NAME = "ConsumeAndQuitThread";

public Handler mConsumerHandler;
private boolean mIsFirstIdle = true;

public ConsumeAndQuitThread() {
super(THREAD_NAME);
}

@Override
public void run() {
Looper.prepare();

mConsumerHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// Consume data
}
};
Looper.myQueue().addIdleHandler(this);//1
Looper.loop();
}


@Override
public boolean queueIdle() {
if (mIsFirstIdle) {//2
mIsFirstIdle = false;
return true;//3
}
mConsumerHandler.getLooper().quit();//4
return false;
}

public void enqueueData(int i) {
mConsumerHandler.sendEmptyMessage(i);
}
}
  1. 当天启动的时候在后台线程注册IdleHanlder,然后Looper就准备好了,然后消息队列就设置好了.

  2. 让第一个 queueIdle 调用通过,因此它会在首个消息到达之前发生

  3. 在一次调用的时候返回true,所以IdleHandler仍然是注册着的

  4. 终止线程

    消息插入是由多个线程并发插入的,并且模拟一个随机的插入时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
final ConsumeAndQuitThread consumeAndQuitThread = new ConsumeAndQuitThread();
consumeAndQuitThread.start();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
SystemClock.sleep(new Random().nextInt(10));
consumeAndQuitThread.enqueueData(i);
}
}
}).start();
}

消息(Message)

消息队列上的每一项都是 android.os.Message 类,这是一个运送数据或任务,或什么也不运的容器对象.数据由消费者线程处理,然而任务只是在出队列的时候执行,你不需要做其它处理:

消息知道他的接受处理者Hanlder,并通过Message.sendToTarget()将其自身入队:

1
2
Message m=Message.obtain(hanlder,runnable);
m.sendToTarget();

正如我们会在handler这一节会看到的,handler 最常用来进行消息入队,同时就消息插入提供了更多的灵活性.

数据消息(Data Message)

数据集有着多个能够被传递给消费者线程的参数,如表 4-2 所示:

表 4-2 消息参数

参数名称 类型 用途
what int 消息标识.消息的通讯意图
arg1,arg2 int 用于传递整型的简单数据,如果最多只有两个整数值要被传递到消费者线程,这些参数比分配一个bundle来传递更为高效.
obj Object 任意对象.如果一个对象要传递到另一个进程的线程中,它必须实现 Parcelable
data Bundle 任意数据值的容器
replyTo Messager 指向其它进程中的Hanlder.使跨进程消息通讯成为可能
callback Runnable 在线程上执行的任务.这是一个持有来自Hanlder.post 方法的 Runnable 对象的内部实例域.

任务消息(Task Message)

任务由运行在消费者线程的java.lang.Runnable 对象代表.任务对象除了任务本身之外不能包含任何数据.

消息队列可以包含仍以数据和任务消息的组合.消费者线程以线性的方式对它们进行处理,不管它们是什么类型.如果消息是数据消息,消费者线程会处理数据.任务消息的处理方式是让Runnable执行在消费者线程上,但是消费者线程没有像数据消息一样在Handler.handleMessage(Message)中收到任何消息.

消息的生命周期是简单的:生产者创建消息,并且最终消息由消费者线程处理.这样的描述适用于大部分使用场景,但是出问题的时候,对消息处理的进一步理解就很有价值了.让我们看看在它的生命周期中,在消息身上到底发生了什么,这个过程可以分为如图4-8中的最重要的4步.运行时将消息对象存储到一个应用范围的对象池,从而使得能对之前的消息进行复用.这个避免了在每个处理的时候创建新的实例的消耗.消息对象的处理时间通常是很短的,并且许多消息在在一个时间单位就处理完了.

图 4-8 消息生命周期状态

状态转换部分由应用控制,部分由平台控制.注意状态不是可观察的,并且应用不能观察到从一个状态到另一个状态的变化.因此,应用不应该对于消息的当前状态作出任何假定.

初始化(Initialized)

在初始化状态,一个可修改状态的消息对象已经被创建,并且如果它是数据对象的话,已经被数据填充了.应用负责使用以下的调用来创建消息对象.它们能够从对象池中获取一个对象:

  • 明确的对象构造器

    1
    Message m=new Message();
  • 工厂方法:

    • 空消息:

      1
      Message m=Message.obtain();
    • 数据消息:

      1
      2
      3
      4
      5
      Message m=Message.obtain(Handler h);
      Message m=Message.obtain(Hanlder h,int what);
      Message m=Message.obtain(Handler h,int what, object o);
      Message m=Message.obtain(Handler h,int what, int arg1, int arg2);
      Message m=Message.obtain(Hanlder,int what,int arg1,int arg2,Object o);
  • 任务消息

    1
    Message m=Message.obtain(Handler h,Runnable task);
  • 拷贝构造器

    1
    Message m=Message.obtain(Message originMsg);

等待(Pending)

消息已经由生产者线程插入队列,并且它正等待着被分发到消费者线程.

已分发(Dispatched)

在此状态下,Looper已经获取并从队列中移除了消息.消息已经被分发到了消费者线程,并且正在被处理.对于这部操作没有应用api,因为分发是由Looper控制的,而不受应用的影响.当Looper分发一个消息,它会检查消息的分发信息然后分发消息到正确的接受者.一旦分发了,消息是在消费者线程上执行的.

已回收(Recycled)

在生命周期的这一时刻,消息的状态已经被清理了并且实例已经被交还给了消息池.当它结束在消费者线程的执行,Looper 处理了消息的回收.消息的回收是由运行时处理的,并且不应该由应用显式地完成.

一旦消息被插入了队列,内容应该不应在被修改了.理论上,在消息处理之前修改内容是合法的.但是,因为状态是不可观察的,在变更数据的时候,消息已经可能被消费者线程处理的,就会导致线程安全问题.更糟糕的是,消息可能已经被回收,因为它已经被交还给了消息池并且被另一个生产者传入到另一个队列中.