Android消息机制之Handler源码分析

前言

Android消息机制分析

Handler源码分析

同步至个人博客


正文


一. 概述

Handler算是我们平时开发中比较常用的一个, Handler所代表的是Android中重要的一部分, 即消息机制; 本文将分析Handler的运行机制

1.1 使用场景

  1. 线程切换: 这个是使用Handler最常见和最频繁的场景了; 我们都知道, UI线程中不允许执行耗时操作, 那么当我们需要先去获取数据(耗时)再进行UI展示或者交互的时候, 线程切换就必不可少了

  2. 任务延时: 即在未来某个时刻再执行任务, 这个主要是通过Handler.postDelayed()Handler.sendMessageDelayed()完成的(实际上postDelayed()也是通过sendMessageDelayed()完成的)

1.2 为什么会有Handler

我们都知道Handler的作用主要是用于切换线程, 那么为什么会出现Handler喃; 我们以UI线程中执行耗时操作为例, 对于耗时操作, 特别是在需要实时交互的应用中, 肯定是需要设计为多线程的, 那么Android设计之初为什么不使用多线程去访问UI呢, 最致命的一点就是, 多线程存在并发访问的不确定性, 这就会造成UI表现的不可预期性, 造成用户体验极差; 当然, 对于多线程并发的问题还是有很多方式去避免和解决的, 最常见的一个就是加锁, 那么为什么不采用多线程访问,
UI加锁的方式实现呢, 一个是加锁会使UI逻辑变得复杂, 另一个是加锁会降低效率, 阻塞某些线程的执行

所以, 基于很多方面的考虑, 特别是对于Android应用这样交互性很强的场景而言, 使得需要去实现自己的一套异步通信机制, 需要达到的目标是, 线程之间的独立性保持和数据交互的便利性, 这也是Handler实现的

1.3 概述

Android消息机制中, 有非常重要的几个部分, 也是我们分析的切入点:

  1. MessageQueue: 单链表, 实现消息存储; 是Java层和Native层的连接纽带

  2. Looper: 无限循环查询是否有新消息

  3. Handler: 消息接受和分发处理的中转站

以下是我们使用Handler最常见的一种写法, 我们通常会重写handleMessage()来实现自己的逻辑; 当在其他线程中通过Handler.sendMessage()来发送消息时, 我们就会在handleMessage()中接收到该消息

    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // do somthing with msg
            super.handleMessage(msg);
        }
    };

    new Thread() {
        @Override
        public void run() {
           mHandler.sendEmptyMessage(0);
        }
    }.start();

Handler进行线程切换和消息传递的一个大致流程是, 当我们通过sendMessage()发送消息的时候, MessageQueue负责存储该消息, Looper会无限循环的去检查MessageQueue中是否有消息, 如果有则调用HandlerdispatchMessage()进行消息分发, 最终会回到handleMessage中进行处理

在开始讲解之前, 需要先明确几个结论(在稍后会验证): 每个线程都可以有自己的Looper, 但是默认没有创建(除了UI线程外); Looper是创建Handler所必须的(否则会抛异常); MessageQueueLooper是一一对应的关系, 也就是说每个线程会有自己的MessageQueueLooper; Handler在哪个线程中创建就在哪个线程中处理消息, 其他线程发送的消息存储在对应Handler所对应的MessageQueue


二. 构造函数

Handler提供了七个构造函数, 如下表

Handler() Handler(Callback callback) Handler(Looper looper)
Handler(Looper looper, Callback callback) Handler(boolean async) Handler(Callback callback, boolean async)
Handler(Looper looper, Callback callback, boolean async)

这七个构造函数最常用的是默认构造函数(即不带参数那个), 其他几个, 一类是带Callback的, 是提供的消息处理函数, 我们使用Handler一般都会重写其handleMessage()方法来处理消息, 但是也可以不重写handleMessage(), 而是在构造Handler的时候传入一个Callback回调, CallbackHandler中的一个接口, 如下; 另一类是带标志位async的, 是设置消息是否为异步处理, Handler中的消息默认是同步处理的

    public interface Callback {
        public boolean handleMessage(Message msg);
    }

