面试官:“我们聊聊 Handler 吧?”

Posted 涂程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试官:“我们聊聊 Handler 吧?”相关的知识,希望对你有一定的参考价值。

作者:承香墨影

一. 序

Handler 机制算是 android 基本功,面试常客。但现在面试,多数已经不会直接让你讲讲 Handler 的机制,Looper 是如何循环的,MessageQueue 是如何管理 Message 等,而是基于场景去提问,看看你对 Handler 机制的掌握是否扎实。

本文就来聊聊 Handler 中的 IdleHandler,这个我们比较少用的功能。它能干什么?怎么使用?有什么合适的使用场景?哪些不是合适的使用场景?在 Android Framework 中有哪些地方用到了它?

二. IdleHandler

2.1 简单说说 Handler 机制

在说 IdleHandler 之前,先简单了解一下 Handler 机制。

Handler 是标准的事件驱动模型,存在一个消息队列 MessageQueue,它是一个基于消息触发时间的优先级队列,还有一个基于此消息队列的事件循环 Looper,Looper 通过循环,不断的从 MessageQueue 中取出待处理的 Message,再交由对应的事件处理器 Handler/callback 来处理。

其中 MessageQueue 被 Looper 管理,Looper 在构造时同步会创建 MessageQueue,并利用 ThreadLocal 这种 TLS,将其与当前线程绑定。而 App 的主线程在启动时,已经构造并准备好主线程的 Looper 对象,开发者只需要直接使用即可。

Handler 类中封装了大部分「Handler 机制」对外的操作接口,可以通过它的 send/post 相关的方法,向消息队列 MessageQueue 中插入一条 Message。在 Looper 循环中,又会不断的从 MessageQueue 取出下一条待处理的 Message 进行处理。

IdleHandler 使用相关的逻辑,就在 MessageQueue 取消息的 next() 方法中。

2.2 IdleHandler 是什么?怎么用?

IdleHandler 说白了,就是 Handler 机制提供的一种,可以在 Looper 事件循环的过程中,当出现空闲的时候,允许我们执行任务的一种机制。

IdleHandler 被定义在 MessageQueue 中,它是一个接口。

// MessageQueue.java
public static interface IdleHandler {
  boolean queueIdle();
}

可以看到,定义时需要实现其 queueIdle() 方法。同时返回值为 true 表示是一个持久的 IdleHandler 会重复使用,返回 false 表示是一个一次性的 IdleHandler。

既然 IdleHandler 被定义在 MessageQueue 中,使用它也需要借助 MessageQueue。在 MessageQueue 中定义了对应的 add 和 remove 方法。

// MessageQueue.java
public void addIdleHandler(@NonNull IdleHandler handler) {
	// ...
  synchronized (this) {
    mIdleHandlers.add(handler);
  }
}
public void removeIdleHandler(@NonNull IdleHandler handler) {
  synchronized (this) {
    mIdleHandlers.remove(handler);
  }
}

可以看到 add 或 remove 其实操作的都是 mIdleHandlers,它的类型是一个 ArrayList。

既然 IdleHandler 主要是在 MessageQueue 出现空闲的时候被执行,那么何时出现空闲?

MessageQueue 是一个基于消息触发时间的优先级队列,所以队列出现空闲存在两种场景。

  1. MessageQueue 为空,没有消息;
  2. MessageQueue 中最近需要处理的消息,是一个延迟消息(when>currentTime),需要滞后执行;

这两个场景,都会尝试执行 IdleHandler。

处理 IdleHandler 的场景,就在 Message.next() 这个获取消息队列下一个待执行消息的方法中,我们跟一下具体的逻辑。

Message next() {
	// ...
  int pendingIdleHandlerCount = -1;
  int nextPollTimeoutMillis = 0;
  for (;;) {
    nativePollOnce(ptr, nextPollTimeoutMillis);

    synchronized (this) {
      // ...
      if (msg != null) {
        if (now < msg.when) {
          // 计算休眠的时间
          nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
        } else {
          // Other code
          // 找到消息处理后返回
          return msg;
        }
      } else {
        // 没有更多的消息
        nextPollTimeoutMillis = -1;
      }
      
      if (pendingIdleHandlerCount < 0
          && (mMessages == null || now < mMessages.when)) {
        pendingIdleHandlerCount = mIdleHandlers.size();
      }
      if (pendingIdleHandlerCount <= 0) {
        mBlocked = true;
        continue;
      }

      if (mPendingIdleHandlers == null) {
        mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
      }
      mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
    }

    for (int i = 0; i < pendingIdleHandlerCount; i++) {
      final IdleHandler idler = mPendingIdleHandlers[i];
      mPendingIdleHandlers[i] = null;

      boolean keep = false;
      try {
        keep = idler.queueIdle();
      } catch (Throwable t) {
        Log.wtf(TAG, "IdleHandler threw exception", t);
      }

      if (!keep) {
        synchronized (this) {
          mIdleHandlers.remove(idler);
        }
      }
    }

    pendingIdleHandlerCount = 0;
    nextPollTimeoutMillis = 0;
  }
}

