Android 的消息机制主要指 Handler 的运行机制,先来看下 Handler 的一张运行架构图来对 Handler 有个大概的了解。
Handler 消息机制图:
Handler 类图:
以上图的解释:
这里从图中可以看到 Android 中 Handler 消息机制最重要的四个对象分别为 Handler 、Message 、MessageQueue 、Looper。
ThreadLocal 是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据, 数据存储以后,只有再指定线程中可以获取到存储的数据,对于其它线程来说则是无法获取到存储的对象。下面就是我们验证 ThreadLocal 存取是否是按照刚刚那样所说。
子线程中存,子线程中取
// 代码测试 new Thread("thread-1"){ @Override public void run() { ThreadLocal<String> mThread_A = new ThreadLocal(); mThread_A.set("thread-1"); System.out.println("mThread_A :"+mThread_A.get()); } }.start(); //打印结果 mThread_A :thread-1 复制代码
主线程中存,子线程取
//主线程中存,子线程取 final ThreadLocal<String> mThread_B = new ThreadLocal(); mThread_B.set("thread_B"); new Thread(){ @Override public void run() { System.out.println("mThread_B :"+mThread_B.get()); } }.start(); //打印结果 mThread_B :null 复制代码
主线程存,主线程取
//主线程存,主线程取 ThreadLocal<String> mThread_C = new ThreadLocal(); mThread_C.set("thread_C"); System.out.println("mThread_C :"+mThread_C.get()); //打印结果 mThread_C :thread_C 复制代码
结果是不是跟上面我们所说的答案一样,那么为什么会是这样勒?现在我们带着问题去看下 ThreadLocal
源码到底做了什么?
从上图可以 ThreadLocal 主要函数组成部分,这里我们用到了 set , get 那么就从 set , get 入手吧。
ThreadLocal set(T):
(图 1)
(图 2)
(图 3)
(图 四)
从 (图一) 得知 set 函数里面获取了当前线程,这里我们主要看下 getMap(currentThread) 主要干什么了?
从 (图二) 中我们得知 getMap 主要是从当前线程拿到 ThreadLocalMap 这个实例对象,如果当前线程的 ThreadLocalMap 为 NULL ,那么就 createMap ,这里的 ThreadLocalMap 可以暂时理解为一个集合对象就行了,它 (图四) 底层是一个数组实现的添加数据。
ThreadLocal T get():
这里的 get() 函数其实已经能够说明为什么在不同线程存储的数据拿不到了。因为存储是在当前线程存储的,取数据也是在当前所在的线程取得,所以不可能拿到的。带着问题我们找到了答案。是不是有点小激动呀?( ^▽^ )
这里我们就直接看源码,一下是我看源码的流程。
创建全局唯一的 Looper 对象和全局唯一 MessageQueue 消息对象。
Activity 中创建 Handler。
Handler sendMessage 发送一个消息的走向。
Handler 消息处理。
Looper 的阻塞主要是靠 MessageQueue 来实现的,在 MessageQueue -> next() nativePollOnce(ptr, nextPollTimeoutMillis) 进行阻塞 , 在 MessageQueue -> enqueueMessage() -> nativeWake(mPtr) 进行唤醒。主要依赖 native 层的 looper epoll 进制进行的。
阻塞和延时,主要是 next() 的 nativePollOnce(ptr , nextPollTimeoutMillis) 调用 native 方法来操作管道,由 nextPollTimeoutMillis 决定是否需要阻塞 , nextPollTimeoutMilis 为 0 的时候表示不阻塞 , 为 -1 的时候表示一直阻塞直到被唤醒,其它时间表示延时。
主要是指 enqueueMessage () @MessageQueue 进行唤醒。
简单的理解阻塞和唤醒就是在主线程的 MessageQueue 没有消息时,便阻塞在 Loop 的 queue.next() 中的 nativePollOnce() 方法里面,此时主线程会释放 CPU 资源进入休眠状态,直到下一个消息到达或者有消息的时候才触发,通过往 pipe 管道写端写入数据来唤醒主线程工作。
这里采用的 epoll 机制,是一种 IO 多路复用机制,可以同时监控多个描述符,当某个描述符就绪 (读或写就绪) , 则立刻通知相应程序进行读或者写操作,本质同步 I/O , 即读写是阻塞的。所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量的 CPU 资源。
主要指 enqueueMessage() 消息入队列(Message 单链表),上图代码对 message 对象池重新排序,遵循规则 ( when 从小到大) 。
此处 for 死循环退出情况分为两种
好了,到了这里 Handler 源码分析算是告一段落了,下面我们来看下面试中容易被问起的问题。
mThread 是主线程,这里会检查当前线程是否是主线程,那么为什么没有在 onCreate 里面没有进行这个检查呢?这个问题原因出现在 Activity 的生命周期中 , 在 onCreate 方法中, UI 处于创建过程,对用户来说界面还不可见,直到 onStart 方法后界面可见了,再到 onResume 方法后页面可以交互,从某种程度来讲, 在 onCreate 方法中不能算是更新 UI,只能说是配置 UI,或者是设置 UI 属性。 这个时候不会调用到 ViewRootImpl.checkThread () , 因为 ViewRootImpl 没有创建。 而在 onResume 方法后, ViewRootImpl 才被创建。 这个时候去交户界面才算是更新 UI。
setContentView 知识建立了 View 树,并没有进行渲染工作 (其实真正的渲染工作实在 onResume 之后)。也正是建立了 View 树,因此我们可以通过 findViewById() 来获取到 View 对象,但是由于并没有进行渲染视图的工作,也就是没有执行 ViewRootImpl.performTransversal。同样 View 中也不会执行 onMeasure (), 如果在 onResume() 方法里直接获取 View.getHeight() / View.getWidth () 得到的结果总是 0。
简单来说就是在主线程的 MessageQueue 没有消息时,便阻塞在 loop 的 queue.next() 中的 nativePollOnce() 方法,此时主线程会释放 CPU 资源进入休眠状态,直到下个消息到达或者有事务发生,通过往 pipe 管道写入数据来唤醒主线程工作。这里采用的是 epoll 机制,是一种 IO 多路复用机制。
如果在 Handler 构造方法里面直接 new Looper(), 可能是无法保证 Looper 唯一,只有用 Looper.prepare() 才能保证唯一性,具体可以看 prepare 方法。
因为一个线程只绑定一个 Looper ,所以在 Looper 构造方法里面初始化就可以保证 mQueue 也是唯一的 Thread 对应一个 Looper 对应一个 mQueue。
由 Looper 所在线程决定的。逻辑是在 Looper.loop() 方法中,从 MessageQueue 中拿出 message ,并且执行其逻辑,这里在 Looper 中执行的,因此有 Looper 所在线程决定。