Android面试里常见的Handler相关问题

524 阅读6分钟
一、Handler、MessageQueue、Looper 的关系
  1. 模型职责

    • Handler: 负责向MQ里入队消息(sendMessage)、删除消息(removeMessage)、处理消息(handleMessage)
    • MessageQueue: 负责投递消息(enqueueMessage),取消息(next)
    • Looper: 负责轮询MQ,将取出的消息分发给对应的 handler 处理(loop)
    • Message: 单链表结构(next),绑定目标Handler(target),记录发送时间(when),发送内容(obj)
  2. 调用关系

    • Handler: 需要指定 Looper,Looper 中会有对应的 MQ
    • MessageQueue: 对应一个待处理的 Message 链表
    • Looper: 对应一个 MQ
    • Message: 对应处理自己的 Handler

    Handler 消息队列机制的使用过程如下

        class HandlerThread extends Thread {
            private Handler mHandler;    
            
            public void run() {
                Looper.prepare();
                mHandler = new Handler(Looper.myLooper());
                Looper.loop();
            }
            
            public Handler getThreadHandler() {
                return mHandler;
            }
        }
        
        class ActivityThread {
            
            public static void main() {
                //...
                
                HandlerThread ht = new HandlerThread();
                ht.start();
                
                ht.getThreadHandler().post(
                    ()-> {
                        //do xxx
                    }
                );
            }
        }
    
  3. 关系确立时机

    • Handler Looper

      class Handler {
          public Handler(Looper looper, Callback callback, boolean async) {
              mLooper = looper;
              mQueue = looper.mQueue;
              mCallback = callback;
              mAsynchronous = async;
          }
      }
      

      初始化 Handler 的时候,会指定 Looper,同时会将 Looper 中的 MessageQueue 也绑定到Handler上

    • Handler Message

      class Handler {
          private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
              msg.target = this;
              if (mAsynchronous) {
                  msg.setAsynchronous(true);
              }
              return queue.enqueueMessage(msg, uptimeMillis);
          }
      }
      

      在发送消息的时候,Handler 会和 Message 发生绑定,msg.target=this,可以理解为谁发的消息就要谁去处理

    • Looper Thread

      class 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));
          }
      }
      

      sThreadLocal.set(new Looper(quitAllowed)),借助 ThreadLocal 将 Looper 和 Thread 进行绑定,所以一个 Thread 只会对应一个 Looper,是 ThreadLocal 决定的,想知道ThreadLocal原理的同学可以自己查查相关资料。

二、有关消息队列的一些问题
  1. 如何保障消息队列中的消息的时间顺序?
  2. 延时消息如何实现的?
  3. 消息的分发过程?
  4. 消息屏障是什么?

解决这些问题的关键在于MQ入队和出队消息的逻辑

首先,解决前两个问题,如何保障消息队列中的消息的时间顺序?延时消息如何实现的?

这需要分析enqueueMessage()

boolean enqueueMessage(Message msg, long when) 

这个方法是在Handler#sendMessageAtTime()中调用的,whenSystemClock.uptimeMillis() + delayMillis的结果,即系统正常运行时间(不算阻塞、休眠)+消息延时时长

去除特殊情况,和加锁逻辑后的代码如下

    boolean enqueueMessage(Message msg, long when) {
            msg.when = when;
            Message p = mMessages;//之前的消息链
            if (p == null || when == 0 || when < p.when) {// New head, wake up the event queue if blocked.
                msg.next = p; //新 msg 作为链表头
                mMessages = msg; //更新消息链
                needWake = mBlocked;
            } else {
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) { //走到链表尾部或找到正确时间线位置
                        break;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg; //将msg插入到消息链
            }  
    }

具体插入逻辑简化为,向队列头部插入消息,比如调用了handler#sendMessageAtFrontOfQueue,按时间线插入消息,如下图所示,假设系统当前运行时间定格在+500的状态,开始向队列插入消息的状况