我们先解释一下 next() 中关于 IdleHandler 执行的主逻辑:

  1. 准备执行 IdleHandler 时,说明当前待执行的消息为 null,或者这条消息的执行时间未到;
  2. pendingIdleHandlerCount < 0 时,根据 mIdleHandlers.size() 赋值给 pendingIdleHandlerCount,它是后期循环的基础;
  3. mIdleHandlers 中的 IdleHandler 拷贝到 mPendingIdleHandlers 数组中,这个数组是临时的,之后进入 for 循环;
  4. 循环中从数组中取出 IdleHandler,并调用其 queueIdle() 记录返回值存到 keep 中;
  5. keep 为 false 时,从 mIdleHandler 中移除当前循环的 IdleHandler,反之则保留;

可以看到 IdleHandler 机制中,最核心的就是在 next() 中,当队列空闲的时候,循环 mIdleHandler 中记录的 IdleHandler 对象,如果其 queueIdle() 返回值为 false 时,将其从 mIdleHander 中移除。

需要注意的是,对 mIdleHandler 这个 List 的所有操作,都通过 synchronized 来保证线程安全,这一点无需担心。

2.3 IdleHander 是如何保证不进入死循环的?

当队列空闲时,会循环执行一遍 mIdleHandlers 数组并执行 IdleHandler.queueIdle() 方法。而如果数组中有一些 IdleHander 的 queueIdle() 返回了 true,则会保留在 mIdleHanders 数组中,下次依然会再执行一遍。

注意现在代码逻辑还在 MessageQueue.next() 的循环中,在这个场景下 IdleHandler 机制是如何保证不会进入死循环的?

有些文章会说 IdleHandler 不会死循环,是因为下次循环调用了 nativePollOnce() 借助 epoll 机制进入休眠状态,下次有新消息入队的时候会重新唤醒,但这是不对的。

注意看前面 next() 中的代码,在方法的末尾会重置 pendingIdleHandlerCount 和 nextPollTimeoutMillis。

Message next() {
	// ...
  int pendingIdleHandlerCount = -1;
  int nextPollTimeoutMillis = 0;
  for (;;) {
    nativePollOnce(ptr, nextPollTimeoutMillis);
    // ...
    // 循环执行 mIdleHandlers
    // ...
    pendingIdleHandlerCount = 0;
    nextPollTimeoutMillis = 0;
  }
}

nextPollTimeoutMillis 决定了下次进入 nativePollOnce() 超时的时间,它传递 0 的时候等于不会进入休眠,所以说 natievPollOnce() 进入休眠所以不会死循环是不对的。

这很好理解,毕竟 IdleHandler.queueIdle() 运行在主线程,它执行的时间是不可控的,那么 MessageQueue 中的消息情况可能会变化,所以需要再处理一遍。

实际不会死循环的关键是在于 pendingIdleHandlerCount,我们看看下面的代码。

Message next() {
	// ...
  // Step 1
  int pendingIdleHandlerCount = -1;
  int nextPollTimeoutMillis = 0;
  for (;;) {
    nativePollOnce(ptr, nextPollTimeoutMillis);

    synchronized (this) {
      // ...
      // Step 2
      if (pendingIdleHandlerCount < 0
          && (mMessages == null || now < mMessages.when)) {
        pendingIdleHandlerCount = mIdleHandlers.size();
      }
     	// Step 3
      if (pendingIdleHandlerCount <= 0) {
        mBlocked = true;
        continue;
      }
      // ...
    }
		// Step 4
    pendingIdleHandlerCount = 0;
    nextPollTimeoutMillis = 0;
  }
}

我们梳理一下:

  • Step 1,循环开始前,pendingIdleHandlerCount 的初始值为 -1;
  • Step 2,在 pendingIdleHandlerCount<0 时,才会通过 mIdleHandlers.size() 赋值。也就是说只有第一次循环才会改变 pendingIdleHandlerCount 的值;
  • Step 3,如果 pendingIdleHandlerCount<=0 时,则循环 continus;
  • Step 4,重置 pendingIdleHandlerCount 为 0;

