27道 Handler 经典面试题,你能答出多少?

简介: 27道 Handler 经典面试题,你能答出多少?
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
return enqueueMessage(queue, msg, uptimeMillis);
}


sendMessageDelayed方法主要计算了消息需要被处理的时间,如果delayMillis为0,那么消息的处理时间就是当前时间。


然后就是关键方法enqueueMessage。


boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
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;
prev.next = msg;
}
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}


不懂得地方先不看,只看我们想看的:


首先设置了Message的when字段,也就是代表了这个消息的处理时间


然后判断当前队列是不是为空,是不是即时消息,是不是执行时间when大于表头的消息时间,满足任意一个,就把当前消息msg插入到表头。


否则,就需要遍历这个队列,也就是链表,找出when小于某个节点的when,找到后插入。


好了,其他内容暂且不看,总之,插入消息就是通过消息的执行时间,也就是when字段,来找到合适的位置插入链表。


具体方法就是通过死循环,使用快慢指针p和prev,每次向后移动一格,直到找到某个节点p的when大于我们要插入消息的when字段,则插入到p和prev之间。 或者遍历到链表结束,插入到链表结尾。


所以,MessageQueue就是一个用于存储消息、用链表实现的特殊队列结构。


5、延迟消息是怎么实现的?


总结上述内容,延迟消息的实现主要跟消息的统一存储方法有关,也就是上文说过的enqueueMessage方法。


无论是即时消息还是延迟消息,都是计算出具体的时间,然后作为消息的when字段进程赋值。


然后在MessageQueue中找到合适的位置(安排when小到大排列),并将消息插入到MessageQueue中。


这样,MessageQueue就是一个按照消息时间排列的一个链表结构。


6、MessageQueue的消息怎么被取出来的?


刚才说过了消息的存储,接下来看看消息的取出,也就是queue.next方法。


Message next() {
for (;😉 {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, 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 && 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;
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
}
}
}


奇怪,为什么取消息也是用的死循环呢?


其实死循环就是为了保证一定要返回一条消息,如果没有可用消息,那么就阻塞在这里,一直到有新消息的到来。


其中,nativePollOnce方法就是阻塞方法,nextPollTimeoutMillis参数就是阻塞的时间。


那什么时候会阻塞呢?两种情况:


1、有消息,但是当前时间小于消息执行时间,也就是代码中的这一句:

if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
}


这时候阻塞时间就是消息时间减去当前时间,然后进入下一次循环,阻塞。


2、没有消息的时候,也就是上述代码的最后一句:

if (msg != null) {}
else {
// No more messages.
nextPollTimeoutMillis = -1;
}


-1就代表一直阻塞。


7、MessageQueue没有消息时候会怎样?阻塞之后怎么唤醒呢?说说pipe/epoll机制?


接着上文的逻辑,当消息不可用或者没有消息的时候就会阻塞在next方法,而阻塞的办法是通过pipe/epoll机制


epoll机制是一种IO多路复用的机制,具体逻辑就是一个进程可以监视多个描述符,当某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作,这个读写操作是阻塞的。在Android中,会创建一个Linux管道(Pipe)来处理阻塞和唤醒。


当消息队列为空,管道的读端等待管道中有新内容可读,就会通过epoll机制进入阻塞状态。


当有消息要处理,就会通过管道的写端写入内容,唤醒主线程。


那什么时候会怎么唤醒消息队列线程呢?


还记得刚才插入消息的enqueueMessage方法中有个needWake字段吗,很明显,这个就是表示是否唤醒的字段。


其中还有个字段是mBlocked,看字面意思是阻塞的意思,去代码里面找找:


Message next() {
for (;😉 {
synchronized (this) {
if (msg != null) {
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
return msg;
}
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}
}
}
}


在获取消息的方法next中,有两个地方对mBlocked赋值:


当获取到消息的时候,mBlocked赋值为false,表示不阻塞。


当没有消息要处理,也没有idleHandler要处理的时候,mBlocked赋值为true,表示阻塞。


好了,确实这个字段就表示是否阻塞的意思,再去看看enqueueMessage方法中,唤醒机制:


boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
boolean needWake;
if (p == null || when == 0 || when < p.when) {
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;
prev.next = msg;
}
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}


当链表为空或者时间小于表头消息时间,那么就插入表头,并且设置是否唤醒为mBlocked。

再结合上述的例子,也就是当有新消息要插入表头了,这时候如果之前是阻塞状态(mBlocked=true),那么就要唤醒线程了。


否则,就需要取链表中找到某个节点并插入消息,在这之前需要赋值needWake = mBlocked && p.target == null && msg.isAsynchronous()

也就是在插入消息之前,需要判断是否阻塞,并且表头是不是屏障消息,并且当前消息是不是异步消息。 也就是如果现在是同步屏障模式下,那么要插入的消息又刚好是异步消息,那就不用管插入消息问题了,直接唤醒线程,因为异步消息需要先执行。


最后一点,是在循环里,如果发现之前就存在异步消息,那就还是设置是否唤醒为false。

