Android 开发艺术探索笔记之十 -- Android 的消息机制

学习内容

  • Android 的消息机制
  • Handler 即其底层支撑

原文开篇部分:

  1. 从开发角度来说,Handler 是 Android 消息机制的上层接口,通过它可以将任务切换到 Handler 所在的线程中执行。
  2. 更新 UI 仅仅是 Handler 的一个特殊的使用场景。本质不是专门用来更新 UI,只是常被用来这么做而已。
  3. Andorid 的消息机制主要指 Handler 的运行机制,Handler 的运行底层的 MessageQueueLooper 的支撑。
    1. MessageQueue(消息队列):采用单链表的数据结构存储消息列表,以队列的形式对外提供插入和删除的工作。只负责存储,不管处理。
    2. Looper 负责处理消息:以无限循环的形式查找是否有新消息,有则处理,无则等待。
    3. ThreadLocal 可以在把不同的线程中互不干扰的存储提供数据,通过它来获取每个线程中的 Looper
  4. 线程默认没有 Looper,因此如果需要使用 Handler 必须先为线程创建 Looper。UI 线程(ActivityThread)创建时会初始化 Looper,因此默认可以在主线程中使用 Handler。

Android 的消息机制概述

1.Handler 的作用:

  • 将一个任务切换到某个制定的线程中执行。

2.为什么提供上述 Handler 的功能?

  • Android 规定访问 UI 只能在主线程中执行,如果在子线程中访问 UI,那么程序会抛出异常。
  • 以上限制,源码中 ViewRootImpl 对 UI 操作进行验证,通过 chearThread 方法实现。
  • 同时由于 Android 的限制,导致必须在主线程中访问 UI,但是如果在主线程中进行耗时操作,可能会导致 ANR,因此。系统提供 Handler,主要原因就是为了解决在子线程中无法访问 UI 的矛盾。(子线程进行耗时操作,通过 Handler 访问 UI)

3. Android 为什么不允许子线程访问 UI?

  • 核心点:Andorid 的 UI 控件并非线程安全。
  • 通常方法及其问题:
    • 对 UI 控件的访问加入*上锁机制
    • 问题:使 UI 访问的逻辑变得复杂;锁机制会降低 UI 访问的效率,因为锁机制会阻塞某些线程的执行。
  • 解决方案:
    • 采用单线程模型来处理 UI 操作
    • 通过 Handler 切换 UI 访问的执行线程即可

4.Handler 的工作原理:

  • Handler 创建时会采用当前线程的 Looper 来构建内部的消息循环系统,当前线程没有 Looper 会报错
  • 创建完毕后,通过 Handler 的 post 方法将 Runnable 投递到 Handler 内部的 Looper 中去处理;或者通过 Handler 的 send 方法发送消息,该消息也会在 Looper 中处理。实际 post 最后也会调用 send 方法。
  • send 方法的工作过程:调用 send 方法后,它会调用 MessageQueue 的 enqueueMessage 方法将该消息放入消息队列中,然后 Looper 发现新消息后,会处理这个消息,最终消息中的 Runnable 或者 Handler 的 handleMessage 方法就会被调用。
    • 注意:Looper 运行在创建 Handler 所在的线程中,这样一来 Handler 中的业务逻辑就被切换到创建 Handler 所在的线程中去了。

Android 的消息机制分析

ThreadLocal 的工作原理

1.基本定义

  • ThreadLocal 是一个线程内部的数据存储类,通过它可以在指定线程中存储数据,数据存储后,只有在制定线程中可以获取到存储的数据,而其他线程则无法获取到数据。
  • why ?(为什么通过 ThreadLocal 可以在不同的线程中维护一套数据的副本并且彼此互不打扰?)
    • (简单说明) 不同线程访问同一个 ThreadLocal 的 get 方法,ThreadLocal 内部会从各自的线程中取出一个数组,然后再从数组中根据当前 ThreadLocal 的索引去查找对应的 value 值。显然,不同线程中的数组是不同的,这也就是原因所在。

2.使用场景

  1. 存储以线程为作用于并且不同县城具有不同的数据副本的数据
    1. 如:通过 ThreadLocal 存储不同线程中的 Looper,供 Handler 获取当前线程的 Looper
  2. 复杂逻辑下的对象传递,比如监听器的传递。
    1. 复杂逻辑:比如 函数调用栈比较深以及代码入口的多样性。
    2. 采用 ThreadLoacl 让监听器作为线程内的全局对象存在,而线程内部只要通过 get 方法就可以获取到监听器。