在第二次循环时,pendingIdleHandlerCount 等于 0,在 Step 2 不会改变它的值,那么在 Step 3 中会直接 continus 继续下一次循环,此时没有机会修改 nextPollTimeoutMillis

那么 nextPollTimeoutMillis 有两种可能:-1 或者下次唤醒的等待间隔时间,在执行到 nativePollOnce() 时就会进入休眠,等待再次被唤醒。

下次唤醒时,mMessage 必然会有一个待执行的 Message,则 MessageQueue.next() 返回到 Looper.loop() 的循环中,分发处理这个 Message,之后又是一轮新的 next() 中去循环。

2.4 framework 中如何使用 IdleHander?

到这里基本上就讲清楚 IdleHandler 如何使用以及一些细节,接下来我们来看看,在系统中,有哪些地方会用到 IdleHandler 机制。

在 AS 中搜索一下 IdleHandler。

简单解释一下:

  1. ActivityThread.Idler 在 ActivityThread.handleResumeActivity() 中调用。
  2. ActivityThread.GcIdler 是在内存不足时,强行 GC;
  3. Instrumentation.ActivityGoing 在 Activity onCreate() 执行前添加;
  4. Instrumentation.Idler 调用的时机就比较多了,是键盘相关的调用;
  5. TextToSpeechService.SynthThread 是在 TTS 合成完成之后发送广播;

有兴趣可以自己追一下源码,这些都是使用的场景,具体用 IdleHander 干什么,还是要看业务。

三.一些面试问题

到这里我们就讲清楚 IdleHandler 干什么?怎么用?有什么问题?以及使用中一些原理的讲解。

下面准备一些基本的问题,供大家理解。

Q:IdleHandler 有什么用?

  1. IdleHandler 是 Handler 提供的一种在消息队列空闲时,执行任务的时机;
  2. 当 MessageQueue 当前没有立即需要处理的消息时,会执行 IdleHandler;

Q:MessageQueue 提供了 add/remove IdleHandler 的方法,是否需要成对使用?

  1. 不是必须;
  2. IdleHandler.queueIdle() 的返回值,可以移除加入 MessageQueue 的 IdleHandler;

Q:当 mIdleHanders 一直不为空时,为什么不会进入死循环?

  1. 只有在 pendingIdleHandlerCount 为 -1 时,才会尝试执行 mIdleHander;
  2. pendingIdlehanderCount 在 next() 中初始时为 -1,执行一遍后被置为 0,所以不会重复执行;

Q:是否可以将一些不重要的启动服务,搬移到 IdleHandler 中去处理?

  1. 不建议;
  2. IdleHandler 的处理时机不可控,如果 MessageQueue 一直有待处理的消息,那么 IdleHander 的执行时机会很靠后;

Q:IdleHandler 的 queueIdle() 运行在那个线程?

  1. 这是陷进问题,queueIdle() 运行的线程,只和当前 MessageQueue 的 Looper 所在的线程有关;
  2. 子线程一样可以构造 Looper,并添加 IdleHandler;

三. 小结时刻

到这里就把 IdleHandler 的使用和原理说清楚了。

IdleHandler 是 Handler 提供的一种在消息队列空闲时,执行任务的时机。但它执行的时机依赖消息队列的情况,那么如果 MessageQueue 一直有待执行的消息时,IdleHandler 就一直得不到执行,也就是它的执行时机是不可控的,不适合执行一些对时机要求比较高的任务。

下面是我收集整理的些Android 往年面试题和知识点,内容对应的参考答案,耗时72小时最终将它们整理成了PDF文档。因文章篇幅长度限制,暂且只能用图片形式展现,如有需要请大家可以直接去我 CodeChina地址:https://codechina.csdn.net/u012165769/Android-T3 访问查阅。



如有需要请大家点击GitHub地址查看获取方式或简信回复【666】,望这些干货能够帮助到大家!!!

以上是关于面试官:“我们聊聊 Handler 吧?”的主要内容,如果未能解决你的问题,请参考以下文章

面试常用的代码片段

面向面试编程代码片段之GC

面试官:Java多线程有哪几种实现方式如何实现?

面试官:IO 操作必须要手动关闭吗?关闭流方法是不是有顺序?

“如何用我的编码打动面试官?我可以在我为打动面试官的问题编写的代码中采用哪些做法?

剑指Offer名企面试官精讲典型编程题pdf