这里我们以构造函数Handler(Callback callback, boolean async)进行分析, 如下; 可以看出, 这里的mLooper是通过Looper.myLooper()获取的, 如果我们获取为null也就是说当前线程没有Looper的话, 会抛出异常, 这个异常也是我们大多数初学者会遇到的; 这里也验证了上面所说的, LooperHandler构造所必须的, 如果当前线程没有Looper的话, 会抛异常(至于为什么说是当前线程, 接下来马上分析~)

    public Handler(Callback callback, boolean async) {
        ...
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

那么Looper.myLooper()又是如何获取当前线程的Looper的呢; 如下, 可以看出是通过sThreadLocal获取的, sThreadLocal是一个ThreadLocal类型的实例

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

ThreadLocal不是一个线程, 而是用于在每个线程中存储数据的, ThreadLocal可以在不同的线程中互不干扰的存储并提供数据; 那么它是如何实现在不同线程中互不干扰的存取数据的呢, 我们先来看其set()方法, 可以看出, 其实际上会先去检查当前线程是否持有ThreadLocalMap, 如果有的话, 直接将value存储进去即可, 否则, 初始化当前线程的ThreadLocalMap然后再存放值, 这样就可以根据不同的线程来存取各自线程的值了

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

那么我们要构造Handler就必须获取当前线程的Looper, 那么我们又是什么时候构造和存储的Looper喃; 前面我们说了, 每个线程都可以有自己的Looper, 但是除了UI线程以外都默认没有创建; 我们先来看UI线程中是什么时候创建的Looper, UI线程就是ActivityThread, 在其main()函数中, 我们发现了如下与Looper相关的语句, 这实际上是开启了主线程的消息处理(因为要处理各种事件, 所以一开始就开启了)

public static void main(String[] args) {
    ...
    Looper.prepareMainLooper();
    ...
    Looper.loop();
}

我们继续看Looper.prepareMainLooper(), 其中实际上是调用了prepare(), 如下; 这里我们看到了熟悉的ThreadLocal.set(), 而且存储的是一个新的Looper, 那么这里就将线程和各自的Looper联系起来了, 我们在Handler构造函数中获取的也就是和各个线程相关的Looper

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

到这里我们知道了, perpare实际上是在准备Looper, 那么Looper.loop()又是在做啥呢, 其实Looper.loop()是开启消息处理循环, 前面我们说了Looper不断的去检测MessageQueue中是否有消息, 实际上就是在loop()中开启的

这里我们暂时不讲loop(), 而是放到后面讲消息处理的时候讲; 我们平时使用Handler的时候, 大多是在UI
线程中创建的, 因为上面我们已经看到了, UI线程中默认是创建了Looper的, 所以不需要我们再去手动创建, 但是我们如何在子线程中构造和使用Handler呢; 其实在Looper的文档中已经给出了示例, 如下; 其实, 和上面UI线程中Looper的处理过程差不多, 只是由于UI线程的特殊性, 所以单独给它准备了一个方法~(实际上是做一些其他特殊判断和处理)

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

三. 消息发送

当我们需要通过Handler发送消息时, 可以使用两个系列, 一个是postXXX系列, 一个sendXXX系列

postXXX: post系列主要是发送一个自定义的Runnable事件, 然后去执行(延迟执行)自定义事件

post(Runnable r)
postAtTime(Runnable r, long uptimeMillis)
postAtTime(Runnable r, Object token, long uptimeMillis)
postDelayed(Runnable r, long delayMillis)
postDelayed(Runnable r, Object token, long delayMillis)
postAtFrontOfQueue(Runnable r)

sendXXX: send系列主要是发送封装的一个msg, 然后根据msg带的tag或者数据在handleMessage()中进行处理

sendMessage(Message msg)
sendEmptyMessage(int what)
sendEmptyMessageDelayed(int what, long delayMillis)
sendEmptyMessageAtTime(int what, long uptimeMillis)
sendMessageDelayed(Message msg, long delayMillis)
sendMessageAtTime(Message msg, long uptimeMillis)
sendMessageAtFrontOfQueue(Message msg)

但是, 实际上post系列最终还是将Runnable事件封装为了一个Message对象交给了send系列处理, 而不管是postXXX还是sendXXX, 最终转到的都是enqueueMessage()进行处理; 另外, 不管是postRunnable还是sendint what等其他类型, 其实最终都会将每一条消息封装为一个Message对象进行传递


四. 消息存储

前面我们说过, MessageQueue会对发送的消息进行存储, 这一步就是在enqueueMessage()中处理的; 如下; 需要注意的是这里传入的MessageQueue是在创建Handler时创建, 也就是说和Handler相关, 因为涉及到多线程的处理, 所以需要清楚MessageQueue的来源, 不然消息处理的时候就混了~

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

这里我们简单看一下MessageQueueMessage的组织方式, 如下, MessageQueue.enqueueMessage(), 可以看出是很明显的单链表的存储方式, 之所以用单链表, 是因为单链表在增加和删除节点上有优势

boolean enqueueMessage(Message msg, long when) {
    ...
    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;
    ...
}

五. 消息处理

还记得前面我们还有一个问题没有解决吗, 就是Looper.loop()过程, Looper.loop()过程实际上就是开启循环处理消息的过程~

该函数如下; 首先获取消息处理线程的Looper, 前面我们已经看到了, Looper.loop()是在消息处理线程中调用的, 所以这里获取到的, 也就是消息处理线程, 即创建Handler的线程; 之后获取对应线程的消息队列, 即MessageQueue, 需要注意这里的MessageQueueLooper关联的, 而Looper的获取是通过Thread.currentThread()来判断和获取的, 那么这里就能获取到和对应Handler相关联的MessageQueue, 当我们在不同线程中调用Handler.sendMsg的时候, 插入的也是和该Handler关联的MessageQueue, 这样, 一整条完整的线就串成了: 不同的线程往需要发送消息的线程的MessageQueue中插入Message, 而和线程关联的Looper又从该MessageQueue中不断获取Message处理, 这样就完成了线程的切换 !!

接下来就是用一个死循环(for)开始消息处理了; 逻辑也比较简单, 其实就是不断的从MessageQueue中获取消息(MessageQueue.next()), 然后回调Handler.dispatchMessage(msg)分发, 需要注意的是, 这里的msg.target实际上是存储的对应Handler的引用

public static void loop() {
    final Looper me = myLooper();
    ...
    final MessageQueue queue = me.mQueue;
    ...

     for (;;) {
        Message msg = queue.next(); // might block
        // 如果消息为null, 不处理咯
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }
        ...
        msg.target.dispatchMessage(msg);
     }

}

我们看一下dispatchMessage的处理逻辑, 如下; 逻辑也很简单, 就是消息处理的优先级: 如果Message自身设置了Callback, 就直接执行MessageCallback(这种情况对应前面说的post(Runnable)的情况); 如果构造Handler的时候设置了Callback那么交给该Callback处理, 否则回调handleMessage()处理

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

这里还要注意的一点是从MessageQueue中获取消息时, 即MessageQueue.next(), 这是一个阻塞方法, 也就是说当MessageQueue中没有消息的时候, 会阻塞在这里, 直到MessageQueue中再次存储消息


六. 总结

到这里Handler的整个Java层面的运行机制我们就讲解完啦~ , 到这里我们对整个Android的消息传递和处理机制也有了比较详细的了解了; 为加深记忆, 可以对着源码自己再过一遍过程~

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

推荐阅读更多精彩内容