Android 消息机制:Handler、MessageQueue 和 Looper

在这篇文章中,我们将会讨论 Android 的消息机制。提到 Handler,有过一些 Android 开发经验的都应该很清楚它的作用,通常我们使用它来通知主线程更新 UI。但是 Handler 需要底层的 MessageQueue 和 Looper 来支持才能运作。这篇文章中,我们将会讨论它们三个之间的关系以及实现原理。

在这篇文章中,因为涉及线程方面的东西,所以就避不开 ThreadLocal。笔者在之前的文章中有分析过该 API 的作用,你可以参考笔者的这篇文章来学习下它的作用和原理,本文中我们就不再专门讲解:《Java 并发编程:ThreadLocal 的使用及其源码实现》

1、Handler 的作用

通常,当我们在非主线程当中做了异步的操作之后使用 Handler 来在主线程当中更新 UI。之所以这么设计无非就是因为 Android 中的 View 不是线程安全的。之所以将 View 设计成非线程安全的,是因为:1).对 View 进行加锁之后会增加控件使用的复杂度;2).加锁之后会降低控件执行的效率。但 Handler 并非只能用来在主线程当中更新 UI,确切来说它有两个作用:

  1. 任务调度:即通过 post() 和 send() 等方法来指定某个任务在某个时间执行;
  2. 线程切换:你也许用过 RxJava,但如果在 Android 中使用的话还要配合 RxAndroid,而这里的 RxAndroid 内部就使用 Handler 来实现线程切换。

下文中,我们就来分别看一下它的这两个功能的作用和原理。

1.1 任务调度

使用 Hanlder 可以让一个任务在某个时间点执行或者等待某段时间之后执行。Handler 为此提供了许多方法,从方法的命名上,我们可以将其分成 post() 和 sned() 两类方法。post() 类的用来指定某个 Runnable 在某个时间点执行,send() 类的用来指定某个 Message 在某个时间点执行。

这里的 Message 是 Android 中定义的一个类。它内部有多个字段,比如 whatarg1arg2replyTosendingUid 来帮助我们指定该消息的内容和对象。同时, Message 还实现了 Parcelable 接口,这表明它可以被用来跨进程传输。此外,它内部还定义了一个 Message 类型的 next 字段,这表明 Message 可以被用作链表的结点。实际上 MessageQueue 里面只存放了一个 mMessage,即链表的头结点。所以,MessageQueue 内部的消息队列,本质上是一个单链表,每个链表的结点就是 Message

当调用 post() 类型的方法来调度某个 Runnable 的时候,首先会将其包装成一个 Message,然后再使用 send() 类的方法进行任务分发。所以,不论是 post() 类的方法还是 send() 类的方法,最终都会使用 HandlersendMessageAtTime() 方法来将其加入到队列中:

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。这种类型的消息无法做更详细的处理。就是说,我们无法利用消息的 whatarg1 等字段(本身我们也没有设置这些字段)。如果我们希望使用 Message 的这些字段信息,就需要:

  1. 首先,要使用 send() 类型的方法来传递我们的 Message 给 Handler;
  2. 然后,我们的 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 是在主线程当中进行获取的。随后,我们调用 handlerpost() 方法之后,传入的 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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值