安卓开发面试题(7/30)消息机制全解析(上)
牛客高级系列专栏:
安卓(安卓系统开发也要掌握)
- 想通关安卓面试,请看(承诺免费售后答疑):《150道安卓高频面试题目录及答案链接》
- 想通关安卓系统面试,请看:《140道安卓系统Framework面试题目录及答案链接》
- 想进阶安卓开发,请看(承诺免费售后答疑):《Android进阶知识体系解析_15大安卓进阶必备知识点》
- 想了解安卓APP完整开发流程,请看(承诺免费售后答疑):《安卓APP完整开发流程》
- 想掌握安卓App性能优化,请看(承诺免费售后答疑):《安卓性能优化讲解和实战专栏》
- 想掌握Gradle语法和配置,制作Gradle插件,请看(承诺免费售后答疑):《安卓Gradle语法解析和实践大全》
嵌入式
- 想通关嵌入式面试,请看: 《111道嵌入式面试题目录及答案链接》
- 想多掌握几个嵌入式项目,请看:《6个嵌入式项目交流分享(附源码)》
本人是2020年毕业于广东工业大学研究生:许乔丹,有国内大厂CVTE和世界500强企业安卓开发经验,该专栏整理本人对常见安卓高频开发面试题的理解;
网上安卓资料千千万,笔者将继续维护专栏,一杯奶茶价格不止提供答案解析,承诺提供专栏内容免费技术答疑,直接咨询即可。助您提高安卓面试准备效率,为您面试保驾护航!
正文开始⬇
如果不懂Handler,你敢去面试安卓吗? 你以为你真的懂Handler吗?不妨看看以下问题:
- 请介绍下Handler消息机制 ⭐⭐⭐⭐⭐
- Handler 引起的内存泄露原因以及最佳解决方案 ⭐⭐⭐⭐⭐
- 为什么我们能在主线程直接使用 Handler,而不需要创建 Looper ? ⭐⭐⭐⭐⭐
- Handler、Thread和HandlerThread的差别 ⭐⭐⭐⭐
- 子线程中怎么使用 Handler? ⭐⭐⭐⭐
- 为什么在子线程中创建 Handler 会抛异常?⭐⭐⭐⭐
- Handler 里藏着的 Callback 能干什么?⭐⭐⭐
- Handler 的 send 和 post 的区别?⭐⭐⭐⭐
- 创建 Message 实例的最佳方式 ⭐⭐⭐
- Message 的插入以及回收是如何进行的,如何实例化一个 Message 呢?⭐⭐⭐
- 妙用Looper机制,或者你知道Handler机制的其他用途吗?⭐⭐⭐
- Looper.loop()死循环一直运行是不是特别消耗CPU资源呢?不会造成应用卡死吗?⭐⭐⭐⭐⭐
- MessageQueue 中如何等待消息?为何不使用 Java 中的 wait/notify 来实现阻塞等待呢?⭐⭐
- 你知道延时消息的原理吗?⭐⭐⭐⭐
- handler postDelay这个延迟是怎么实现的?⭐⭐⭐⭐
- 如何保证在msg.postDelay情况下保证消息次序?⭐⭐⭐
- 更新UI的方式有哪些 ⭐⭐⭐⭐
- 线程、Handler、Looper、MessageQueue 的关系?⭐⭐⭐⭐
- 多个线程给 MessageQueue 发消息,如何保证线程安全?⭐⭐⭐
- View.post 和 Handler.post 的区别?⭐⭐⭐
- 你知道 IdleHandler 吗?⭐⭐
一个建议:学习Handler一定要啃下源码!
看完以下的解析,一定可以让面试官眼前一亮。
目录
- 1、什么是Handler消息传递机制
- 1.1 Handler的组成
- 1.2 Handler的使用和代码实例
- 2、源码分析
- 2.1 Handler机制源码分析
- 2.2 Handler发送信息的方法有几种
- 2.2.1 post方法
- 2.2.2 send方法
- 2.3 Message源码分析
- 2.4 Looper源码分析
- 2.5 MessageQueue源码分析
- 2.5.1 存放信息的方法:enqueueMessage()
- 2.5.2 获取消息的方法:next()
- 3、一问一答:常见问题汇总
- 3.1 Handler 引起的内存泄露原因以及最佳解决方案
- 3.2 为什么我们能在主线程直接使用 Handler,而不需要创建 Looper ?
- 3.3 Handler、Thread和HandlerThread的差别
- 3.4 子线程中怎么使用 Handler?
- 3.5 为什么在子线程中创建 Handler 会抛异常?
- 3.6 Handler 里藏着的 Callback 能干什么?
- 3.7 Handler 的 send 和 post 的区别?
- 3.8 创建 Message 实例的最佳方式
- 3.9 Message 的插入以及回收是如何进行的,如何实例化一个 Message 呢?
- 3.10 妙用Looper机制,或者你知道Handler机制的其他用途吗?
- 3.11 Looper.loop()死循环一直运行是不是特别消耗CPU资源呢?不会造成应用卡死吗?
- 3.12 MessageQueue 中如何等待消息?为何不使用 Java 中的 wait/notify 来实现阻塞等待呢?
- 3.13 你知道延时消息的原理吗?
- 3.14 handler postDelay这个延迟是怎么实现的?
- 3.15 如何保证在msg.postDelay情况下保证消息次序?
- 3.16 更新UI的方式有哪些
- 3.17 线程、Handler、Looper、MessageQueue 的关系?
- 3.18 多个线程给 MessageQueue 发消息,如何保证线程安全?
- 3.19 View.post 和 Handler.post 的区别?
- 3.20 你知道 IdleHandler 吗?
1、什么是Handler消息传递机制
1.1 Handler的组成
Handler消息传递机制,从名字看就可以联想到是Handler会发送出一个一个消息,同时系统会根据每一个不同的消息进行不同的处理流程。具体如何实现,直接上图。
图片来自参考目录1。
Handler由3个模块组成,Handler、MessageQueue、Looper:
- Handler:主要作用是发送信息以及处理信息(为何发送还自己处理?),其中发送的信息叫作Message,可以传递数据哦;
- MessageQueue:消息队列,由一个一个Message汇成,遵循先进先出规则,由Looper进行管理;
- Looper:从MessageQueue里读取消息,并按消息分发机制分配到目标Handler进行消息处理。
1.2 Handler的使用和代码实例
结合1.1小节,简述下完整的流程:
- Handler通过sendMessage()发送消息Message,每个Message都在MessageQueue里排队。
- Looper通过loop()从MessageQueue里读取Message,并按消息分发机制分配到目标Handler进行消息处理。
- 目标Handler收到需要处理的Message时,调用自己的handleMessage()方法来处理Message,因此自定义的Handler都需要重写handlerMessage方法。
private void updateBluetoothStatus(int state) {
new Thread(new Runnable() {
@Override
public void run() {
...
//1. 子线程执行完耗时操作后,触发更新蓝牙列表
if (mHandler != null) {
mHandler.removeMessages(H.MSG_XURUI_BLUETOOTH_LIST);
mHandler.sendEmptyMessage(H.MSG_XURUI_BLUETOOTH_LIST);
}
}
}).start();
}
private void updateBluetoothList() {
//3. 更新蓝牙列表
}
private static class H extends BaseHandler<BlueToothTestActivity> {
public static final int MSG_XURUI_BLUETOOTH_LIST = 100;
public H(BlueToothTestActivity activity) {
super(activity);
}
@Override
protected void handleMessage(BlueToothTestActivity activity, Message msg) {
if (msg.what == MSG_XURUI_BLUETOOTH_LIST) {
//2. 通过Handler机制调用updateBluetoothList
activity.updateBluetoothList();
}
}
}
我们可以看到子线程执行完耗时操作后,需要更新UI的时候,需要返回到主线程(子线程不能或不建议更新UI),因此在子线程通过Handler机制,很容易切换到Handler所在的主线程去执行updateBluetoothList()函数来更新UI。
仔细的同学应该有看到BaseHandler类,为何要封装Handler的基类,这是工程上提高代码稳定性的常用做法,后续马上揭晓,但需循序渐进,我们先看看Handler源码。
2、源码分析
因为Handler在面试中的地位,良心建议,再难都必须过一遍源码,我尽量讲的更容易理解。
2.1 Handler机制源码分析
创建Handler只要执行。。。,其源码见下分析:
public Handler() {
this(null, false);
}
public Handler(Callback callback, boolean async) {
//1:获取Looper
mLooper = Looper.myLooper();
//如果获取不到Looper会提示我们要事先运行Looper.prepare()
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
//2:消息队列,来自Looper对象
mQueue = mLooper.mQueue;
mCallback = callback;
//设置消息为异步处理方式
mAsynchronous = async;
}
注释2很重要,如果面试官问Looper和MessageQueue怎么关联在一起?那么现在就知道是在Handler的构造函数中关联的。重点继续看看注释1,Looper.myLooper()做了什么?
public static @Nullable Looper myLooper() {
return sThreadLocal.get();//1
}
从注释1看出来,仅仅是从sThreadLocal获取一个Looper变量。思路继续,我们从sThreadLocal变量入手,如何初始化的?在哪里使用?有get()那就一定有set()。继续研究源码:
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
public static void prepare() {
prepare(true);
}
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
//1:一个线程只能有一个 Looper
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));//2
}
如果某些数据在不同的线程作用域下有各自的数据副本,那么可以用ThreadLocal对数据进行线程隔离。从两个两个函数很容易知道,通过调用Looper.prepare()函数即可执行到注释2,将新new出来的Looper放到sThreadLocal里,供Looper.myLooper()去获取。
如果已经有实际开发经验的同学可能会想到通过Android studio创建的MainActivity可以直接用Handler,然而上一段才说使用Handler需要提前执行Looper.prepare()函数。其实这不矛盾,因为MainActivity所以在的主线程ActivityThread的main()函数已经自动执行了Looper.prepare()函数。 ActivityThread的main方法源码如下:
public static void main(String[] args) {
...
Looper.prepareMainLooper(); //1
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
public static void prepareMainLooper() {
prepare(false); //2
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has
already been prepared.");
}
sMainLooper = myLooper();
}
}
ActivityThread的main方法通过调用prepareMainLooper()进而调用prepare(false)方法,进而new一个Looper放到sThreadLocal里。
2.2 Handler发送信息的方法有几种
最重要的源码已经分析完了,我们先来看看Hander如何发送信息。发送消息有两种方式,post和send。
2.2.1 post方法
post方法有以下两个函数post()和postDelayed(),用法是Handler.post(Runnable r),举个例子:
public void readLog() {
myHandler.post(XRReadLog);
}
Runnable XRReadLog = new Runnable() {
@Override
public void run() {
// do something
}
};
用法非常简单,进一步看看post源码:
public final boolean post(Runnable r){
return sendMessageDelayed(getPostMessage(r), 0);
}
public final boolean postDelayed(Runnable r, long delayMillis){
return sendMessageDelayed(getPostMessage(r), delayMillis);
}
可以看到只是调用了sendMessageDelayed(),这就是send方法了。
注:getPostMessage()方法是将Runnable转换为Message,因为sendMessageDelayed接收的是Message参数。
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
2.2.2 send方法
send方法有3个,函数作用从名称上就可以一眼看出:
- sendMessage():发送带有数据的信息
- sendMessageDelayed():发送带有数据的信息并延迟执行
- sendEmptyMessage():发送空信息 就以sendMessageDelayed为例看看使用方法:
Message logMessage = Message.obtain();
logMessage.what = WIFI_CONNECT_LOG; //信息的名称
logMessage.arg1 = 1;
logMessage.obj = "some thing"; //信息所携带的数据
myHandler.sendMessageDelayed(logMessage,1000); //延迟1000毫秒执行
用法也比较简单,根据自己需要决定是否携带数据,是否延迟执行。看一下源码:
public final boolean sendMessage(Message msg){
return sendMessageDelayed(msg, 0);
}
public final boolean sendEmptyMessage(int what){
return sendEmptyMessageDelayed(what, 0);
}
public final boolean sendMessageDelayed(Message msg, long delayMillis){
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);//1
}
可以看出来,无论是post还是send,最后都是用SystemClock.uptimeMillis()获取系统开机到当前的时间,加上我们设置的delayMillis时间,并调用sendMessageAtTime()方法做进一步逻辑。
住:不能用System.currentTimeMilis(系统时间),因为用户很可能自己修改系统时间。
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
//转到 MessageQueue 的 enqueueMessage 方法
return queue.enqueueMessage(msg, uptimeMillis);//1
}
通过注释1,Handler发送的消息最终发送到消息队列。2.5小节详细讲queue.enqueueMessage(),我们先一起看一下Message和Looper的源码,这样可以更好的理解。
2.3 Message源码分析
Handler机制传递的Message是怎么生成的,每个Message里面都有什么数据。看下源码就可以很清楚的了解了:
public final class Message implements Parcelable {
public int what;//消息的标识
//系统自带的两个参数,注意是int型的,其他类型数据不能放这里
public int arg1;
public int arg2;
long when; //处理消息的相对时间
Bundle data;//可以使用message.setData()使用bundle的实型传参
Handler target;
Runnable callback;
Message next; //消息池是以链表结构存储 Message
private static Message sPool; //消息池中的头节点
public Message() {
}
//1:直接通过链表操作获得 Message (注释1)
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();
}
//回收消息
public void recycle() {
if (isInUse()) {
if (gCheckRecycle) {
throw new IllegalStateException("This message cannot be recycled because it "
+ "is still in use.");
}
return;
}
recycleUnchecked();
}
void recycleUnchecked() { //2
//清空消息的所有标志信息
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++;
}
}
}
}
通过源码,首先要知道Message可以传递数据,主要的方式有:
- what:用户自定义的消息识别标识
- arg1和arg2:只能传递int型参数
- obj:可以传递object类型参数,用得最多的就是传递String
- data:可以使用bundle类型传递 可以看看2.2.2小节的代码案例加深理解,所以在上面注释2的recycleUnchecked()方法里需要把对应的变量重置。
同时,从2.2.2小节的代码案例也可以看到我是用Message.obtain()来获得一个Message实例的,因为这种方案是直接从Message的消息池里直接获取,避免了new Message()的重复创建开销。
2.4 Looper源码分析
在2.1小节,就有分析到Looper.myLooper()、Looper.prepare() 、 Looper.prepareMainLooper()这三个方法,在此再做个复习:
- Looper.myLooper():从sThreadLocal获取已创建的Looper实例;
- Looper.prepare():创建Looper实例;
- Looper.prepareMainLooper() :主线程main函数中调用的方法,在该方法里会调用到Looper.prepare()来创建Looper实例。 剩下的源码还有:
public final class Looper {
final MessageQueue mQueue;
final Thread mThread;
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed); //1:在此创建MessageQueue
mThread = Thread.currentThread();
}
public void quit() {
mQueue.quit(false);
}
public void quitSafely() {
mQueue.quit(true);
}
public static void loop() { //2
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 (;;) {
//3;从MessageQueue 中取消息
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
//4:通过 Handler 分发消息
msg.target.dispatchMessage(msg);
//5:回收消息
msg.recycleUnchecked();
}
}
}
从注释1可以知道,MessageQueue是在Looper构建函数里生成的。这个知识点要记一下。
关键是看看注释2,这是整个Handler最重要的源码了,前面所有的源码都最终服务与loop()方法!从源码就能知道该方法中一直在死循环做三件事:
- 调用next方法从MessageQueue里获取Message;
- 通过dispatchMessage方法将Message分配到目标Handler;
- 通过recycleUnchecked方法回收Message;
其中next()函数待会分析MessageQueue一并分析,现在看看Handler.dispatchMessage 做了什么事:
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
//通过 handler.pos 形式传入的 Runnable,对应2.2.1小节
handleCallback(msg); //1
} else {
if (mCallback != null) {
//以 Handler(Handler.Callback) 写法
if (mCallback.handleMessage(msg)) { //2
return;
}
}
//以 Handler(){} 写法
handleMessage(msg); //3
}
}
private static void handleCallback(Message message) {
message.callback.run(); //4
}
可以看出dispatchMessage()就是按照既定的优先级策略,决定Message由谁去处理,我们优先级由高到低介绍:
- handleCallback(msg):对应注释1,Message里自带的callback优先级最高,实际是调用注释4,最终调用Runnable重写的run(),对应案例可以看看2.2.1小节,getPostMessage(Runnable r)方法。
- mCallback.handleMessage(msg):也就是Handler.Callback 写法,可以看出这个方法是有返回值的,如果返回true则注释3不会运行到;代码案例见下:
//创建mCallback
private final Handler.Callback mCallback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {switch (msg.what) {
case MSG_DELETE_INFORMATION: {
//do something
}
}
}
//创建mHandler,并传入mCallback
mHandler = new Handler(this.getMainLooper(), mCallback);
//使用
mHandler.sendEmptyMessageDelayed(MSG_DELETE_INFORMATION, 1000);
- handleMessage(msg):重写handlerMessage()方法,优先级最低,代码案例见下:
public class BaseHandler extends Handler {
@Override
public void handleMessage(Message msg) {
//do something
}
}
2.5 MessageQueue源码分析
恭喜你看到这里,这是最后一个源码分析了。 经过上面的分析,我们还遗留了两个关于MessageQueue的函数没有分析,分别是enqueueMessage()和next()。
2.5.1 存放信息的方法:enqueueMessage()
回到2.2.2小节Handler发送消息的函数最后调用了queue.enqueueMessage()将要发送的Message发送到MessageQueue里做处理。
boolean enqueueMessage(Message msg, long when) {
...
synchronized (this) {
if (mQuitting) { //正在退出时,回收msg,加入到消息池
msg.recycle();
return false;
}
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
//p为null(代表MessageQueue没有消息)或者msg的触发时间是队列中最早的, 则进入该该分支
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
//将消息按时间顺序插入到MessageQueue。一般地,不需要唤醒事件队列,除非
//消息队头存在barrier,并且此时Message是队列中最早的异步消息。
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
//遍历链表,找到合适位置插入新的消息
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) { //2
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p;
prev.next = msg;
}
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
如果是第一个触发的信息,则作为对头的消息,否则在注释2,根据when(相对时间)的大小排序找到合适的插入位置。到此就完成了Handler通过调用post或者send方法将一个Message发送到MessageQueue并放在合适的链表位置的逻辑。那么该如何取出来呢?
2.5.2 获取消息的方法:next()
Message next() {
int pendingIdleHandlerCount = -1; // 循环迭代的首次为-1
int nextPollTimeoutMillis = 0; //代表下一个消息到来前,还需要等待的时长
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
//1:阻塞操作,当等待nextPollTimeoutMillis时长,或者消息队列被唤醒,都会返回
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// 开始尝试获取信息,获取到就返回信息
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
//取出头节点 Message
Message msg = mMessages;
//2:当消息Handler为空时,在队列中找到下一个异步消息。
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
//3:如果消息不为空,根据Message的相对时间判断(when变量)立即取出还是延迟取出
if (msg != null) {
if (now < msg.when) {
// 下一个消息还没有准备好。设置一个下一轮的等待时间nextPollTimeoutMillis
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// 消息准备好了则成功从MessageQueue里获取一个Message并返回
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
msg.markInUse();
return msg;
}
} else {
// No more messages.(没有消息,-1代表一直等待)
nextPollTimeoutMillis = -1;
}
}
}
获取一个Message的步骤如下:
- 注释1是阻塞操作,nextPollTimeoutMillis代表下一个消息到来前,还需要等待的时长,当为-1时代表一直等待。当等待时间到了或者消息队列被唤醒,则开始从队列里获取信息;
- 注释2,如果获取的信息是空的,则找下一个节点;
- 注释3,获取到非空的信息后,判断是否立刻取出来还是等待一段时间后取出,但最后都会成功返回一个Message;
- 如果队列里没有信息了,则在for循环里又回到了注释1进行阻塞等待。
也就是说next()方法根据消息出发的相对时间,返回下一条需要执行的消息,队列中消息为空时,则会进行阻塞操作。
参考目录
#提供免费售后答疑!!花一杯奶茶的钱获得安卓面试答疑服务,稳赚不赔# Android发展已经很多年,安卓资料网上千千万,本专栏免费提供专栏内容技术答疑!!私聊当天必回。在阅读过程或者其他安卓学习过程有疑问,都非常欢迎私聊交流。