意思就是,如果之前有异步消息了,那肯定之前就唤醒过了,这时候就不需要再次唤醒了。


最后根据needWake的值,决定是否调用nativeWake方法唤醒next()方法。


8、同步屏障和异步消息是怎么实现的?


其实在Handler机制中,有三种消息类型:


同步消息。也就是普通的消息。


异步消息。通过setAsynchronous(true)设置的消息。


同步屏障消息。通过postSyncBarrier方法添加的消息,特点是target为空,也就是没有对应的handler。


这三者之间的关系如何呢?


正常情况下,同步消息和异步消息都是正常被处理,也就是根据时间when来取消息,处理消息。


当遇到同步屏障消息的时候,就开始从消息队列里面去找异步消息,找到了再根据时间决定阻塞还是返回消息。


Message msg = mMessages;
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}


也就是说同步屏障消息不会被返回,他只是一个标志,一个工具,遇到它就代表要去先行处理异步消息了。


所以同步屏障和异步消息的存在的意义就在于有些消息需要“加急处理”。


9、同步屏障和异步消息有具体的使用场景吗?


使用场景就很多了,比如绘制方法scheduleTraversals。


void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 同步屏障,阻塞所有的同步消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 通过 Choreographer 发送绘制任务
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);


在该方法中加入了同步屏障,后续加入一个异步消息MSG_DO_SCHEDULE_CALLBACK,最后会执行到FrameDisplayEventReceiver,用于申请VSYNC信号。


10、Message消息被分发之后会怎么处理?消息怎么复用的?


再看看loop方法,在消息被分发之后,也就是执行了dispatchMessage方法之后,还偷偷做了一个操作——recycleUnchecked。


public static void loop() {
for (;😉 {
Message msg = queue.next(); // might block
try {
msg.target.dispatchMessage(msg);
}
msg.recycleUnchecked();
}
}
//Message.java
private static Message sPool;
private static final int MAX_POOL_SIZE = 50;
void recycleUnchecked() {
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++;
}
}
}


在recycleUnchecked方法中,释放了所有资源,然后将当前的空消息插入到sPool表头。


这里的sPool就是一个消息对象池,它也是一个链表结构的消息,最大长度为50。


那么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();
}


直接复用消息池sPool中的第一条消息,然后sPool指向下一个节点,消息池数量减一。


11、Looper是干嘛呢?怎么获取当前线程的Looper?为什么不直接用Map存储线程和对象呢?


在Handler发送消息之后,消息就被存储到MessageQueue中,而Looper就是一个管理消息队列的角色。 Looper会从MessageQueue中不断的查找消息,也就是loop方法,并将消息交回给Handler进行处理。


而Looper的获取就是通过ThreadLocal机制:


static final ThreadLocal sThreadLocal = new ThreadLocal();
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));
}
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}


通过prepare方法创建Looper并且加入到sThreadLocal中,通过myLooper方法从sThreadLocal中获取Looper。


12、ThreadLocal运行机制?这种机制设计的好处?


下面就具体说说ThreadLocal运行机制。


//ThreadLocal.java
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings(“unchecked”)
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}


从ThreadLocal类中的get和set方法可以大致看出来,有一个ThreadLocalMap变量,这个变量存储着键值对形式的数据。


  • key为this,也就是当前ThreadLocal变量。
  • value为T,也就是要存储的值。

然后继续看看ThreadLocalMap哪来的,也就是getMap方法:


//ThreadLocal.java
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;


原来这个ThreadLocalMap变量是存储在线程类Thread中的。


所以ThreadLocal的基本机制就搞清楚了:


在每个线程中都有一个threadLocals变量,这个变量存储着ThreadLocal和对应的需要保存的对象。


这样带来的好处就是,在不同的线程,访问同一个ThreadLocal对象,但是能获取到的值却不一样。


挺神奇的是不是,其实就是其内部获取到的Map不同,Map和Thread绑定,所以虽然访问的是同一个ThreadLocal对象,但是访问的Map却不是同一个,所以取得值也不一样。


这样做有什么好处呢?为什么不直接用Map存储线程和对象呢?


打个比方:


de5dc2da9a8e8b797a2411eb4ee6f381_63734ee9a564845d36b888c337483664.png


  • ThreadLocal就是老师。
  • Thread就是同学。
  • Looper(需要的值)就是铅笔。


现在老师买了一批铅笔,然后想把这些铅笔发给同学们,怎么发呢?两种办法:


  • 1、老师把每个铅笔上写好每个同学的名字,放到一个大盒子里面去(map),用的时候就让同学们自己来找。

这种做法就是Map里面存储的是同学和铅笔,然后用的时候通过同学来从这个Map里找铅笔。


这种做法就有点像使用一个Map,存储所有的线程和对象,不好的地方就在于会很混乱,每个线程之间有了联系,也容易造成内存泄漏。


  • 2、老师把每个铅笔直接发给每个同学,放到同学的口袋里(map),用的时候每个同学从口袋里面拿出铅笔就可以了。

