Android、Java 学习笔记--消息机制(Handler、Looper、MessageQueue)与 ThreadLocal 原理

Author Avatar
ChihoPang 12月 02, 2017
  • 在其它设备中阅读本文章

Android 是消息驱动型系统,其消息机制通过 Handler 、 Looper 、 MessageQueue 组成,其中,Handler 作为上层接口,Looper 、 MessageQueue 提供底层支持。同时,由于 Looper 是采用 ThreadLocal 来存储的,所以本篇笔记也有 ThreadLocal 类的相关使用和原理记录。

参考资料

《开发艺术探索 第十章》

原理图&思维导图

img
img

Handler

使用方法

UI 线程已自动包含 Looper,故参考方式 1 。其余线程可通过 Looper.myLooper() == null 判断是否含有 Looper。

  • 当前线程具有 Looper 的情况下
    //直接创建对象即可使用
    Handler handler=new Handler(){
      @Override public void handleMessage(Message msg) {
        super.handleMessage(msg);
        handle();
      }
    };
    
  • 当前线程没有 Looper 的情况下
    Looper.prepare();//为当前线程创建 Looper
    Handler handler=new Handler(){...};
    Looper.loop();//启动当前线程 Looper
    
  • 发送消息
    handler.send(msg);
    handler.post(runnable);//最终调用 send()
    
  • 处理消息
  1. 重写 handler.handleMessage(msg) 方法
  2. 设置 msg.callback 变量,构建消息时自定义处理方法

系统提供 Handler 的原因

由于 UI控件非线程安全,为防止 UI 控件处于不可预期状态,系统不允许子线程访问 UI 的原因。Handler 主要用于解决子线程无法访问 UI 的问题。

Handler 发送消息流程

send() 方法最终调用 queue.enqueueMessage(msg,uptimeMillis);向消息队列插入了一条信息。

Handler 处理消息流程

Looper.loop() 获取到消息后,调用 msg.target.dispatchMessage(msg) 分发信息,dispatchMessage(msg) 接收到消息后,检查 msg.callback 是否为空,非空则交由 msg.callback 处理;再检查 mCallBack 是否为空,mCallBack 是创建 Handler 实例时可选择传入的参数,用于简单的消息处理,不必创建 Handler 子类;最后,再调用 handleMessage(msg) 处理。

MessageQueue

消息的插入 enqueueMessage()

Handler.send(msg) 最终调用此方法,往消息队列插入一条消息。

消息的读取 next()

由 looper.loop() 调用。当消息队列中没有消息时,next() 方法一直阻塞;当有新消息时,next 方法返回该消息并将其从单链表中移除。

底层实现

单链表,便于插入删除

Looper

构造时会自动创建 MessageQueue

为当前线程创建 Looper

调用 Looper.prepare() 方法,为当前线程创建一个 Looper,如果当前线程已有 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));
    }

开始消息循环 loop()

调用 Looper.loop() 即可开启当前线程 Looper 的消息循环。若当前线程没有 Looper,会抛出异常。
loop() 为死循环方法,会阻塞当前线程,直至 queue.next() 返回 null 时才 return。
若 loop() 方法中获取到了消息,则调用 msg.target.dispatchMessage(msg) 进行消息分发。

关于 Looper.loop() 阻塞当前线程问题

Android中为什么主线程不会因为Looper.loop()里的死循环卡死? - 知乎

退出消息循环 quit()、quitSafely()

调用 Looper.quit() 或 Looper.quitSafely() 退出消息循环,两者最终都会调用 quque.quit(boolean safe),使 queue.next() 方法返回 null,从而使 loop() 方法退出。
调用 quit() 方法会直接退出循环,而调用 quitSafe() 方法则处理完消息队列的消息后才退出循环。
退出循环后,Handler 发送消息会失败,handler.send() 方法返回 false。
建议在不需要时终止 Looper。

底层存储方式

Looper类 采用 ThreadLocal 来存储各线程对应的 Looper 实例。

判断当前线程是否为 UI 线程

由于 Looper.getMainLooper() 可以返回主线程使用的 Looper,而 Looper.myLooper() 可以返回当前线程 Looper,故可以通过 Looper.myLooper()==Looper.getMainLooper() 判断当前线程是否为 UI 线程。

ThreadLocal

作用

对同类型变量,为每一个线程创建独立的副本进行存储。

使用

//创建(一般设置为静态变量)
ThreadLocal<Integer> value=new ThreadLocal<>();
//存储数据
value.set(666);
//获取数据
value.get();

ThreadLocal 对象会自动根据当前线程存取对应数据。

原理

每一个线程都具有一个 ThreadLocalMap,存储在 Thread.currentThread().threadLocals 变量中。ThreadLocalMap 中含有一个 ThreadLocal 的弱引用数组。其实现与 HashMap 相似,即以 ThreadLocal 为键值,泛型类(数据类)为 value 值。

set() 方法最终调用的是 map.set(ThreadLocal<?> key, Object value);
map 根据 ThreadLocal 的 hash 值找到相关存储位置,存储 value。

get() 方法先找到通过 map.getEntry(threadLocal) 找到对应弱应用,再获取 value。