Handler机制作为Android Framework层的基础,很多问题需要研究一下源码才可以弄清楚,如果只是停留在对于一些面试答案的背诵上是没有更好的代码理解的。所以我想结合面试问题来研究Handler源码。
文章内容主要分成以下几个方面:
Handler中所有发送消息的方法都都最终会调用到Handler#enqueueMessage:
//Handler#enqueueMessage private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) { // 指定需要发送的Message的接收对象Target为当前的Handler msg.target = this; msg.workSourceUid = ThreadLocalWorkSource.getUid(); if (mAsynchronous) { msg.setAsynchronous(true); } //会调用 MessageQueue#enqueMessage return queue.enqueueMessage(msg, uptimeMillis); }
我们一般在写Handler时,都会复写Handler中的空handleMessage方法。在Handler#enqueueMessage通过写message的target对象是当前的Handler就可以保证发送的Message被我们复写的方法处理。
// MessageQueue#enqueMessage boolean enqueueMessage(Message msg, long when) { // .... // 线程同步操作 synchronized (this) { //.... // mMessages是一个没有头节点的单链表,enqueMessage时,通过Message中时间顺序(when) // 将Message加入mMessage中 Message p = mMessages; boolean needWake; // ------- 插入到开头 -------- if (p == null || when == 0 || when < p.when) { // Message加入 // 在以下情况下,使用头插法,建立新的头结点 // 1\. 队列(单链表)为空 // 2\. when=0,时间最前面 // 3\. 当前队列的头结点的时间小于待插入的Message 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 (needWake && p.isAsynchronous()) { needWake = false; } } // 插入节点 msg.next = p; // invariant: p == prev.next prev.next = msg; } // 注意这个点:nativeWake 和 nativePollOnce相互配合实现被阻塞的线程苏醒 if (needWake) { nativeWake(mPtr); } } return true; }
从MessageQueue的关键方法enqueMessage中,我们可以得到以下关键信息 mMessage在数据结构上是一个单链表的形式,但是MessageQueue会根据Message的时间进行排序。使用的是头插法,when越小,时间越提前
Message消息分成三种:同步、异步、屏障消息。
我们通常使用的消息都是同步消息:
// Handler#constructor public Handler(@NonNull Looper looper) { //指定async字段为false this(looper, null, false); }
我们常规构建的Handler都会指定async字段为false,这样就会间接修改全局变量mAsynchronous
,一条同步消息被添加到队列中。
// Handler#enqueueMessage private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) { msg.target = this; msg.workSourceUid = ThreadLocalWorkSource.getUid(); // 判断 if (mAsynchronous) { msg.setAsynchronous(true); } return queue.enqueueMessage(msg, uptimeMillis); }
发送异步消息的话就需要指定async为true。
同步和异步消息都会指定message.target处理对象,但是屏障消息会设置target为空。所以屏障消息可以理解为是消息队列中一个特殊的节点。
屏障消息会妨碍 MessageQueue#next取出同步消息。但是不会阻止取出异步消息。异步消息就是在存在消息屏障的时候有更搞的优先级。
既然MessageQueue在形式上可以看做是一个根据时间排序的优先级队列,同步消息根据时间排队势必会影响消息的处理,下面引用Handler之消息屏障你应该知道的中的一句话来说明同步消息的阻塞对于Android系统的影响:
在Android系统中存在一个VSync消息,它主要负责每16ms更新一次屏幕展示,如果用户同步消息在16ms内没有执行完成,那么VSync消息的更新操作就无法执行在用户看来就出现了掉帧或卡顿的情况,为此Android开发要求每个消息的执行需要限制在16ms之内完成。但是消息队列中可能会包含多个同步消息,假如当前主线程消息队列有10个同步消息,每个同步消息要执行10ms,总共也就需要执行100ms,这段时间内就会有近7帧无法正常刷新展示,应用执行过程中遇到这种情况还是很普遍的。
Message next() { // ... // ------------- 取消息循环 -------------- int nextPollTimeoutMillis = 0; for (;;) { //.... // ------------- 休眠 ---------------- nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { // .... // ------------- 取消息的逻辑 ---------------- } }
MessageQueue#next的逻辑还是非常清楚的,主要分成这样几个部分。
### MessageQueue#next Message next() { // ..... int nextPollTimeoutMillis = 0; for (;;) { //.... nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { Message prevMsg = null; Message msg = mMessages; // msg.target == null 是Message为屏障消息的特征 // 当前的msg 是MessageQueue中的第一个对象 // 如果第一个对象是屏障消息,找异步消息 if (msg != null && msg.target == null) { 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 { // 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; } // 和Idlehandler有关 } }
Message#next在取出消息时会检查是否有屏障消息的存在,如图所示,如果存在屏障消息就不会优先处理同步消息,会先出处理异步消息。
当取出了消息,但是消息没有到时的话,会通过nativePollOnce方法暂时阻塞线程。这个方法和MessageQueue#enqueMessage方法中的nativeWake相互配合。这两个方法都是native方法,ativePollOnce使用Linux中的epoll机制监听文件描述符,而nativeWake就写入这个文件描述符。
Message在Looper#loop中被取出来之后,会被复用:
msg.recycleUnchecked();
// MessageQueue#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 = UID_NONE; workSourceUid = UID_NONE; when = 0; target = null; callback = null; data = null; synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) { next = sPool; sPool = this; sPoolSize++; } } }
在复用方法中,Message的字段被重新初始化并且被放入了复用池sPool中:
private static Message sPool;
Message本身也可以被理解为一个链表的结构,sPool就是Message链表的头结点。我们在使用Message时,应该尽量使用obtain方法复用在Message池中的对象,这样一来可以尽量避免创建、销毁对象带来的内存抖动。
// Message#obtain 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(); }
A1: 是的。 关于这个问题的回答,需要查看一下Looper#prepare:
private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { // 在ThreadLocal中找,如果重复创建了Looper会抛出异常 throw new RuntimeException("Only one Looper may be created per thread"); } // 创建一个新的Looper并且设置到ThreadLocal中 sThreadLocal.set(new Looper(quitAllowed)); }
ThreadLocal可以认为是一个这样的Map结构: Map<Thread, Looper>
不同的线程对应的Looper对象(如果创建了)是不同的实例。通过Looper的构造方法,每个Looper创建时都会实例化新的的MessageQueue对象。
private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); }
因此Looper、MessageQueue和Thread三者都是一一对应的关系。一个线程只有一个Looper。 更重要的是,Looper#prepare中会检查,如果重复创建了Looper,会抛出异常。这样一来可以保证一个Thread对应一个Looper。
//MessageQueue#enqueueMessage boolean enqueueMessage(Message msg, long when) { // ..... synchronized (this) { // ..... } return true; } // MessageQueue#next Message next() { // .... for (;;) { //... synchronized (this) { // ... } } }
这两个方法都使用了synchronized,锁的对象是MessageQueue自身。因为这些Handler都共用了同一个Looper,也就是共用了同一个MessageQueue,这样添加Message时可以保证写入和读取按照设置的时间先后顺序。
Handler中发送消息在某个delay之后到达的方法都是使用了当前的时间+delay时间:
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) { if (delayMillis < 0) { delayMillis = 0; } return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis); }
如果没有指定具体的时间,就会设置delay为0:
public final boolean post(@NonNull Runnable r) { return sendMessageDelayed(getPostMessage(r), 0); }
MessageQueue会根据时间先后排序这些消息。但是因为MessageQueue可能会被不同线程的,持有了同一Looper对象的Handler访问,因为锁机制,后到的访问者需要排队,所以这种即时性是不能保证的。会在具体设置的时间稍后。
A1: Handler造成的内存泄漏应该从两个方面来思考
主要是说一下第二种问题:
// Handler#enqueueMessage private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) { msg.target = this; msg.workSourceUid = ThreadLocalWorkSource.getUid(); if (mAsynchronous) { msg.setAsynchronous(true); } return queue.enqueueMessage(msg, uptimeMillis); }
从上述代码中可以看出,Message会指定Target为当前的Handler。所以Message持有Handler的引用,并且被放入了MessageQueue中等待唤醒。结合Q1和Q2两个问题,Handler处理机制中重要的几个类,以及它们之间对应的关系如图所示:
Handler机制设计的重要方法也归纳在下图中:
MessageQueue中可以有多个Message,Message在被放入MessageQueue(Handler#enqueMessage)时会指定handler对象。 一个Thread在ThreadLocal的管理下,只会有一个Looper,Looper通过Looper#prepare创建了一个MessageQueue。Looper通过loop开始循环,取出消息(MessageQueue#next)。
具体的代码可以参考:
private CurHandler handler = new CurHandler(this); private static class CurHandler extends Handler { WeakReference<Activity> outer; public CurHandler(@NonNull Activity outer){ super(outer.getMainLooper()); this.outer = new WeakReference<Activity>(outer); } @Override public void handleMessage(@NonNull Message msg) { super.handleMessage(msg); } } @Override protected void onDestroy() { super.onDestroy(); handler.removeCallbacksAndMessages(null); }
这个是一个老生常谈的问题,我们在Activity中使用Handler时,如果使用匿名内部类继承Handler,使用不写任何构造函数的方法(这个构造函数Android Studio会显示不推荐,最好是使用指定Looper的方式,避免错误),会获取当前线程的Looper。如果是在Activity中,Activity的Looper已经在ActivityThread#main中进行了prepare和loop。所以在子线程中也要prepare和loop。
// 开启一个子线程,使用Kotlin自带的方法,会自动创建并start thread thread { // Case#1 // 运行在mainLooper上 val handlerMain = object: Handler(mainLooper){ override fun handleMessage(msg: Message) { when(msg.what){ TEST_HANDLER -> Log.d("TEST Main", "收到通知") else -> {} } } } // Case#2 运行在线程指定的Looper上 Looper.prepare() val handlerOther = object: Handler(Looper.myLooper()!!){ override fun handleMessage(msg: Message) { when(msg.what){ TEST_HANDLER -> Log.d("TEST other", "收到通知") else -> {} } } } handlerOther.sendEmptyMessage(TEST_HANDLER) Looper.loop() Looper.myLooper()!!.quitSafely() } }
这个地方还要记得退出Looper,因为Looper#loop线程主要负责消息循环,其中使用了for(;;)维持运转,在没有消息的时候进入阻塞状态。在这里使用quit退出for循环,释放子线程的相关资源。
A4: 首先看看官网对于ANR的定义
虽然Looper中的loop是死循环,回忆之前说过的Message#next,在线程没有接收到消息时进入阻塞状态,但是如果用户继续有时间输入,消息队列还是会进行处理:
for (;;) { Message msg = queue.next(); // might block if (msg == null) { // No message indicates that the message queue is quitting. return; } //..... }
总结来说,Looper#loop中的for循环不会造成主线程无法处理消息,反倒是Handler的消息机制需要for循环来不断尝试从消息队列中取出消息。