Android中线程间通信机制Handler疑问解答
说起Handler的使用,几乎是开发者入门必备的开发技能。而且都会知道Handler配合一个Looper和MessageQueue来实现消息的创建、分发、处理。
每一个Handler会绑定到创建它的线程以及一个消息队列。
通过Handler,我们可以跟其他线程发消息实现线程切换,也可以给当前线程Handler发消息实现定时任务。
这里总结一下对Handler一些可能有的疑问。
消息循环时是否会阻塞主线程
会
当Looper开始循环后,会开启一个死循环,在这个循环中不停的通过next()方法从MessageQueue中读取消息,而MessageQueue的next()是一个可能会被阻塞的方法。
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;
for (;;) {
//当读取下一条消息,该循环可能会被next()方法阻塞住,直到读取到消息。
//如果消息为空,说明线程已退出,该循环也可以直接跳出了。
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
...省略部分代码
}
}
那么MessageQueue的next()是如何在哪阻塞的呢?
答案是 nativePollOnce(ptr, nextPollTimeoutMillis);
这是一个本地C++实现的方法,ptr是native层NativeMessageQueue的指针,nextPollTimeoutMillis是一次阻塞的超时时间。
如果nextPollTimeoutMillis为0,则无需阻塞直接返回。
如果nextPollTimeoutMillis为-1,表示需要无限阻塞,直到被唤醒。
如果nextPollTimeoutMillis为其他正整数,则会阻塞相应的时间,然后返回。
Message next() {
final long ptr = mPtr;
int nextPollTimeoutMillis = 0;
for (;;) {
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
//寻找下一条合法的消息
Message msg = mMessages;
//如果msg.target为null,代表是异步消息,要一直找到下一条同步消息
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
//如果找到一条待处理的消息,有两种情况需要考虑
//1. 此消息的执行时间还未到,需要阻塞一段时间
//2. 此消息的执行时间已到,则返回
if (msg != null) {
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// 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;
}
}
}
注:消息在入队列时,会按照消息的执行时间点排序,所以查询到的第一条消息,如果还没有到执行的时间点没有到,那么后面的消息都不应该处理。
为什么要阻塞主线程
防止主线程退出
我们都知道一个线程是CPU最小可执行单元,当线程中代码执行完成后,该线程就会退出销毁。而Android中的主线程负责UI绘制、事件响应等,如果主线程退出了,应用就无法继续运行下去了。那么保持一个线程一直执行的方法就是无限循环。
所以当主线程没有消息需要处理时,也必须阻塞住,不能退出。
主线程阻塞机制
Linux epoll
本地方法nativePollOnce(ptr, nextPollTimeoutMillis)的核心实现就是epoll命令。
下面看epoll在Android中应用:
//system/core/libutils/Looper.cpp
//构造唤醒事件的fd
mWakeEventFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
//创建新的epoll实例,并注册wake管道
mEpollFd = epoll_create(EPOLL_SIZE_HINT);
//将唤醒事件(mWakeEventFd)添加到epoll实例(mEpollFd)
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);
//等待返回
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
// 向管道mWakeEventFd写入字符,唤醒等待事件mWakeEventFd的epoll实例
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
其中epoll_wait()是个阻塞方法,这个方法有三种情况才会返回。
1. eventCount > 0,等待的事件发生,被唤醒
2. eventCount == 0,等待超时,超时时间由timeoutMillis指定
3. eventCount < 0,等待过程中出现异常
epoll是一种高效的I/O多路复用机制。相对于select,epoll不受最大监视文件描述符数量的限制,相对于poll,epoll不是采用轮训方式去获取就绪的文件描述符。
epoll通过事先epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
主线程阻塞为什么不会ANR
主线程阻塞时,并没有任务(页面绘制、触摸输入、键盘输入)执行。因为这些任务处理时也会给主线程发送消息并唤醒主线程,所以不会ANR。
一个纯净的应用进程至少包含几个线程
线程名称 | 备注 |
---|---|
com.qfpay.xxx | 主线程 |
Heap thread poo | 异步的堆线程 |
Signal Catcher | 捕捉Kernel信息,比如SIGNAL_QUIT |
JDWP | 调试线程 |
ReferenceQueueD | 引用队列守护线程 |
FinalizerDaemon | 析构守护线程 |
FinalizerWatchd | 析构监控的守护线程 |
HeapTrimmerDaem | 堆正路的守护线程 |
GCDaemon | 垃圾回收守护线程 |
Binder_1 | Binder通信线程 |
RenderThread | UI绘制线程 |
GL updater | |
hwuiTask1 |
ThreadLocal的作用
存储Looper实例,一个线程只能和一个Looper关联,一个ThreadLocal只能存储一个变量,使用ThreadLocal可以防止多次创建。
ThreadLocal的特性就是在同一个线程,多个类或方法间使用同一个变量副本。而在不同的线程间使用不同的变量副本。
//android.os.Looper
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
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));
}
ThreadLocal实现机制
简单来说,就是在线程实现类Thread中声明了一个集合类型的全局变量。每当对ThreadLocal进行存取时,均通过Thread.currentThread()拿到当前线程实例,从而实现线程作用域内的变量。
//java.lang.Thread
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
private void exit() {
threadLocals = null;
}
}
//java.lang.ThreadLocal
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
ThreadLocal变量集合类型
上面说到的集合类型在ThreadLocal.ThreadLocalMap这个包级私有的静态类中,核心实现是一个数组。
//java.lang.ThreadLocal
static class ThreadLocalMap {
private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
- table初始大小16,扩容的临界值为原容量的2/3,每次扩容后的容量为原容量的2倍。
- 每一项Entry的键为ThreadLocal对象,并声明为弱引用。值为要存储的变量,强引用。
- 为了避免长期持有value对象而引起内存泄漏,当进行set操作时,会把key对应value置为null,并把entry从数组中清理掉。
有几种处理消息的方式
当从消息队列取出消息后,会交给消息的目标Handler处理这个消息,Handler中处理消息的方法为dispatchMessage(Message)
//android.os.Handler
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
- 优先执行Message本身的runnable
- 然后由Handler实例的成员变量Callback处理,这个变量在Handler的构造函数中被初始化。例如AsyncLayoutInflater使用了这种方式
- 最后由handlerMessage(Message)方法处理,此方法可以由子类复写,是最常用的方式
可不可以在主线程休眠时做额外工作
线程空闲是指暂时没有消息需要处理,即将被epoll.waite阻塞,有两种情况:
1. 消息队列空闲
2. 消息队列头部消息没有到达执行时间
IdleHandler,当线程没有需要处理的消息,将要被阻塞的时候,会遍历已有的IdleHandler,转而处理IdleHandler类型的消息。
//android.os.MessageQueue
public static interface IdleHandler {
//线程空闲时回调此方法。返回true此回调一直存在,返回false会自动删除。
boolean queueIdle();
}
public void addIdleHandler(@NonNull IdleHandler handler) {
if (handler == null) {
throw new NullPointerException("Can't add a null IdleHandler");
}
synchronized (this) {
mIdleHandlers.add(handler);
}
}
添加IdleHandler方式如下:
Looper.myLooper().getQueue().addIdleHandler(new IdleHandler() {...});
消息队列调用时机如下:
//android.os.MessageQueue
public final class MessageQueue {
Message next() {
int pendingIdleHandlerCount = -1;
for (;;) {
//进入阻塞状体时,要获取IdleHandler的数量
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
//开始执行IdleHandler
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
//如果queueIdle()返回false,则将对应的IdleHandler从集合中移除
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
//IdleHandler重置为0,下次重新获取
pendingIdleHandlerCount = 0;
//如执行了IdleHandler,则有可能在执行期间有需要处理的消息,
//所以需要把阻塞超时时间设置为0,表明需要立即读取消息
nextPollTimeoutMillis = 0;
}
}
消息缓存池实现及大小
Android中Message类被设计成链表结构,MessageQueue中持有即将被处理的消息mMessages,当此条消息被处理后,指向下一条需要处理消息。
为了防止消息创建带来的开销,Message中持有一个消息队列mPool,mPool是静态的,由所有线程共用。
//android.os.Message
public final class Message implements Parcelable {
Message next;
private static Message sPool;
//缓存池最大缓存50个消息
private static final int 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; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
//消息回收
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++;
}
}
}
}
当调用obtain()方法时,会从消息缓存池中去除一个消息出来。当回收一个消息时,会放到消息缓存池的头部。
同步/异步消息
Message类有一个flag字段,其中可以取值FLAG_ASYNCHRONOUS,表明这个消息时异步消息,否则是同步消息。
同步消息:在消息队列中等待依次被执行
异步消息:在消息队列中,优先被执行,无论是不是在消息队列的头部
通过Message的setAsynchronous(boolean)方法可以将一个消息设置为异步消息。
那么在消息循环时是怎么优先处理异步消息的呢?
这里要解释一个异步栅栏(SyncBarrier)的东西,异步栅栏是一个特殊的Message,它的target Handler为空,也就是这个消息没有人处理。
当消息队列中存在这样一个异步栅栏的时候,消息循环会只从消息队列取出异步消息,而不是顺序执行。看起来就像是把同步消息拦在了外面。直到这个异步栅栏被移除后,消息队列则又恢复成依次处理消息的模式。
下面是向消息队列中添加和移除一个异步栅栏的步骤:
//android.os.MessageQueue
public final class MessageQueue {
//添加异步栅栏
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;
Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.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.
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.");
}
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);
}
}
}
}
下面是消息循环时处理异步消息的过程:
//android.os.MessageQueue
public final class MessageQueue {
Message next() {
for (;;) {
synchronized (this) {
Message msg = mMessages;
if (msg != null && msg.target == null) {
//如果msg.target为空,表明是一个异步栅栏类型的消息
//那么就会一直在消息队列中寻找异步消息,然后去处理
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
mBlocked = false;
if (prevMsg != null) {
//如果prevMsg不为空,说明当前消息时异步消息,需要从消息队列中抽取出来执行
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
}
}
}
}
}
需要注意的是添加/移除异步栅栏的api是隐藏的,也就是只有系统才有权调用,开发者用不了,除了反射。
系统在渲染页面的时候会用到这个特性。
//android.view.ViewRootImpl
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//添加异步栅栏
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
void unscheduleTraversals() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除异步栅栏
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
mChoreographer.removeCallbacks(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}