Android开发的艺术探索第十章

第10章 Android的消息机制

Handler并不是 专门用于更新UI的,它只是常被开发者用来更新UI。Android的消息机制主要是指Handler的运行机制,Handler的运行需要底层的 MessageQueue和Looper的支撑。
MessageQueue消息队列,内部存储了一组消息,以队列的形式对外提供插入和删除的工作,采用单链表的数据结构来存储消息列表。
由于MessageQueue只是一个消息 的存储单元,它不能去处理消息,而Looper就填补了这个功能,Looper会以无限循环的形 式去查找是否有新消息,如果有的话就处理消息,否则就一直等待着。Looper中还有一个 特殊的概念,那就是ThreadLocal,ThreadLocal并不是线程,它的作用是可以在每个线程 中存储数据。我们知道,Handler创建的时候会采用当前线程的Looper来构造消息循环系 统,那么Handler内部如何获取到当前线程的Looper呢?这就要使用ThreadLocal了, ThreadLocal可以在不同的线程中互不干扰地存储并提供数据,通过ThreadLocal可以轻松 获取每个线程的Looper。当然需要注意的是,线程是默认没有Looper的,如果需要使用 Handler就必须为线程创建Looper。我们经常提到的主线程,也叫UI线程,它就是 ActivityThread,ActivityThread被创建时就会初始化Looper,这也是在主线程中默认可以 使用Handler的原因。

10.1 Android的消息机制概述

Android的消息机制主要是指Handler的运行机制以及Handler所附带的 MessageQueue和Looper的工作过程,这三者实际上是一个整体。
Handler的主要作用是将一个任务切换到某个指定的线程中 去执行,那么Android为什么要提供这个功能呢?或者说Android为什么需要提供在某个具 体的线程中执行任务这种功能呢?这是因为Android规定访问UI只能在主线程中进行,如 果在子线程中访问UI,那么程序就会抛出异常。ViewRootImpl对UI操作做了验证,这个 验证工作是由ViewRootImpl的checkThread方法来完成的,如下所示:
Android开发的艺术探索第十章_第1张图片
针对checkThread方法中抛出的异常信息,相信读者在开发中都曾经遇到过。由于这 一点的限制,导致必须在主线程中访问UI,但是Android又建议不要在主线程中进行耗时操作,否则会导致程序无法响应即ANR。考虑一种情况,假如我们需要从服务端拉取一 些信息并将其显示在UI上,这个时候必须在子线程中进行拉取工作,拉取完毕后又不能 在子线程中直接访问UI,如果没有Handler,那么我们的确没有办法将访问UI的工作切换 到主线程中去执行。因此,系统之所以提供Handler,主要原因就是为了解决在子线程中 无法访问UI的矛盾。
系统为什么不允许在子线程中访问UI呢?这是因为Android的UI控 件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态, 那为什么系统不对UI控件的访问加上锁机制呢?缺点有两个:首先加上锁机制会让UI访 问的逻辑变得复杂;其次锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。鉴于这两个缺点,最简单且高效的方法就是采用单线程模型来处理UI操作,对于开 发者来说也不是很麻烦,只是需要通过Handler切换一下UI访问的执行线程即可。
Handler创 建时会采用当前线程的Looper来构建内部的消息循环系统,只需要为当前线程创建Looper即可,或者在一个有Looper的线程中创建Handler也行。
Handler创建完毕后,这个时候其内部的Looper以及MessageQueue就可以和Handler一 起协同工作了,然后通过Handler的post方法将一个Runnable投递到Handler内部的Looper中 去处理,也可以通过Handler的send方法发送一个消息,这个消息同样会在Looper中去处 理。其实post方法最终也是通过send方法来完成的,接下来主要来看一下send方法的工作 过程。当Handler的send方法被调用时,它会调用MessageQueue的enqueueMessage方法将这 个消息放入消息队列中,然后Looper发现有新消息到来时,就会处理这个消息,最终消息 中的Runnable或者Handler的handleMessage方法就会被调用。注意Looper是运行在创建 Handler所在的线程中的,这样一来Handler中的业务逻辑就被切换到创建Handler所在的线 程中去执行了,这个过程可以用图10-1来表示。
Android开发的艺术探索第十章_第2张图片