这种做法就是Map里面存储的是老师和铅笔,然后用的时候老师说一声,同学只需要从口袋里拿出来就行了。


很明显这种做法更科学,这也就是ThreadLocal的做法,因为铅笔本身就是同学自己在用,所以一开始就把铅笔交给同学自己保管是最好的,每个同学之间进行隔离。


13、还有哪些地方运用到了ThreadLocal机制?


比如:Choreographer。

public final class Choreographer {
// Thread local storage for the choreographer.
private static final ThreadLocal sThreadInstance =
new ThreadLocal() {
@Override
protected Choreographer initialValue() {
Looper looper = Looper.myLooper();
if (looper == null) {
throw new IllegalStateException(“The current thread must have a looper!”);
}
Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
if (looper == Looper.getMainLooper()) {
mMainInstance = choreographer;
}
return choreographer;
}
};
private static volatile Choreographer mMainInstance;


Choreographer主要是主线程用的,用于配合 VSYNC 中断信号。


所以这里使用ThreadLocal更多的意义在于完成线程单例的功能。


14、可以多次创建Looper吗?


Looper的创建是通过Looper.prepare方法实现的,而在prepare方法中就判断了,当前线程是否存在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));
}
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}


所以同一个线程,只能创建一个Looper,多次创建会报错。


15、Looper中的quitAllowed字段是啥?有什么用?


按照字面意思就是是否允许退出,我们看看他都在哪些地方用到了:


void quit(boolean safe) {
if (!mQuitAllowed) {
throw new IllegalStateException(“Main thread not allowed to quit.”);
}
synchronized (this) {
if (mQuitting) {
return;
}
mQuitting = true;
if (safe) {
removeAllFutureMessagesLocked();
} else {
removeAllMessagesLocked();
}
}
}


哦,就是这个quit方法用到了,如果这个字段为false,代表不允许退出,就会报错。


但是这个quit方法又是干嘛的呢?从来没用过呢。 还有这个safe又是啥呢?


其实看名字就差不多能了解了,quit方法就是退出消息队列,终止消息循环。


  • 首先设置了mQuitting字段为true。
  • 然后判断是否安全退出,如果安全退出,就执行removeAllFutureMessagesLocked方法,它内部的逻辑是清空所有的延迟消息,之前没处理的非延迟消息还是需要取处理,然后设置非延迟消息的下一个节点为空(p.next=null)。
  • 如果不是安全退出,就执行removeAllMessagesLocked方法,直接清空所有的消息,然后设置消息队列指向空(mMessages = null)


然后看看当调用quit方法之后,消息的发送和处理:

相关文章
|
24天前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
|
27天前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
47 1
|
3月前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android 消息处理机制估计都被写烂了,但是依然还是要写一下,因为Android应用程序是通过消息来驱动的,Android某种意义上也可以说成是一个以消息驱动的系统,UI、事件、生命周期都和消息处理机制息息相关,并且消息处理机制在整个Android知识体系中也是尤其重要,在太多的源码分析的文章讲得比较繁琐,很多人对整个消息处理机制依然是懵懵懂懂,这篇文章通过一些问答的模式结合Android主线程(UI线程)的工作原理来讲解,源码注释很全,还有结合流程图,如果你对Android 消息处理机制还不是很理解,我相信只要你静下心来耐心的看,肯定会有不少的收获的。
202 3
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
|
4月前
|
消息中间件 调度 Android开发
Android经典面试题之View的post方法和Handler的post方法有什么区别?
本文对比了Android开发中`View.post`与`Handler.post`的使用。`View.post`将任务加入视图关联的消息队列,在视图布局后执行,适合视图操作。`Handler.post`更通用,可调度至特定Handler的线程,不仅限于视图任务。选择方法取决于具体需求和上下文。
53 0
|
Android开发
Android面试常客之Handler全解1
Android面试常客之Handler全解
|
消息中间件 Android开发
Android面试常客之Handler全解2
Android面试常客之Handler全解
|
消息中间件
一个 Handler 面试题引发的血案!!!
一个 Handler 面试题引发的血案!!!
116 0
|
消息中间件 Android开发
|
消息中间件 调度 Android开发
面试:Handler 的工作原理是怎样的?
面试场景 平时开发用到其他线程吗?都是如何处理的? 基本都用 RxJava 的线程调度切换,嗯对,就是那个 observeOn 和 subscribeOn 可以直接处理,比如网络操作,RxJava 提供了一个叫 io 线程的处理。
1212 0
|
消息中间件 Android开发
Android 面试(五):探索 Android 的 Handler
这是 面试系列 的第五期。本期我们将来探讨一下 Android 异步消息处理线程 —— Handler。 往期内容传递:Android 面试(一):说说 Android 的四种启动模式Android 面试(二):如何理解 Activity 的生命周期Android 面试(三):用广播 BroadcastReceiver 更新 UI 界面真的好吗?Android 面试(四):Android Service 你真的能应答自如了吗? 开始 Android 的消息机制,也就是 Handler 机制,相信各位都已经是烂熟于心了吧。
1539 0