三年前我写过一篇Android的消息机制的文章,时隔三年,重新再讲Handler,希望有新的理解和收获。
Android线程的通信
何为线程
Android的消息机制,是也就是Handler机制,是Android上进行线程间通信的最重要的框架。既然说到线程间通信,那么线程是什么呢?
教科书上说,进程是资源分配的最小单位,线程是CPU调度的最小单位。不过更好的说法是,进程和线程都是一个时间段的描述,是CPU工作时间段的描述,不过是颗粒大小不同。
知乎上某位答主的回答很好,我就大段引用一下:
CPU+RAM+各种资源(比如显卡,光驱,键盘,GPS, 等等外设)构成我们的电脑,但是电脑的运行,实际就是CPU和相关寄存器以及RAM之间的事情。
一个最最基础的事实:CPU太快,太快,太快了,寄存器仅仅能够追的上他的脚步,RAM和别的挂在各总线上的设备完全是望其项背。那当多个任务要执行的时候怎么办呢?轮流着来?或者谁优先级高谁来?不管怎么样的策略,一句话就是在CPU看来就是轮流着来。
一个必须知道的事实:执行一段程序代码,实现一个功能的过程介绍 ,当得到CPU的时候,相关的资源必须也已经就位,就是显卡啊,GPS啊什么的必须就位,然后CPU开始执行。这里除了CPU以外所有的就构成了这个程序的执行环境,也就是我们所定义的程序上下文。当这个程序执行完了,或者分配给他的CPU执行时间用完了,那它就要被切换出去,等待下一次CPU的临幸。在被切换出去的最后一步工作就是保存程序上下文,因为这个是下次他被CPU临幸的运行环境,必须保存。
串联起来的事实:前面讲过在CPU看来所有的任务都是一个一个的轮流执行的,具体的轮流方法就是:先加载程序A的上下文,然后开始执行A,保存程序A的上下文,调入下一个要执行的程序B的程序上下文,然后开始执行B,保存程序B的上下文。。。。
进程和线程就是这样的背景出来的,两个名词不过是对应的CPU时间段的描述,名词就是这样的功能。
进程就是包换上下文切换的程序执行时间总和 = CPU加载上下文+CPU执行+CPU保存上下文
线程是什么呢?进程的颗粒度太大,每次都要有上下文的调入,保存,调出。如果我们把进程比喻为一个运行在电脑上的软件,那么一个软件的执行不可能是一条逻辑执行的,必定有多个分支和多个程序段,就好比要实现程序A,实际分成 a,b,c等多个块组合而成。那么这里具体的执行就可能变成:程序A得到CPU =》CPU加载上下文,开始执行程序A的a小段,然后执行A的b小段,然后再执行A的c小段,最后CPU保存A的上下文。这里a,b,c的执行是共享了A的上下文,CPU在执行的时候没有进行上下文切换的。这里的a,b,c就是线程,也就是说线程是共享了进程的上下文环境,的更为细小的CPU时间段。
以上引用自线程和进程的区别是什么? - zhonyong的回答 - 知乎
线程的状态
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。 - 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
后面关于消息队列,就涉及到线程的阻塞状态。
我曾经写过一篇关于如何打印出java线程的六种状态的文章,可以参考:
线程通信的方式
Handler机制,这即是Android上进行线程间通信的最重要的方法,也是我们今天要讲的重点。
Broadcast广播,广播机制可用于进程/线程间通信
共享内存,例如单例,类成员变量等
文件/数据库
传统的java技术,例如java.io包的管道(Pipes),Object的信号量(Signalling), 阻塞队列(BlockingQueue)等
这一篇博文讲的很详细:
Chapter 4. Thread Communication
Handler消息机制
概述
Android系统中将通信的消息封装成Message对象,并且配备有专门的Handler去做事件的处理。而Message存放在一个叫MessageQueue的消息队列当中被分发处理的,而保证消息队列中消息不断被分发出去,正是Looper对象所做的事。
Looper
线程中如何启用Looper
正如我们概述中最后说的,是Looper所做的事。我们先直观的看Looper在线程中是如何启用的
1 | public class LooperThread extends Thread { |
很简单,那Looper本身到底是什么?
1 | public final class Looper { |
可以看到Looper本身持有线程本身,持有消息队列,还持有线程局部变量ThreadLocal,并且通过线程局部变量持有Looper对象本身。
ThreadLocal
穿插简单讲一下ThreadLocal是什么?
ThreadLocal也就是线程局部变量,为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。
从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
更多内容可以参考:深入研究java.lang.ThreadLocal类
Looper.prepare()
回到Looper上来,我们来看一下Looper的prepare()方法到底做了什么
1 | /** Initialize the current thread as a looper. |
看起来很简单哈,也就是创建了Looper对象,并通过线程局部变量存了下来。
Looper.loop()
前面的介绍我们知道Looper的loop()方法会启动消息队列的循环,来进行消息的实际分发,我们来看一下looper里面具体做的事:
代码很长,我们拆分成两部分看,先看外壳:
1 | /** |
准备工作没什么可说的,我们直接看for循环里面的代码
1 | for (;;) { |
代码很长,我们看几个关键点,首先是从消息队列中读取下一个消息
1 | Message msg = queue.next(); // might block |
这里因为MessageQueue本身是个堵塞队列,所以这里可能会阻塞住,next()方法里面具体怎么执行的,待会说。
接着就是log和trace操作没什么可说的,下一个关键点是消息的分发:
1 | //target就是Handler,可以看到这里通过调用Handler去分发消息事件 |
最后就是回收Message资源
1 | msg.recycleUnchecked(); |
其实可以看到,整个loop()方法里面,核心的代码其实可以简化为:
1 | public static void loop() { |
MessageQueue
看完Looper的代码,我们顺着留下来的MessageQueue.next()方法,来看消息队列的实现。
MessageQueue初始化
1 | public final class MessageQueue { |
可以看到,MessageQueue里面有相当多的native代码,其实Android的消息机制中,除了Java层的handler机制外,还包括native的AHandler机制。对应着Java层的Looper和Message,就有对应的ALooper和AMessage.
其中nativeInit()方法对应的native方法实现如下,创建了NativeMessageQueue,并将这个对象的指针赋给了Java层的mPtr。
1 | static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) { |
MessageQueue读取下一条消息
来看next()方法
1 | Message next() { |
不出意外,这里的方法也很长,同样的,我们分析下核心的代码执行了些什么操作
1 | nativePollOnce(ptr, nextPollTimeoutMillis); |
1 | static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj, |
pollOnce()最终都是通过 Linux 的 epoll 模型来实现的。pollOnce() 通过等待被激活,然后从消息队列中获取消息。对应的也有一个wake()方法,则是激活处于等待状态的消息队列,通知它有消息到达了。这就是典型的生产者-消费者模型.
接着往下看,nativePollOnce()之后的大段逻辑就是如何分发Message,这里就不多说了。
MessageQueue插入消息
对应的方法是enqueueMessage(Message msg, long when)
1 | boolean enqueueMessage(Message msg, long when) { |
这里的核心代码nativeWake(mPtr)前面也提到过了,和nativePollOnce(ptr, nextPollTimeoutMillis)一样,都是基于Linux 的 epoll 模型来实现的。
1 | static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) { |
关于Android中Natvie层消息机制,这篇文章我认为讲的不错:
Android多线程分析之四:MessageQueue的实现
总结
首先我们了解了线程,进程的基本概念,Java线程的六种状态,Android中线程间通信的几种基础方式等。随后我们结合了源码,着重介绍了Android中Handler的运行机制,在分析源码的过程中,我们发现了Android系统中,除了Java层的消息机制外,Natvie层也有一层消息机制,并且内部是通过Linux的epoll模型来实现的。而消息队列不断读取消息的本质,就是线程的阻塞。