文章目录
Handler 概述
Handler 消息机制是我们接触 Android 开发每时每刻都会遇到的组件,主线程的运行也是基于Handler,主线程同样的也是一段可执行的程序,如果主线程退出,程序也无法运行。无论是四大组件的生命周期,还是平常的开发发送消息通知,我们都会使用到它;Handler 是让整个 Android 程序能够正常运行且是必不可少的重要组件。它主要由四个部分组成:Handler、MessageQueue、Looper 和 Message。
在平常开发中,我们直接 new Handler() 就可以直接使用了,这是因为当 Zygote 创建了应用程序并且创建主线程时,在 ActivityThread 的 main() 入口已经为我们默认创建了主线程的 Looper 和 MessageQueue:
ActivityThread.java
public static void main(String[] args) {
// ...
// 创建主线程Looper
Looper.prepareMainLooper();
// ...
// 启动消息循环
Looper.loop();
// ...
}
Looper.java
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
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));
}
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed); // 创建了消息队列
mThread = Thread.currentThread();
}
当启动消息循环即调用了 Looper.loop(),Looper、MessageQueue 和 Handler 才能协同工作,消息机制才开始运转。
ThreadLocal
从上面的概述中主线程创建 Looper 的代码,Looper 创建后是被存储到了 ThreadLocal。
ThreadLocal 简单说就是 一个作用域以线程为单位的数据存储工具。ThreadLocal 可以做到 线程与线程之间的数据读取互不干扰。
为什么说它是以线程为单位?我们可以从 ThreadLocal 最常用的 set() 或 get() 中了解到:
ThreadLocal.java
public void set(T value) {
Thread t = Thread.currentThread(); // 获取调用ThreadLocal时所在线程
ThreadLocalMap map = getMap(t); // 以线程为key获取到存储的数据
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 通过 key=ThreadLocal 获取 value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; // 从线程的变量获取 ThreadLocalMap
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue); // key=ThreadLocal
}
可以看到,Thread 是持有的 ThreadLocalMap,key 是 ThreadLocal,value 是需要在该线程存放的对象。
ThreadLocal 的使用场景如下:
-
当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候
-
复杂逻辑下的对象传递。比如监听器的传递,有些时候一个线程中的任务过于复杂,这可能表现为函数调用栈比较深以及代码入口的多样性,我们可以在需要使用监听器的线程使用 threadLocal.set() 存储,需要使用时 threadLocal.get() 获取
在 Handler 中,ThreadLocal 存储的 Looper,这让我们可以在主线程获取到唯一的 Looper 和 MessageQueue。
MessageQueue
MessageQueue 作为消息机制的部件之一,内部是使用单链表的数据结构,它主要的作用是插入消息以及取出消息。
当我们使用 handler.sendXxx() 或 handler.post() 发送一条消息时,在 Handler 会将 Message 插入到消息队列中:
Handler.java
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis); // 将消息插入到消息队列中
}
MessageQueue 每次插入数据都会按时间顺序进行插入,也就是说 MessageQueue 中的 Message 都是按照时间排好序的,这样就能从前往后一个个的取出 Message 即可,Message 按时间先后顺序被消费。
MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
// ...
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
// 需要立即执行的同步消息插入到当前链表头,并唤醒线程消费消息
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// ...
}
// ...
// 唤醒线程处理消息
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
当线程被唤醒时,在 Looper.loop()
的 queue.next()
会接收到消息并处理消息:
Looper.java
public static void loop() {
// ...
for (;;) {
Message msg = queue.next(); // 获取消息
// ...
}
}
MessageQueue.java
Message next() {
// ...
int nextPollTimeoutMillis = 0;
for (;;) {
// ...
// 没有消息时该方法会阻塞
// nextPollTimeoutMillis更新链表中还没到指定时间处理的消息的时间
nativePollOnce(ptr, nextPollTimeoutMillis);
// ...
if (msg != null) {
// 线程被唤醒时,如果链表中处理消息的时间还没到,更新该消息的剩余时间
if (now < msg.when) {
nextPollTimeoutMills = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// 获取到要处理的消息并返回给上层的Looper.loop()处理消息
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
// ...
}
}
可以发现,Looper.loop() 是一个死循环,MessageQueue 的 next() 内部也是一个死循环。这里使用了 Linux 的 epoll 机制,当没有接收到消息时,next() (更确切说是 nativePollOnce())会一直阻塞让线程休眠,收到新的消息时,线程才会被唤醒然后处理消息。
Message
在消息机制中 Message 是数据的载体,但同时也需要注意,消息机制在运行时是生产消费 Message 的频率是很高的,如果频繁的创建销毁 Message 对象,就可能会发生内存抖动的情况。
分析源码可以得知,Message 确实不会直接 new 对象,而是使用了享元模式复用 Message:
public final class Message implements Parcelable {
int flags;
long when;
Bundle data;
Handler target;
Runnable callback;
Message next;
public static final Object sPoolSync = new Object();
private static Message sPool;
private static int sPoolSize = 0;
private static final MAX_POOL_SIZE = 50;
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // 清除 Message 被使用的标记
sPoolSize--;
return m;
}
}
return new Message();
}
public void recycle() {
...
recycleUnchecked();
}
void recycleUnchecked() {
// Mark the message as in use while it remains in the recycled object pool.
// Clear out all other details.
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = -1;
when = 0;
target = null;
callback = null;
data = null;
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
}
Message 回收池也是一个单链表,需要一个唯一的静态引用链表头 sPool,用静态变量 sPoolSize 记录链表的节点数量,MAX_POOL_SIZE 限定最多节点数量是 50。
在 MessageQueue 有变量 mMessages,会在消息插入和获取时即 enqueueMessage() 和 next() 赋值和获取。
为什么 Message 会用单链表的数据结构?不用数组、List、Map 这类数据结构呢?
因为获取消息并不需要知道使用的哪个 Message,只要你是 Message 就可以使用,所以无论是数组、List 还是 Map 都具有查找的功能,这种场景并不需要查找功能,所以最适合的数据结构是链表,而且是单向链表。
Looper
Looper 在消息机制中扮演着消息循环的角色,具体来说就是它会不停地从 MessageQueue 中查看是否有新消息,如果有新消息就会立刻处理,否则就一直阻塞在那里。
创建 Looper 的时候会同时创建出 MessageQueue,Looper 也会存储到 ThreadLocal,由 Looper 的构造方法可以看出:
Looper.java
private static void prepare() {
prepare(true);
}
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created pre thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
private Looper(Boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
Handler 的工作需要 Looper,没有 Looper 的线程如果尝试启动消息循环即调用 Looper.loop() 就会报错,所以在其他线程中使用 Handler 前需要调用 Looper.prepare() 创建 Looper:
Looper.java
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
// ...
}
new Thread("Thread#2"){
public void run() {
Looper.prepare(); // 在其他子线程需要手动的创建Looper才能使用
Handler handler = new Handler(Looper.myLooper()); // Handler在子线程处理耗时操作
Looper.loop();
// 当子线程Looper不使用时应该及时looper.quit()退出循环
}
}.start();
Looper中的其他方法:
-
prepareMainLooper():该方法主要给主线程也就是 ActivityThread 创建 Looper 使用,本质也是通过 prepare() 实现
-
getMainLooper():通过该方法可以在任何地方获取到当前进程的主线程 Looper
-
quit():调用该方法会直接退出 Looper,如果在其他线程中使用 Handler,最好是在不使用 Handler 时退出 Looper,否则线程就会一直阻塞
-
quitSafely():调用该方法会设定一个退出标记,把 MessageQueue 中的消息处理完毕后才退出 Looper
Looper 最重要的一个方法是 loop(),只有调用了 loop() 之后,消息循环才会真正的起作用:
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// ...消息处理逻辑
}
}
Handler
Handler 的工作主要包含消息的发送和接收过程,消息的发送可以调用 post() 或 sendXxx();post() 最终是通过 sendMessageAtTime() 将消息插入 MessageQueue:
public final boolean sendMessage(Message msg) {
return sendMessageDelayed(msg, 0);
}
public final boolean sendMessageDelayed(Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this; // 给 msg.target 传递了当前 Handler 对象
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis); // 将消息插入到 MessageQueue 队列中
}
Handler 发送消息的过程仅仅是向消息队列中插入了一条消息,MessageQueue 的 next() 就会返回这条消息给 Looper,Looper 收到消息后就开始处理了,最终消息由 Looper 交由 Handler 的 dispatchMessage() 处理:
Looper.java
public static void loop() {
// ...
// 处理消息,msg.target就是Handler,即会调用Handler.dispatchMessage()
// Handler的dispatchMessage()是在创建Handler时所使用的Looper中执行的
// 这样就成功地将代码逻辑切换到制定的线程中去执行了
msg.target.dispatchMessage(msg);
// ...
}
Handler.java
public void dispatchMessage(Message msg) {
// msg.callback是一个Runnable对象,实际上就是Handler的post()所传递的Runnable参数
if (msg.callback != null) {
handleCallback(msg);
} else {
// 这里的mCallback是在创建Handler对象的时候传递的
// 当我们不想派生子类时,就可以通过Callback实现
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
// 处理Handler.post(Runnable)的消息
private static void handleCallback(Message message) {
message.callback.run(); // message.callback就是Runnable,最终回调回去
}
/**
* Callback interface you can use when instantiating a Handler to avoid
* having to implement your own subclass of Handler.
*/
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);
}
public Handler(Callback callback) {
this(callback, false);
}
消息机制的处理流程
上面简单的分析了 Handler、MessageQueue 和 Looper 的工作原理,我们简单梳理下消息机制的工作流程。
-
使用 Handler 的 post() 或 sendXxx() 发送 Message 时,会将消息插入到消息队列 MessageQueue,调用 MessageQueue 的 enqueueMessage()
-
线程从阻塞状态被唤醒,此时会通过 MessageQueue 的 next() 获取到待处理的消息,因为 Looper 是在所在线程创建的,所以在这里处理的消息将会被切换到所在的线程上
-
处理后的消息返回给 Handler 接收,调用 msg.target.dispatchMessage(),Handler 的handleMessage() 获取处理消息
消息机制的基本流程如下图所述:
可以发现消息机制从本质上就是一个生产消费者模型,MessageQueue 就是一个传送带,Looper 是生产者,Handler 是消费者,可以用下图表示:
epoll
Android 是基于 Linux 开发的一套嵌入式系统,常见的有三种 I/O 多路复用机制:select、poll 和 epoll。I/O 多路复用就是通过一种机制,一个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
Handler 的底层就是使用 Linux 的 epoll 实现的。那什么是 epoll?已经有 select、poll 这种系统调用方案,为什么要使用 epoll?
select 和 poll
I/O 复用虽然能同时监听多个文件描述符,但它是阻塞监听的,当有多个文件描述符同时就绪时,若不采取额外措施,程序只能按照顺序依次处理其中的每一个就绪事件。
select 是一个函数监听文件描述符,调用后 select 会阻塞,直到有描述符就绪或者超时,函数返回后就可以遍历描述符,找到就绪的描述符。
poll 和 select 一样函数返回后需要轮询获取就绪的描述符。
select 的缺点:
-
单个进程能够监听的文件描述符数量存在最大限制,通常是 1024;但由于 select 采用轮训的方式扫描文件描述符,数量越多性能也越差
-
内核/用户空间内存拷贝问题,select 需要复制大量的句柄数据结构,产生巨大的开销
-
select 返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件
-
select 的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行 IO 操作,那么之后每次 select 调用还是会将这些文件描述符通知进程
相比 select,poll 使用链表保存文件描述符,虽然没有了监听文件数量限制,但其他三个缺点依然存在。
select 和 poll 最主要的两个问题:
-
需要遍历所有文件描述符,文件描述符数量越多性能越差
-
内核空间和用户空间的内存拷贝开销大
epoll
epoll 是 Linux 2.6 内核的一个新的系统调用,epoll 在设计之初,就是为了解决上述 select 和 poll 的问题。epoll 使用红黑树数据结构,在将句柄注册到红黑树时已经提前排序,所以随着文件描述符的增长同样能保持良好的时间复杂度。
-
select 和 poll 监听文件描述符 list,线性查找时间复杂度为 O(n)
-
epoll 使用红黑树查找时间复杂度为 O(1)
下面展示了文件描述符的量级和 CPU 耗时:
Number of File Descriptions | poll() CPU time | select() CPU time | epoll() CPU time |
---|---|---|---|
10 | 0.61 | 0.73 | 0.41 |
100 | 2.9 | 3 | 0.42 |
1000 | 35 | 35 | 0.53 |
10000 | 990 | 930 | 0.66 |
并且 epoll 使用 mmap 让内核空间和用户空间减少内存拷贝。
Android 选用 epoll 主要有以下几点:
-
epoll 是使用的红黑树的数据结构,当 app 调用 Looper.prepare() 时会注册句柄到红黑树,有事件处理时直接从红黑树查找不用每次都遍历所有句柄提高效率
-
epoll 只要少量的用户空间和内核空间的句柄拷贝
-
epoll 是通过内核与用户 mmap 同一块内存,避免了无谓的内存拷贝
-
IO 性能不会随着监听的 fd 数量增长而下降
epoll 阻塞唤醒的简单理解
为什么阻塞能让 CPU 不占用时间片?阻塞的原理是什么?
用一个例子讲解阻塞为什么不占用 CPU 资源。
// 创建socket
int socket = socket(AF_INET, SOCK_STREAM, 0);
// 绑定
bind(s, ...);
// 监听
listen(s, ...);
// 连接客户端
int c = accept(s, ...);
// 接收客户端数据
recv(...);
// ....
上面是一段最基础的网络编程代码,创建了 Socket 对象,依次绑定、监听、连接客户端,recv() 是阻塞方法,当程序运行到 recv() 时,它会一直等待,直到接收到数据才往下执行。
1、工作队列
操作系统为了支持多任务,实现了进程调度的功能,会把进程分为“运行”和“等待”等几种状态。
运行状态是进程获得CPU使用权,正在执行代码的状态;等待状态是阻塞状态,接收到数据后才重新回到运行状态。
操作系统会分时执行各个运行状态的进程,由于速度很快,看上去就像是同时执行多个任务。
下图的计算机中运行着A、B、C三个进程,三个进程都被操作系统的工作队列所引用处于运行状态:
2、等待队列
当进程 A 执行到创建 Socket 的语句时,操作系统会创建一个由文件系统管理的 Socket 对象,Socket 对象包含了发送缓冲区、接收缓冲区与等待队列等成员。等待队列是非常重要的结构,它指向所有需要等待该 Socket 事件的进程。
当程序执行到 recv() 时,操作系统会将进程 A 从工作队列移动到该 Socket 的等待队列中:
由于工作队列只剩下了进程 B 和 C,依据进程调度,CPU 会轮流执行这两个进程的程序,不会执行进程 A 的程序。所以进程 A 被阻塞,不会往下执行代码,也不会占用 CPU 资源。
阻塞的原理其实就是进程被放到等待队列中处于休眠,在进程调度时,不会分配 CPU 时间片给在等待队列休眠的进程,自然也就不会占用 CPU 资源了。
3、唤醒进程
当系统接收到中断信号后,CPU 执行中断程序将数据写入到对应的 Socket 接收缓冲区,然后再唤醒进程,并重新将进程 A 放入到工作队列中:
注:操作系统添加等待队列只是添加了对这个 “等待中” 进程的引用,以便在接收到数据时获取进程对象、将其唤醒,而非直接将进程管理纳入自己之下。上图为了方便说明,直接将进程挂到等待队列之下
唤醒其实就是将等待队列的进程重新放回工作队列,从休眠等待状态回到运行状态。
epoll 和 Handler 的关系
上面简单的讲解了 epoll ,Android 的消息机制是基于 epoll 实现的。
epoll 提供了三个函数:
-
epoll_create():创建一个 epoll 实例,也就是文件描述符
-
epoll_ctl():将监听的文件描述符添加到 epoll 实例中
-
epoll_wait():等待 epoll 事件从 epoll 实例中发生,并返回事件以及对应文件描述符
在 ActivityThread 中 main() 被调用时,会创建主线程的 Looper 并创建 MessageQueue,而创建 MessageQueue 时就是通过 JNI 调用了 epoll_create(),会返回一个指针维系上层和底层的消息处理:
MessageQueue.java
public final class MessageQueue {
MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit(); // JNI
}
private native static long nativeInit();
}
android_os_MessageQueue.cpp
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
nativeMessageQueue->incStrong(env);
return reinterpret_cast<jlong>(nativeMessageQueue);
}
NativeMessageQueue::NativeMessageQueue()
: mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
mLooper = Looper::getForThread();
if (mLooper == NULL) {
mLooper = new Looper(false); // 创建 Native Looper
Looper::setForThread(mLooper);
}
}
Looper.cpp
Looper::Looper(bool allowNonCallbacks) :
mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
mPolling(false), mEpollFd(-1), mEpollRebuildRequired(false),
mNextRequestSeq(0), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
mWakeEventFd = eventfd(0, EFD_NONBLOCK); // 用于唤醒的文件描述符
AutoMutex _l(mLock);
rebuildEpollLocked();
}
void Looper::rebuidEpollLocked() {
if (mEpolled >= 0) {
close(mEpollFd);
}
// 创建 epoll
mEpollFd = epoll_create(EPOLL_SIZE_HINT);
struct epoll_event eventItem;
memset(&eventItem, 0, sizeof(epoll_event));
eventItem.events = EPOLLIN; // 可读事件
eventItem.data.fd = mWakeEventFd;
// 将唤醒事件 mWakeEventFd 添加到 epoll 实例 mEpollFd
// 调用 epoll_wait 阻塞后,一旦向该文件操作符所代表的文件中写入数据,那么 epoll_wait 便会返回
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);
...
}
当 MessageQueue 没有消息时,便阻塞在 Looper.loop() 的 queue.next(),更具体说是阻塞在 nativePollOnce(),此时主线程会释放 CPU 资源进入休眠状态。阻塞休眠实际是调用的 epoll_wait():
MessageQueue.java
Message next() {
// ...
for (;;) {
// ...
// 如果没有消息将会阻塞并释放CPU资源,等待唤醒
nativePollOnce(ptr, nextPollTimeoutMillis);
// ...
// 当前线程的MessageQueue的消息都执行完成了
// 就执行IdleHandler通过Looper.myQueue().addIdleHandler()添加的IdleHandler
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null;
boolean keep = flase;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wf(TAG, "IdleHandler threw exception", t);
}
if (!keep) {
synchronized(this) {
mIdleHandlers.remove(idler);
}
}
}
}
}
android_os_MessageQueue.cpp
void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
...
mLooper->pollOnce(timeoutMillis); // 调用的 Looper.cpp 的 pollOnce()
...
}
Looper.cpp
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
...
result = pollInner(timeoutMillis);
}
int Looper::pollInner(int timeoutMillis) {
...
// 进入阻塞等待,在 nativeWake() 向管道写端写入字符,则该方法会返回
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
...
}
当接收到有消息时,唤醒线程工作:
MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
// ...
// We can assume mPtr != 0 because mQuitting is false.
// 唤醒线程 nativePollOnce
if (needWake) {
nativeWake(mPtr);
}
}
void NativeMessageQueue::wake() {
mLooper->wake();
}
void Looper::wake() {
uint64_t inc = 1;
// 向 mWakeEventFd 写入,调用 write 唤醒
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
...
}
Handler 为何采用管道而非 Binder?
Handler 的线程唤醒是使用的管道,Handler 不采用 Binder,并非 Binder 完成不了这个功能,而是太浪费 CPU 和内存资源了。因为 Binder 采用 C/S 架构,一般用于不同进程间通信。
从内存角度,通信过程中 Binder 还涉及一次内存拷贝,Handler 消息机制中的 Message 根本不需要拷贝,本身就是在同一块内存,Handler 需要的仅仅是告诉另一个线程有数据了。
从 CPU 角度,为了 Binder 通信底层驱动还需要一个 Binder 线程池,每次通信涉及 Binder 线程的创建和内存分配等,比较浪费 CPU 资源。
从上面的分析可得,Binder 用于进程间通信,而 Handler 消息机制用于同进程的线程间通信,不宜采用 Binder。
同步屏障和异步消息
同步屏障的作用可以理解为拦截同步消息的执行,同步屏障其实也是一个具有标识的 Message 消息。插入一个同步屏障也就是插入一条 msg.target == null
的消息到 MessageQueue 链表中。
当调用 postSyncBarrier() 插入一个同步屏障消息到链表并唤醒线程后,MessageQueue 的 next() 如果判断到有同步屏障消息即 msg.target == null
,会暂停所有同步消息优先处理被标记 为 msg.isAsynchronous() 的异步消息,处理完异步消息后又重新休眠线程进入阻塞,直到调用 removeSynBarrier() 将同步屏障消息移除出链表,否则主线程会一直不去处理同步屏障消息之后的同步消息。
MessageQueue.java
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
private int postSyncBarrier(long when) {
// Enqueue a new sync barrier token.
// We don't need to wake the queue because the purpose of a barrier is to stall it.
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
// 下面的代码根据消息执行的时间when查找到这条同步屏障消息适合插入到链表的位置
Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
// 找到适合插入同步屏障消息的位置
// 插入前:prev->next
// 插入后:prev->msg->next
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}
public void removeSyncBarrier(int token) {
// Remove a sync barrier token from the queue.
// If the queue is no longer stalled by a barrier then wake it.
// 下面的代码根据target==null从链表查找到同步屏障,然后移除
synchronized (this) {
Message prev = null;
Message p = mMessages;
while (p != null && (p.target != null || p.arg1 != token)) {
prev = p;
p = p.next;
}
if (p == null) {
throw new IllegalStateException("The specified message queue synchronization "
+ " barrier token has not been posted or has already been removed.");
}
// 找到消息
// 移除前:prev->msg->msg.next
// 移除后:prev->msg.next
final boolean needWake;
if (prev != null) {
prev.next = p.next;
needWake = false;
} else {
mMessages = p.next;
needWake = mMessages == null || mMessages.target != null;
}
p.recycleUnchecked();
// If the loop is quitting then it is already awake.
// We can assume mPtr != 0 when mQuitting is false.
if (needWake && !mQuitting) {
nativeWake(mPtr);
}
}
}
和同步消息一样,异步消息也是在 MessageQueue的 next() 处理:
MessageQueue.java
Message next() {
// ...
for (;;) {
// ...
nativePollOnce(ptr, nextPollTimeoutMillis);
// ...
// 有同步屏障,优先处理异步消息
if (msg != null && msg.target == null) {
// 找到同步屏障后的异步消息,同步消息会直接过滤不处理
// Stalled by a barrier. Find the next asynchronous message in the queeue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
// 消息还没到时间处理,更新消息处理的剩余时间
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// 拿到消息,返回Message返回上层Looper.loop()处理消息
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
// ...
}
}
也许你会有疑问:为什么 Android 要设计同步屏障?能用优先队列来替代同步屏障吗?异步消息具体是干嘛用的?
在介绍同步屏障时我们提到它的作用是让异步消息优先执行,异步消息常见于 Android sdk 的一些系统处理,比如 ViewRootImpl 和 Choreographer 处理 View 的绘制流程就是使用的异步消息,让界面绘制的消息会比其他同步消息优先执行,避免因为 MessageQueue 消息太多导致绘制消息被阻塞导致画面卡顿,绘制完成后,就会移除同步屏障。
ViewRootImpl.java
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalSceduled = true;
// View绘制前在MessageQueue添加一个同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// Choreographer界面刷新,mTraversalRunnable在接收到Vsync信号后就会被执行启动View绘制流程
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferdInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
void unscheduleTraversals() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
mChoreographer.removeCallbacks(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
// 启动View绘制流程,measure、layout、draw
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
同步屏障是阻止同步消息执行并且优先执行异步消息,为什么不用优先队列 PriorityQueue 对 Message 按优先级排序来实现呢?
个人的想法是,在 Android 中,MessageQueue 的设计是单链表,整个链表是根据时间排序 Message,如果此时再加入一个优先级的排序规则,一方面会让排序规则变得复杂,另一方面,也会让消息不可控。因为优先级队列 PriorityQueue 它需要能提供给用户优先级的处理,如果用户在外面总是将消息填写为最高优先级,这就会导致系统的消息会被延后消费,整个系统运作就会出问题,影响用户体验。所以 Android 设计同步屏障的机制还是挺巧妙的。
常见的问题解答
用一句话概括 Handler,并简述其原理
Handler 是 Android 系统的根本,在 Android 应用被启动时,会分配一个单独的虚拟机,虚拟机会执行 ActivityThread 中的 main(),在 main() 中对主线程 Looper 初始化,也就是几乎所有代码都执行在 Handler 内部,Handler 也可以作为主线程和子线程通讯的桥梁。
Handler 通过 sendMessage 发送消息,将消息放入 MessageQueue 中,在 MessageQueue 中通过时间的维度来进行排序,Looper 调用 loop() 不断的从 MessageQueue 中获取消息,执行 Handler 的 dispatchMessage(),最后调用 handleMessage()。
一个线程中最多有多少个 Handler、Looper、MessageQueue?
一个线程只能有一个 Looper,可以有多个 Handler,在线程中我们需要调用 Looper.prepare(),它会创建一个 Looper 并且将 Looper 保存在 ThreadLocal 中,每个线程都有一个 LocalThreadMap,会将 Looper 保存在对应线程中的 LocalThreadMap,key 为 ThreadLocal,value 为 Looper。
Looper 死循环为什么不会导致 ANR?
这是一个误导性问题,实际上 Looper 死循环和 ANR 没有什么关系。
ANR 是当我们处理点击事件时 5s 内没有响应,我们在处理点击事件时也是用的 Handler,所以一定会有消息执行,并且 ANR 也会发送 Handler 消息,同样的不会阻塞主线程。
Looper.loop() 无限循环为什么不会阻塞主线程?
android 应用是在 Handler 机制上运行的,主线程也是一段可执行的程序,如果不阻塞主线程执行完程序就会退出,也就是应用也会退出,这肯定不是需要的,所以肯定要有阻塞在这个循环内处理消息。
多个 Handler 可以在 MessageQueue 中插入消息或移除消息,是否有线程问题?
不会有线程问题,如果有详细查看源码,可以发现在处理消息时都会加上同步锁来保证线程安全。
MesssageQueue.java
Message next() {
// ...
for (;;) {
// ...
nativePollOnce(ptr, nextPollTimeoutMillis();
// 添加同步锁,保证线程安全
synchronized(this) {
// ...
}
}
}
boolean enqueueMessage(Message msg, long when) {
// ...
// 添加同步锁,保证线程安全
synchronized(this) {
// ...
}
}
private int postSyncBarrier(long when) {
synchornized(this) {
// ...
}
}
public void removeSyncBarrier(int token) {
synchronized(this) {
// ...
}
}
Handler 的内存泄漏最终是谁持有的 Activity?(为什么 Handler 内部类或界面销毁时发送 msg 会导致内存泄漏?)
实际上这个问题是想考的 JVM 的回收算法,JVM 回收算法是可达性分析,GCRoot 可达的引用不会被回收,不可达的会被回收。
如果直接回答内部类是错误的,它只是一个导致内存泄漏的环节。Handler 会内存泄漏的原因在于,handler.send/post消息时,msg.target 持有的 Handler 对象,而 Handler 如果是创建在 Activity/Fragment 的内部类,内部类会持有的外部类的引用,msg 间接导致了 Activity/Fragment 内存泄漏。
引用链:Looper -> MessageQueue -> Message -> Handler -> Activity
Handler 内部类持有的外部类 Activity 引用(java 内部类在构造中会持有外部类的对象),msg.target 持有 Handler,msg 被 MessageQueue 持有,而 MessageQueue 是 Looper 的 mQueue,Looper 在 ActivityThread 创建时是静态变量,GC Root 就是 Looper。
最终分析到 Activity 是被 ActivityThread 中的 Looper 可达,ActivityThread 是应用进程 app 存活就一直存在,所以 Activity 内存泄漏。
Looper 什么时候进入循环?
主线程在 ActivityThread 的 main() 执行时,在最后会调用 Looper.loop(),主线程的 Looper 是在应用启动的时候。
子线程的 Looper 要自己调用 Looper.loop() 才会开始循环。
HandlerThread 的原理
我们都知道 HandlerThread 可以让我们的 Handler 运行在子线程(其实就是在子线程创建的 Looper,传给 Handler 带有 Looper 参数的构造函数),会有这样的写法:
HandlerThread handlerThread = new HandlerThread();
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
HandlerThread 其实是一个子线程,调用 start() 运行线程,那 handlerThread.getLooper() 是怎么保证 getLooper() 时能获取到 Looper?
这涉及到多线程并发问题,当 handlerThread.start() 和 getLooper() 添加了同步锁:
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
public Looper getLooper() {
if (!isAlive()) {
return null;
}
// If the thread has been started, wait until the looper has been created.
synchronized (this) {
while (isAlive() && mLooper == null) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
return mLooper;
}
Handler 如何处理发送延时消息?(MessageQueue 如何管理消息的?)
MessageQueue.enqueueMessage() 插入消息时会遍历 mMessages 消息池(单链表),通过判断 msg.when 对比,按时间顺序排序消息待执行的消息。
MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
...
synchronized (this) {
...
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWak && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p;
prev.next = msg;
}
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
在 MessageQueue.next() 获取消息时,消息会和当前时间判断,如果还没到消息的执行时间重新计算 nextPollTimeoutMillis = now - msg.when 调用 nativePollOnce(ptr, nextPollTimeoutMillis) 阻塞,由 Linux 的 epoll 机制在时间到时唤醒线程消费消息。
如果没有消息,nativePollOnce() 会一直阻塞直到有消息。
MessageQueue.java
Message next() {
...
int nextPollTimeoutMillis = 0;
for (;;) {
...
// 根据 nextPollTimeoutMillis 让 epoll 在指定时间唤醒
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
...
if (msg != null) {
if (now < msg.when) {
// 执行消息的时间还没到,重新计算 nextPollTimeoutMillis,到时间就唤醒消费 message
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// 有到时间的 message,取出来消费
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
....
}
}
}
我们使用 Message 时应该如何创建它?
使用 handler.obtainMessage(),不建议使用 new Message() 直接创建对象。
Message 使用了享元模式重复利用内存空间,减少对象的创建,内部维护了一个链表,并且最大长度是 50,当消息处理完之后会将消息内的属性设置为空,并且插入到链表的头部,使用 obtain() 创建的 Message 会从头部获取空的 Message。
不直接创建 Message,如果消息量很大非常频繁就会容易导致 GC,会出现内存抖动和卡顿问题。
引申问题:Message 为什么要使用链表这种数据结构做回收池?
因为获取消息我并不需要知道使用的哪个 Message,只要你是 Message 就可以使用,所以无论是 List、数组还是 map 都具有查找的功能,这种场景并不需要查找功能,所以最适合的数据结构是链表,而且是单向链表(使用这种数据结构需要有一个静态的引用指向链表头【sPool】还有一个静态的变量记录链表的节点数量,当然还需要有一个常量限定最多节点数量)。
子线程中维护 Looper 在消息队列无消息的时候处理方案是怎样的?
子线程创建了 Looper,当没有消息的时候子线程会被 block 无法被回收,所以我们需要手动调用 quit 方法将消息删除并且唤醒 Looper,然后 next 方法返回 null 退出 loop。
关于 ThreadLocal,谈谈你的理解?
ThreadLocal 类似于每个线程有一个单独的内存空间,不共享,ThreadLocal 在 set 的时候会将数据存入对应线程的 ThreadLocalMap 中,key=ThreadLocal,value=值。
为什么不能在子线程更新 UI?为什么要设计成只能在主线程更新 UI?
不能在子线程更新 UI 是因为在创建 ViewRootImpl 时会在构造初始化 mThread,因为启动时就是在主线程,所以 mThread 就是主线程,当尝试在子线程更新 UI 时会检查所在线程与 mThread 不一致抛出异常。
但非 UI 线程是可以刷新 UI 的,前提是它要拥有自己的 ViewRoot。如果想直接创建 ViewRoot 实例,你会发现找不到这个类。那怎么做呢?通过 WindowManager。
class NonUiThread extends Thread{
@Override
public void run() {
Looper.prepare();
TextView tx = new TextView(MainActivity.this);
tx.setText("non-UiThread update textview");
WindowManager windowManager = MainActivity.this.getWindowManager();
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
200, 200, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
WindowManager.LayoutParams.TYPE_TOAST,PixelFormat.OPAQUE);
// WindowManager.addView() 会创建 ViewRoot,对应的 mThread 就是子线程了
windowManager.addView(tx, params);
Looper.loop();
}
}
设计成只能在主线程更新 UI,主要还是因为 Android UI 操作并不是线程安全的,并且这些操作必须在 UI 线程执行。
作一个假设,现在我用 invalidate 在子线程中刷新界面,同时 UI 线程也在用 invalidate 刷新界面,这样会不会导致界面的刷新不能同步?既然刷新不同步,那么 invalidate 就不能在子线程中使用。这就是 invalidate 不能在子线程中使用的原因。
引申问题:为什么 UI 线程不设计成线程安全的?
总所周知,如果设计成线程安全的,那性能肯定是大打折扣,而 UI 更新的要求有如下特性:
-
UI 是具有可变性的,甚至是高频可变
-
UI 对响应时间很敏感,这就要求 UI 操作必须要高效
-
UI 组件必须批量绘制来保证效率
所以为了保证渲染性能,UI 线程不能设计成线程安全的。Android 设计了 Handler 机制来更新 UI 是避免多个子线程更新 UI 导致的 UI 错乱问题,也避免了通过加锁的机制设计成线程安全的,因为那样会导致性能下降。
点击页面上的按钮更新 TextView 的内容,谈谈你的理解?(同步屏障)
点击按钮的时候会发送消息到 Handler,但是为了保证优先执行,会加一个标记异步,同时会发送一个 target 为 null 的消息,这样在调用 MessageQueue 的 next() 时,如果发小消息的 target 为 null,那么遍历消息队列将有异步标记的消息获取出来优先执行,执行完之后会将 target 为 null 的消息移除。
描述下 epoll 机制
传统的 select 模型存在问题:
-
需要遍历每个文件描述符(app),没有排序
-
需要从内核空间拷贝
针对以上两个问题 android 使用了 epoll 机制:
-
epoll 是使用的红黑树的数据结构,当 app 调用 Looper.prepare() 时会注册句柄到红黑树,有事件处理时直接从红黑树查找不用每次都遍历所有句柄提高效率
-
epoll 只要少量的用户空间和内核空间的句柄拷贝
-
epoll 是通过内核与用户 mmap 同一块内存,避免了无谓的内存拷贝
-
IO 性能不会随着监听的 fd 数量增长而下降