Android消息机制

Android的消息机制主要指Handler的运行机制,Handler机制由底层的 MessageQueue 和 Looper 支撑,MessageQueue 是一个消息队列 Handler 通过 post 或 send 的消息就存储在这个队列中(MessageQueue 用于存储消息的数据结构并不是队列,而是一个由 Message 构成的单链表,但是我们可以按照队列去理解它),其中 Looper 恰如其意就是无限循环读取MessageQueue中的消息,有消息就拿出来处理,没有消息则阻塞在MessageQueue.next()方法中。
注:文章的中间部分是对 Handler 涉及内容的深入讲解,暂时不感兴趣的话可以直接跳至文末的 “Handler 工作过程”。

为线程实例化一个Handler

我们在实例化一个Handler时可能会遇到这样一个问题:

java.lang.RuntimeException:Can't create handler inside thread that has not called Looper.prepare()

意思是不能在一个没有调用Looper.prepare()方法的线程中创建Handler。Looper源码的开头有这样一段注释给出了在线程中初始化 Handler 的正确操作:

  /* 
  *  class LooperThread extends Thread {
  *      public Handler mHandler;
  *
  *      public void run() {
  *          Looper.prepare();
  *
  *          mHandler = new Handler() {
  *              public void handleMessage(Message msg) {
  *                  // process incoming messages here
  *              }
  *          };
  *
  *          Looper.loop();
  *      }
  *  }
  */

正确操作是先调用 Looper.prepare()为线程初始化一个Looper,实例化Handler用于向该线程发消息,最后调用Looper.loop()方法启动 Looper 的循环。

Handler的初始化过程

在Handler的构造方法中有这样一段代码:

mLooper = Looper.myLooper();
if (mLooper == null) {
    throw new RuntimeException( "Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;

代码首先要获得当前线程的 Looper 实例然后赋值给 mLooper,如果 mLooper 为空就会抛出上面说的那个异常。如果 mLooper 不为空则将 mLooper.mQueue 赋值给自己的 mQueue,这个 mQueue 就是当前线程消息队列 MessageQueue 的实例,Handler 就是通过这个实例向线程发送消息的,Looper.myLooper()是如何获取当前线程的Looper实例的呢?进入Looper.myLooper()的源码实现可以看到:

public static Looper myLooper() {
    return sThreadLocal.get();
}

sThreadLocal 的声明和初始化:

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

这是一个由 ThreadLocal 派生的专门用于获取当前线程 Looper 实例的静态对象,Handler 就是通过用它的get()方法获取线程的Looper实例的。

ThreadLocal工作原理

ThreadLocal 是一个线程内部的数据存储类,我们通过它可以在指定线程中存储数据,数据存储后对于其他线程来说是不可见的。
每个线程都持有一个ThreadLocal.Values 实例 localValues,ThreadLocal.Values 中有一个数组Object[] table它就是线程用来存储内部数据的容器,ThreadLocal 对外提供了一对用于存储和获得数据的方法set(T value)get(),我们搞清楚两个方法的工作流程就可以知道 ThreadLocal 的工作原理了。
set 的工作流程:
首先获得当前线程的 localValues,得到 localValues 的 table 数组,以 ThreadLocal 自身实例 this 为键值计算出一个 int 值 index,在 table[index] 中存储 this 的弱引用 reference,这个 reference 在 get() 的时候会用到,在 table[index + 1] 中存储 value,这样就完成了数值 value 的存储。
get 的工作流程:
与 set 相同,get 也是要先获得当前线程的 localValues,进而得到 localValues 的 table 数组,还是用 this 计算得到的值作为数组下标得到 table[index] 并与 this.reference 作比较,相同则返回 table[index + 1],table[index + 1] 就是 set 时存入的 value。

Looper的工作过程

Looper的工作是从在线程中调用 Looper.prepare() 开始的,prepare 方法就是 Looper 的初始化方法,下面是 prepare 源码:

public static void prepare() {
    prepare(true);
}

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));
}

prepare() 会调用 prepare(boolean quitAllowed) 方法,方法的 quitAllowed 参数是用来标志当前 Looper 循环是否可以通过调用 quit() 方法退出的,可以看出 prepare(boolean quitAllowed) 是私有静态方法所以我们只能用 prepare() 初始化 Looper,我们初始化的 Looper 的 quitAllowed 永远是 true,只有主线程也就是 UI 线程的 quitAllowed 为 false,也就是说我们手动开启的 Looper 循环是可以手动退出的,主线程的 Looper 是不能够手动退出的。prepare 的工作很简单,就是实例化一个 Looper 对象,Looper 对象实例化的过程中会同时实例化一个 MessageQueue 对象 mQueue,mQueue 就是这个线程中的消息队列,Looper 对象初始化后会被存入静态对象 sThreadLocal 中,sThreadLocal 就是上一节讲到的 ThreadLocal 的实例,用于存储线程对应的 Looper 对象。
Looper.prepare() 之后需要调用 Looper.loop() 来循环读取 MessageQueue 中的消息

public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;
    ...
    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
             // No message indicates that the message queue is quitting.
             return;
        }
        ...
        msg.target.dispatchMessage(msg);
        ...
        msg.recycleUnchecked();
    }
}