3.原理

  1. ThreadLocal 是一个泛型类,定义为 public class ThreadLocal<T>,核心在于 ThreadLocal 的 get 和 set 方法。

  2. set 方法:

    ​ (lz这里查看了一下现版本的源码,发现和书中有些许差异)

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

    ​ 虽说和书中源码有出入,但是大体思想是一致的:通过一个结构 ThreadLocalMap 存储线程的 ThreadLocal 数据。

    ​ ThreadLocalMap 部分代码如下:

    static class ThreadLocalMap {
            static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
        
        private Entry[] table;
       
        //...
    

    ​ 内部的这个 private Entry[] table 数组即用来存储 ThreadLocal 的值。

    ​ 回到上面 ThreadLocal 的 set 方法,如果ThreadLocalMap 为空,则创建该线程的 ThreadLocalMap 对象,并将 ThreadLocal 的值存储。如果非空,则直接通过 ThreadLocalMap.set 方法存储ThreadLocal 的值,ThreadLocalMap.set 的源码如下:

    private void set(ThreadLocal<?> key, Object value) {
    
                Entry[] tab = table;
                int len = tab.length;
                int i = key.threadLocalHashCode & (len-1);
    
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) {
                    ThreadLocal<?> k = e.get();
    
                    if (k == key) {
                        e.value = value;
                        return;
                    }
    
                    if (k == null) {
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
    
                tab[i] = new Entry(key, value);
                int sz = ++size;
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }
    

    ​ ThreadLocal 的值直接存储在 Entry.key 匹配的 Entry.value 中。(和书中有所差异,并非凭借索引位置)

  3. set 方法介绍完,下面介绍 get 方法:

    public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            return setInitialValue();
        }
    

    ​ get 方法实际就获取 ThreadLocalMap 对象,如果非空,则调用 ThreadLocalMap.getEntry 方法,得到目标 Entry 对象,该 Entry 对象的 value 字段即为 ThreadLocal 值。如果 ThreadLocalMap 为空,那么返回初始值,该初始值可通过重写 initialValue 方法更改。

    ​ ThreadLocalMap.getEntry 方法如下:

    private Entry getEntry(ThreadLocal<?> key) {
                int i = key.threadLocalHashCode & (table.length - 1);
                Entry e = table[i];
                if (e != null && e.get() == key)
                    return e;
                else
                    return getEntryAfterMiss(key, i, e);
            }
    

    ​ 可以看到,逻辑很清晰,取出 table 数组,并找出 ThreadLocal 的引用对象所在的 Entry ,返回之。

  4. 小结

    1. 操作对象是当前线程的 ThreadLocalMap 内的 private Entry[] table 数组,读写仅限于各自线程的内部,
    2. 因此多个线程可以互不干扰的存储和修改数据。

消息队列的工作原理

1.基本介绍

  • 消息队列在 Android 中指的是 MessageQueue,内部通过一个单链表的数据结构来维护消息列表(方便插入和删除)

2. 原理

  1. enqueueMessage 方法:向消息队列中插入一条消息。
  2. next 方法:无限循环的方法。从消息队列中取出一条消息并将其从消息队列中移除。如果消息队列中没有消息,那么 next 方法会一直阻塞。

Loop 的工作原理

1.基本介绍

  • Looper 在 Android 的消息机制中扮演着消息循环的角色,具体来说就是它会不断的从 MessageQueue 中查看是否有新消息,如果有新消息就会立刻处理,否则就会一直阻塞在那里。
  • Handler 的工作需要 Looper,没有 Looper 的线程会报错

2.原理

  1. 构造方法:创建一个 MessageQueue,然后保存当前线程的对象。
  2. 通过 Looper.prepare() 为当前线程创建一个 Looper,然后就可以创建 Handler 的对象了,接着通过 Looper.loop() 方法来开启消息循环。
  3. 除 prepare 外,还有一个 prepareMainLooper 方法,主要是给主线程 ActivityThread 创建 Looper 使用的,本质也是通过 prepare 方法实现的。对应的,系统提供 getMainLooper 方法来在任意位置获取主线程的 Looper。
  4. Looper 终止:
    1. quit 方法:直接退出 Looper
    2. quitSafely 方法:设定退出标记,当消息队列中的已有消息处理完毕后安全地退出。
    3. Looper 退出后,通过 Handler 发送的消息会失败,此时 Handler 的 send 方法会返回 false。子线程中,如果手动创建了 Looper,那么所有事情做完后,应当调用 quit 方法来终止消息循环,否则这个子线程会一直处于等待的状态,而如果退出 Looper 以后,这个县城就会立刻终止,因此 建议不需要的时候终止 Looper
  5. Looper.loop 方法
    1. 调用了 loop 方法后,消息循环系统才会 真正的起作用。
    2. 工作过程:
      1. loop 方法是一个死循环,当且仅当 MessageQueue.next 方法返回了 null 时,跳出循环。当 Looper 的 quit 方法被调用时,Looper 就会调用 MessageQueue 的 quit 或者 quitSafely 方法来通知消息队列退出,此时,next 的方法就会返回 null。
      2. loop 方法会调用 MessageQueue 的 next 方法来获取新消息,当没有信息时,next 会阻塞,这也导致 loop 方法一直阻塞。
      3. 当 next 方法返回新消息时,Looper 就会处理这条新消息:msg.target.dispatchMessage(msg),这里的 msg.target 是发送这条消息的 Handler 对象,这样 Handler 发送的消息最终又交给它的 dispatchMessage 方法来处理了。
      4. Handler 的 dispatchMessage 方法是在创建 Handler 时所使用的 Looper 中执行的,这样就将代码逻辑切换到制定的线程中了。

Handler 的工作原理

1.原理

Handler 的工作包含消息的发送和接收过程。

  1. 消息的发送:

    1. 通过 post 的一系列方法和 send 的一系列方法来实现,post 相关方法最终是通过 send 相关方法来实现的。
    2. Handler 发送消息只是向消息队列中插入一条消息,MessageQueue 的 next 方法就会返回这条信息给 Looper,Looper 收到消息后就会开始处理这条消息,最终消息由 Looper 交由 Looper 处理,即 Handler 的 dispatchMessage 方法会被调用,此时进入处理消息的阶段。
  2. 消息的处理

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

    流程如下:

    1. 首先,检查 Message 的 callback 是否为 null,不为null 就通过 handlerCallback 来处理消息。Message 的 callback 是一个 Runnable 对象,实际上就是 Handler 的 post 方法所传递的 Runnable 参数。

      private static void handleCallback(Message message) {
              message.callback.run();
          }
      
    2. 其次,检查 mCallback 是否为 null,不为 null 就调用 mCallback 的 handleMessage 方法来处理消息。

      public interface Callback {
              /**
               * @param msg A {@link android.os.Message Message} object
               * @return True if no further handling is desired
               */
              public boolean handleMessage(Message msg);
          }
      

      通过 Callback 可以如下方式创建 Handler 对象:Handler handler = new Handler(callback)。这样做的意义在于提供另外一种无需派生 Handler 子类并重写其 handlerMessage 方法处理具体消息的 Handler 的使用方式。

    3. 最后,调用 Handler 的 handlerMessage 方法来处理消息。只是一个空方法,需要我们派生 Handler 子类时,重写该方法。

      public void handleMessage(Message msg) {
          }
      
  3. 补充:

    1. Handler 还有一个特殊的构造方法,通过一个特定的 Looper 来构造 Handler

      public Handler(Looper looper) {
              this(looper, null, false);
          }
      
      public Handler(Looper looper, Callback callback, boolean async) {
              mLooper = looper;
              mQueue = looper.mQueue;
              mCallback = callback;
              mAsynchronous = async;
          }
      

      原文中说“通过这个构造方法可以实现一些特殊的功能”,然而因为我接触事件尚短,不清楚具体能做些什么。。。

    2. 默认构造方法中对 Looper 对象进行判断,如果为空的话,那么就会报这个常见的异常"Can't create handler inside thread that has not called Looper.prepare()"。

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

主线程的消息循环

消息循环模型

  1. Android 的主线程 ActivityThread 的入口方法 main 中,会通过 Looper.prepareMainLooper 来创建主线程的 Looper 以及 MessageQueue,并通过 Looper.loop 方法来开启主线程的消息循环。
  2. 主线程的消息循环开始之后,ActivityThread 需要一个 Handler 来和消息队列进行交互,这个 Handler 就是 ActivityThread.H,内部定义了一组消息模型,主要包含四大组件的启动和停止等过程。
  3. ActivityThread 通过 ApplicationThread 和 AMS 进行进程间通信,AMS 以进程间通信的方式完成 ActivityThread 的请求后回调 ApplicationThread 中的 Binder 方法,然后 ApplicationThread 会向 H 发送消息,H 收到消息后会将 ApplicationThread 中的逻辑切换到 ActivityThread 中去执行,即切换到主线程中执行。
  4. 以上即为主线程的消息循环模型。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容