面试官:“看你简历上写熟悉 Handler 机制,那聊聊 IdleHandler 吧?”
一. 序
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 是一个基于消息触发时间的优先级队列,所以队列出现空闲存在两种场景。
- MessageQueue 为空,没有消息;
- 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 执行的主逻辑:
- 准备执行 IdleHandler 时,说明当前待执行的消息为 null,或者这条消息的执行时间未到;
- 当
pendingIdleHandlerCount < 0
时,根据mIdleHandlers.size()
赋值给pendingIdleHandlerCount
,它是后期循环的基础; - 将
mIdleHandlers
中的 IdleHandler 拷贝到mPendingIdleHandlers
数组中,这个数组是临时的,之后进入 for 循环; - 循环中从数组中取出 IdleHandler,并调用其
queueIdle()
记录返回值存到keep
中; - 当
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。
简单解释一下:
- ActivityThread.Idler 在
ActivityThread.handleResumeActivity()
中调用。 - ActivityThread.GcIdler 是在内存不足时,强行 GC;
- Instrumentation.ActivityGoing 在 Activity onCreate() 执行前添加;
- Instrumentation.Idler 调用的时机就比较多了,是键盘相关的调用;
- TextToSpeechService.SynthThread 是在 TTS 合成完成之后发送广播;
有兴趣可以自己追一下源码,这些都是使用的场景,具体用 IdleHander 干什么,还是要看业务。
三.一些面试问题
到这里我们就讲清楚 IdleHandler 干什么?怎么用?有什么问题?以及使用中一些原理的讲解。
下面准备一些基本的问题,供大家理解。
Q:IdleHandler 有什么用?
- IdleHandler 是 Handler 提供的一种在消息队列空闲时,执行任务的时机;
- 当 MessageQueue 当前没有立即需要处理的消息时,会执行 IdleHandler;
Q:MessageQueue 提供了 add/remove IdleHandler 的方法,是否需要成对使用?
- 不是必须;
- IdleHandler.queueIdle() 的返回值,可以移除加入 MessageQueue 的 IdleHandler;
Q:当 mIdleHanders 一直不为空时,为什么不会进入死循环?
- 只有在 pendingIdleHandlerCount 为 -1 时,才会尝试执行 mIdleHander;
- pendingIdlehanderCount 在 next() 中初始时为 -1,执行一遍后被置为 0,所以不会重复执行;
Q:是否可以将一些不重要的启动服务,搬移到 IdleHandler 中去处理?
- 不建议;
- IdleHandler 的处理时机不可控,如果 MessageQueue 一直有待处理的消息,那么 IdleHander 的执行时机会很靠后;
Q:IdleHandler 的 queueIdle() 运行在那个线程?
- 陷进问题,queueIdle() 运行的线程,只和当前 MessageQueue 的 Looper 所在的线程有关;
- 子线程一样可以构造 Looper,并添加 IdleHandler;
三. 小结时刻
到这里就把 IdleHandler 的使用和原理说清除了。
IdleHandler 是 Handler 提供的一种在消息队列空闲时,执行任务的时机。但它执行的时机依赖消息队列的情况,那么如果 MessageQueue 一直有待执行的消息时,IdleHandler 就一直得不到执行,也就是它的执行时机是不可控的,不适合执行一些对时机要求比较高的任务。
本文就到这里,对你有帮助吗?有任何问题欢迎留言。觉得有帮助别忘了转发、点好看,谢谢!
推荐阅读:
公众号后台回复成长『成长』,将会得到我准备的学习资料。
面试官:“看你简历上写熟悉 Handler 机制,那聊聊 IdleHandler 吧?”的更多相关文章
- Java 内存模型都不会,就敢在简历上写熟悉并发编程吗
从 PC 内存架构到 Java 内存模型 你知道 Java 内存模型 JMM 吗?那你知道它的三大特性吗? Java 是如何解决指令重排问题的? 既然CPU有缓存一致性协议(MESI),为什么 JMM ...
- 不会看 Explain执行计划,劝你简历别写熟悉 SQL优化
昨天中午在食堂,和部门的技术大牛们坐在一桌吃饭,作为一个卑微技术渣仔默默的吃着饭,听大佬们高谈阔论,研究各种高端技术,我TM也想说话可实在插不上嘴. 聊着聊着突然说到他上午面试了一个工作6年的程序员, ...
- 答应我,不会这些概念,简历不要写 “熟悉” zookeeper
整理了一些Java方面的架构.面试资料(微服务.集群.分布式.中间件等),有需要的小伙伴可以关注公众号[程序员内点事],无套路自行领取 一口气说出 9种 分布式ID生成方式,面试官有点懵了 面试总被问 ...
- 阿里P7面试官:请你简单说一下类加载机制的实现原理?
面试题:类加载机制的原理 面试官考察点 考察目标: 了解面试者对JVM的理解,属于面试八股文系列. 考察范围: 工作3年以上. 技术背景知识 在回答这个问题之前,我们需要先了解一下什么是类加载机制? ...
- 面试官:看你简历说写精通ThreadLocal,这几道题你都会吗?
问题 和Synchronized的区别 存储在jvm的哪个区域 真的只是当前线程可见吗 会导致内存泄漏么 为什么用Entry数组而不是Entry对象 你学习的开源框架哪些用到了ThreadLocal ...
- 面试官:能用JS写一个发布订阅模式吗?
目录 1 场景引入 2 代码优化 2.1 解决增加粉丝问题 2.2 解决添加作品问题 3 观察者模式 4 经纪人登场 5 发布订阅模式 6 观察者模式和发布订阅模式的对比 什么是发布订阅模式?能手写实 ...
- 硬核剖析ThreadLocal源码,面试官看了直呼内行
工作面试中经常遇到ThreadLocal,但是很多同学并不了解ThreadLocal实现原理,到底为什么会发生内存泄漏也是一知半解?今天一灯带你深入剖析ThreadLocal源码,总结ThreadLo ...
- 硬核解析MySQL的MVCC实现原理,面试官看了都直呼内行
1. 什么是MVCC MVCC全称是Multi-Version Concurrency Control(多版本并发控制),是一种并发控制的方法,通过维护一个数据的多个版本,减少读写操作的冲突. 如果没 ...
- 一个HashMap能跟面试官扯上半个小时
一个HashMap能跟面试官扯上半个小时 <安琪拉与面试官二三事>系列文章 一个HashMap能跟面试官扯上半个小时 一个synchronized跟面试官扯了半个小时 一个volatile ...
随机推荐
- beetlex网关之聚合和url请求过虑
在这里主要介绍beetlex应用网关的两个插件,分别是聚合和url请求过虑.通过聚合插件可以把整合多个请求的数据来应答请求端,而Url请求过虑同可以拒绝一些有非常关键字的请求. 请求聚合 在网关服务中 ...
- JVM之JVM的体系结构
一.JDK的组成 JDK:JDK是Java开发工具包,是Sun Microsystems针对Java开发员的产品.JDK中包含JRE(在JDK的安装目录下有一个名为jre的目录,里面有两个文件夹bin ...
- Airbnb如何应用AARRR策略成为全球第一民宿平台
案例背景 基于房东和租客的痛点构建短租平台,但困于缓慢增长 2007年,住在美国旧金山的两位设计师——BrianChesky与Joe Gebbia正在为他们付不起房租而困扰.为了赚点外块,他们计划将阁 ...
- 从头学pytorch(十九):批量归一化batch normalization
批量归一化 论文地址:https://arxiv.org/abs/1502.03167 批量归一化基本上是现在模型的标配了. 说实在的,到今天我也没搞明白batch normalize能够使得模型训练 ...
- Spring学习记录6——ThreadLocal简介
Spring通过各种模板类降低了开发者使用各种数据持久化技术的难度.这些模板类是线程安全的,所以 多个DAO可以复用同一个模板实例而不会发生冲突.在使用模板类访问底层数据时,模板类需要绑定数据连接或者 ...
- Xcode10:library not found for -lstdc++.6.0.9 临时解决
1.https://pan.baidu.com/s/1IkbZb6qaxgvghP1HEFQa6w?errno=0&errmsg=Auth%20Login%20Sucess&& ...
- 创建django报错使用miniconda
sqlite文件缺失 下载地址 https://sqlite.org/download.html https://blog.csdn.net/xuzhexing/article/details/905 ...
- spring boot 整合 swagger2
swagger2为了更好的管理api文档接口 swagger构建的api文档如下,清晰,避免了手写api诸多痛点 一,添加依赖 <!--swagger2的官方依赖--> <depen ...
- maven常用的远程仓库地址
<mirror> <id>nexus-aliyun</id> <name>Nexus aliyun</name> <url>ht ...
- 通过Excel表创建sql脚本
Excel.sql脚本 1)准备好存有数据的excel表格: 这里我们有些小技巧可以让表下面和右边的表格隐藏,在第8行的位置按住“Ctrl+Shift+↓”可以选定下面的空格,然后鼠标右键 隐藏即可, ...