enqueue.jpg

特殊说明: 在+4100插入了一个延时500的消息,在入队的时候需要调整链表位置,所以+4100的消息会插入到+4200的后面,保证了消息的时间顺序,同时巧妙的实现了延时消息。

然后,分析一下取出正常消息(同步消息)的过程,精简代码如下

Message next() {
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            nativePollOnce(ptr, nextPollTimeoutMillis); //阻塞 nextPollTimeoutMillis 时间

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                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); //此时会给 nextPollTimeoutMillis 赋值,在循环开始时会阻塞
                    } 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;
                }
                
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }
        }
    }

正常从消息队列取消息的过程可以理解为如下过程

dequeue.jpg

假设系统时间为+4300,此时取出next为链表中的最后一个消息,when=+4500,此时因为还未到执行消息的时候,就会给nextPollTimeoutMillis赋值,然后会检查pendingIdleHandlerCount,此时一定没有IdleHandler要处理任务,进入下一次循环,进入阻塞状态

第三个问题,消息的分发过程? 在上面刚刚分析了取消息的过程 MessageQueue#next(),现在看看是谁调用了 next 并且得到了当前的 Message 对象,这个问题答案在Looper#loop()中,简化逻辑后的代码如下

    for (;;) {
            Message msg = queue.next(); // might block
            
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            
            msg.target.dispatchMessage(msg);
        }
    }
    

逻辑很简单,就是死循环调用next(),如果得到msg,就调用 msg.target.dispatchMessage 即回到 Handler#dispatchMessage

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

逻辑也很简单,就是指定了一个回调的优先级,先看 msg 是否指定了 Callback,如果没有就查看是否给 Handler 设置了 Callback,如果也没有就执行 Handler#handleMessage,这个方法需要自己重写

第四,消息屏障是什么?

简单来说,消息屏障就是个标志,标志开启时会优先处理异步消息 这个问题也需要在消息的入队出队时寻找答案,由于应用开发的时候使用场景比较少,所以放到最后解答,现在看一下有关消息屏障的源码

   Message next() {
       Message prevMsg = null;
       Message msg = mMessages;
       if (msg != null && msg.target == null) { 
          do {// Stalled by a barrier.  Find the next asynchronous message in the queue.
              prevMsg = msg;
              msg = msg.next;
          } while (msg != null && !msg.isAsynchronous());
       }
       //后面走上面分析过的流程,但此时的msg已经是一个异步消息了
   }

判断有无消息屏障的条件是 msg.target == null,在有消息屏障下,会不断循环链表找到异步消息,条件为!msg.isAsynchronous(),现在再来看看如何设置消息屏障MessageQueue#postSyncBarrier

    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;
            //此处没设置target,所以target是null

            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;
        }
    }

这个方法就是在指定时间处插入一个屏障消息,如果是0,就是在消息队列最头部插入,然后消息队列在取消息的时候发现有消息屏障就会向后遍历直到找到异步消息(msg.isAsynchronous()),将消息分发处理。

个人理解: 消息屏障的作用其实并不是名字定义的同步异步的意思,而是给消息定了优先级,给异步消息开了后门(只要有屏障消息在,之后读取的消息都是异步消息)。

三、小结

  • 消息队列中的消息按时间顺序排序和延时消息的实现,都是依靠系统时间的推移实现的
  • 如果下一个消息的时间超过系统当前时间,则会阻塞
  • 屏障消息的判断条件是 msg.target == null,正常使用Handler发的消息是无法将 target 置为 null 的,需要手动调用postSyncBarrier

补充:

IdleHandler的作用是什么?

IdleHandler 可以用来提升提升性能,主要用在我们希望能够在当前线程消息队列空闲时做些事情(譬如UI线程在显示完成后,如果线程空闲我们就可以提前准备其他内容)的情况下,不过最好不要做耗时操作。

参考文章 mp.weixin.qq.com/s/kpl8X9ZjO…