10.2 Android的消息机制分析

10.2.1 ThreadLocal的工作原理

ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据, 数据存储以后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取 到数据。在日常开发中用到ThreadLocal的地方较少,但是在某些特殊的场景下,通过 ThreadLocal可以轻松地实现一些看起来很复杂的功能,这一点在Android的源码中也有所 体现,比如Looper、ActivityThread以及AMS中都用到了ThreadLocal。具体到ThreadLocal 的使用场景,这个不好统一来描述,一般来说,当某些数据是以线程为作用域并且不同线 程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。比如对于Handler来说,它 需要获取当前线程的Looper,很显然Looper的作用域就是线程并且不同线程具有不同的 Looper,这个时候通过ThreadLocal就可以轻松实现Looper在线程中的存取。如果不采用 ThreadLocal,那么系统就必须提供一个全局的哈希表供Handler查找指定线程的Looper, 这样一来就必须提供一个类似于LooperManager的类了,但是系统并没有这么做而是选择 了ThreadLocal,这就是ThreadLocal的好处。
ThreadLocal之所以有这么奇妙的效果,是因为不同线 程访问同一个ThreadLocal的get方法,ThreadLocal内部会从各自的线程中取出一个数组, 然后再从数组中根据当前ThreadLocal的索引去查找出对应的value值。很显然,不同线程 中的数组是不同的,这就是为什么通过ThreadLocal可以在不同的线程中维护一套数据的副本并且彼此互不干扰
这里由于所用的API版本与书中不一致,所以以下分析是自己分析与书中不同。
下面分析ThreadLocal的内部实 现,ThreadLocal是一个泛型类,它的定义为public class ThreadLocal,只要弄清楚 ThreadLocal的get和set方法就可以明白它的工作原理:
Android开发的艺术探索第十章_第3张图片
在上面的set方法中,首先会通过getMap(t)方法来获取当前线程中的ThreadLocalMap在获取ThreadLocal数据,如何获取呢?其实获取的方式也是很简单的,在Thread类的内部有一个成员专门用于存储线程的ThreadLocal的数据: ThreadLocal.ThreadLocalMap threadLocals,因此获取当前线程的 ThreadLocal数据就变得异常简单了。如果threadLocals的值为null,那么就需要对其进行初始化,初始化后再将ThreadLocal的值进行存储。下面看一下ThreadLocal的值到底是如何在threadLocals中进行存储的。
ThreadLocalMaps内部有一个数组:private Entry[] table,ThreadLocal的值就存在在这个table数组中。

Android开发的艺术探索第十章_第4张图片上面的代码实现了数据的存储过程,这里不去分析它的具体算法,但是我们可以得出 一个存储规则,那就是ThreadLocal的值在table数组中的存储位置总是为ThreadLocal的 reference字段所标识的对象的下一个位置,比如ThreadLocal的reference对象在table数组中 的索引为index,那么ThreadLocal的值在table数组中的索引就是index+1。最终ThreadLocal 的值将会被存储在table数组中:table[index + 1] = value。
Android开发的艺术探索第十章_第5张图片
从ThreadLocal的set和get方法可以看出,它们所操作的对象都是当前线程的 ThreadLocalMap对象的table数组,因此在不同线程中访问同一个ThreadLocal的set和get方法, 它们对ThreadLocal所做的读/写操作仅限于各自线程的内部,这就是为什么ThreadLocal可 以在多个线程中互不干扰地存储和修改数据理解ThreadLocal的实现方式有助于理解 Looper的工作原理。

10.2.2 消息队列的工作原理