loop() 方法就是开启一个无限循环调用 MessageQueue 实例 mQueue 的 next() 方法读取 MessageQueue 中的消息,当 MessageQueue 中有消息时 next 返回消息 msg,loop() 就会调用 msg.target.dispatchMessage(msg)来处理消息,如果 MessageQueue 中没有消息 loop 循环则会阻塞在 next 方法上,这也是为什么 loop 这个无限循环不会造成 CPU 性能浪费的原因,next 是如何实现阻塞的会在下一节介绍。
loop 唯一正确跳出循环的方式是 next() 返回 null。当 Looper 的 quit 方法被调用时,Looper 会调用 MessageQueue 的 quit 方法通知 MessageQueue 退出,当 MessageQueue 被标记为退出时它的 next() 方法就会返回 null,当前线程的 Looper 也就退出了。

MessageQueue 实现

MessageQueue 读取消息的方法是 Message next()方法,该方法内也是一个无限循环 for (;;),next 方法执行过程中会被阻塞在 nativePollOnce 方法上,阻塞直到 Handler 发送消息时 MessageQueue 的 enqueueMessage 方法调用 nativeWake 后才会解除,nativeWake 方法的调用也就意味着 MessageQueue 中有新消息可以返回给 Looper 处理了。显然 nativePollOnce/nativeWake 都是 native 方法,它们的底层基于名为 epoll 的 Linux 系统调用,epoll 的作用是监听某个文件描述符上是否有 IO 操作,nativePollOnce 就是在某个文件描述符上调用 epoll_wait 来监听这个文件描述符上的 IO 操作,nativeWake 会在该文件描述符上进行 IO 操作 nativePollOnce 的阻塞状态就解除了。
对 nativePollOnce 方法的讲解还没完,该方法需要传入两个参数:

final long ptr = mPtr;
int nextPollTimeoutMillis = 0;

mPtr 的值是调用 nativeInit() 方法返回的,上面讲过,nativePollOnce/nativeWake 是对统一文件描述符 IO 的监听和操作,这个 mPtr 就代表那个文件描述符;
nextPollTimeoutMillis 参数与 Handler 的 boolean sendMessageDelayed(Message msg, long delayMillis) 的 delayMillis 有关系,我们常会使用 Handler 的 postDelayed 或者 sendMessageDelayed 方法发送一个延迟消息,消息只有到指定时间才会被处理,我们在向消息队列写入消息时也就是调用 MessageQueue的 enqueueMessage 方法时方法内会判断该条消息的预约执行是否小于消息队列队首消息的预约时间,如果小于则把消息插入队首,否则插入队列中第一个预约时间大于待插入消息预约时间的消息前面,消息插入后调用 nativeWake(mPtr)唤醒被 nativePollOnce 阻塞的 next 方法,next 的阻塞状态会被解除,如果 next 处理消息时发现这条消息 msg.when 要大于当前时间说明这是条消息是延时消息,next 会用 msg.when 减去当前时间并赋值给 nextPollTimeoutMillis,代码进入下一次循环并再次被阻塞在 nativePollOnce(ptr, nextPollTimeoutMillis)方法,这个 nextPollTimeoutMillis 就是阻塞的定时解锁时间,为的是除延迟消息外没有新消息时阻塞也可以在延时消息到时的时候解除,next 就可以处理这条延时消息了。

Handler 工作过程

首先调用 Looper.prepare() 为线程创建一个 Looper 实例,这个实例会被存储在 ThreadLocal 中这样在线程作用域的任何位置只要调用 Looper.myLooper() 就可以得到这个 Looper 实例,Looper 的实例化过程中会实例化一个 MessageQueue 对象 mQueue,这个 mQueue 就是当前线程的消息队列。Looper prepare 后才可以创建 Handler 实例,否则会抛出找不到 Looper 的错误。接下来调用 Looper.loop() 方法开启循环读取消息并阻塞在 MessageQueue 的 next 方法直到有消息进入消息队列,现在我们就可以通过 Handler 实例向它所在线程发送消息了。
Handler 有两种发送消息的方式:post 一个 Runnable 或者 send 一条 Message,然而 post 的实现方式也是将 Runnable 封装在一个 Message 中发送出去的,sendMessage 会将 this 也就是发送消息的 Handler 对象保存在 Message 的 target 字段,用于 Looper 处理消息时找到目标 Handler,sendMessage 再通过 MessageQueue 的 enqueueMessage 方法将消息压入消息队列中,enqueueMessage 会使用 nativeWake 方法唤醒因 nativePollOnce 阻塞的 next 方法,next 的阻塞解除后会将符合要求的消息(因为有延迟消息的存在可能会重新返回阻塞状态)返回给 Looper,Looper 在接到消息后会调用

// target 就是 msg 内保存的目标 Handler 实例
msg.target.dispatchMessage(msg) 

将消息交给目标 Handler 处理,dispatchMessage 的处理逻辑如下:

public void dispatchMessage(Message msg) {
    // 处理 post 消息
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        // 处理以 Handler(Callback callback) 方式初始化的 Handler
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        // 处理
        // new Handler() {
        //     @Override
        //     public void handleMessage(Message msg) {
        //         super.handleMessage(msg);
        //     }
        // }
        // 中的 super.handleMessage
        handleMessage(msg);
    }
}

至此就完整地跑完了一次 Handler 工作过程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351