消息队列在Android中指的是MessageQueue,MessageQueue主要包含两个操作:插入 和读取。读取操作本身会伴随着删除操作,插入和读取对应的方法分别为enqueueMessage 和next,其中enqueueMessage的作用是往消息队列中插入一条消息,而next的作用是从消 息队列中取出一条消息并将其从消息队列中移除。
它是通过一个单链表的数据结构来维护消息列表, 单链表在插入和删除上比较有优势。
Android开发的艺术探索第十章_第6张图片
从enqueueMessage的实现来看,它的主要操作其实就是单链表的插入操作
Android开发的艺术探索第十章_第7张图片
Android开发的艺术探索第十章_第8张图片
Android开发的艺术探索第十章_第9张图片
可以发现next方法是一个无限循环的方法,如果消息队列中没有消息,那么next方法 会一直阻塞在这里。当有新消息到来时,next方法会返回这条消息并将其从单链表中移 除。

10.2.3 Looper的工作原理

Looper在 Android的消息机制中扮演着消息循环的角色,具体来说就是它会不停地从MessageQueue 中查看是否有新消息,如果有新消息就会立刻处理,否则就一直阻塞在那里。
在构造方法中它会创建一个MessageQueue即消息队列,然后将当前线程 的对象保存起来,
通过Looper.prepare()即可为当前线程创建一个Looper, 接着通过Looper.loop()来开启消息循环,
**Looper的loop方法的工作过程也比较好理解,loop方法是一个死循环,唯一跳出循环的方式是MessageQueue的next方法返回了null。**当Looper的quit方法被调用时,Looper就会 调用MessageQueue的quit或者quitSafely方法来通知消息队列退出,当消息队列被标记为退 出状态时,它的next方法就会返回null。也就是说,Looper必须退出,否则loop方法就会无 限循环下去。loop方法会调用MessageQueue的next方法来获取新消息,而next是一个阻塞 操作,当没有消息时,next方法会一直阻塞在那里,这也导致loop方法一直阻塞在那里。 如果MessageQueue的next方法返回了新消息,Looper就会处理这条消息: msg.target.dispatchMessage(msg),这里的msg.target是发送这条消息的Handler对象,这样 Handler发送的消息最终又交给它的dispatchMessage方法来处理了。但是这里不同的是, Handler的dispatchMessage方法是在创建Handler时所使用的Looper中执行的,这样就成功 地将代码逻辑切换到指定的线程中去执行了。

10.2.4 Handler的工作原理

Handler的工作主要包含消息的发送和接收过程,
Android开发的艺术探索第十章_第10张图片
Handler发送消息的过程仅仅是向消息队列中插入了一条消息, MessageQueue的next方法就会返回这条消息给Looper,Looper收到消息后就开始处理了, 最终消息由Looper交由Handler处理,即Handler的dispatchMessage方法会被调用,这时 Handler就进入了处理消息的阶段。
Android开发的艺术探索第十章_第11张图片
Android开发的艺术探索第十章_第12张图片

10.3 主线程的消息循环

Android的主线程就是ActivityThread,主线程的入口方法为main,在main方法中系统 会通过Looper.prepareMainLooper()来创建主线程的Looper以及MessageQueue,并通过 Looper.loop()来开启主线程的消息循环,这个过程如下所示。
Android开发的艺术探索第十章_第13张图片
主线程的消息循环开始了以后,ActivityThread还需要一个Handler来和消息队列进行 交互,这个Handler就是ActivityThread.H,它内部定义了一组消息类型,主要包含了四大 组件的启动和停止等过程
ActivityThread通过ApplicationThread和AMS进行进程间通信,AMS以进程间通信的方式完成ActivityThread的请求后会回调ApplicationThread中的Binder方法,然后 ApplicationThread会向H发送消息,H收到消息后会将ApplicationThread中的逻辑切换到 ActivityThread中去执行,即切换到主线程中去执行,这个过程就是主线程的消息循环模型。
此处要说一下关于网上很多人问Android中为什么主线程不会因为Looper.loop的死循环卡死?
在上面主线程代码中: thread.attach(false);
便会创建一个Binder线程(具体是指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message发送给主线程。
Looper.loop: 此处使用了pipe管道机制和epoll
pipe机制,在没有消息时阻塞线程并进入休眠释放cpu资源,有消息时唤醒线程
Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce() 方法里
Android开发的艺术探索第十章_第14张图片
参考文章https://blog.csdn.net/u013626215/article/details/88796172

你可能感兴趣的:(安卓开发,笔记,安卓基础)