写点什么

27 道 Handler 经典面试题,你能答出多少?,关于 Android 程序员最近的状况

用户头像
Android架构
关注
发布于: 刚刚

}


其实就是判断了当前线程 是否是 ViewRootImpl创建时候的线程,如果不是,就会崩溃。


而 ViewRootImpl 创建的时机就是界面被绘制的时候,也就是 onResume 之后,所以如果在子线程进行 UI 更新,就会发现当前线程(子线程)和 View 创建的线程(主线程)不是同一个线程,发生崩溃。


解决办法有三种:


  • 在新建视图的线程进行这个视图的 UI 更新,主线程创建 View,主线程更新 View。

  • ViewRootImpl创建之前进行子线程的 UI 更新,比如 onCreate 方法中进行子线程更新 UI。

  • 子线程切换到主线程进行 UI 更新,比如Handler、view.post方法。


4、MessageQueue 是干嘛呢?用的什么数据结构来存储数据?




看名字应该是个队列结构,队列的特点是什么?先进先出,一般在队尾增加数据,在队首进行取数据或者删除数据。


Hanlder中的消息似乎也满足这样的特点,先发的消息肯定就会先被处理。但是,Handler中还有比较特殊的情况,比如延时消息。


延时消息的存在就让这个队列有些特殊性了,并不能完全保证先进先出,而是需要根据时间来判断,所以Android中采用了链表的形式来实现这个队列,也方便了数据的插入。


来一起看看消息的发送过程,无论是哪种方法发送消息,都会走到sendMessageDelayed方法


public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {


if (delayMillis < 0) {


delayMillis = 0;


}


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<Looper> sThreadLocal = new ThreadLocal<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));


}


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 存储线程和对象呢?


打个比方:



  • 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<Choreographer> sThreadInstance =


new ThreadLocal<Choreographer>() {


@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 方法之后,消息的发送和处理:


//消息发送


boolean enqueueMessage(Message msg, long when) {


synchronized (this) {


if (mQuitting) {


IllegalStateException e = new IllegalStateException(


msg.target + " sending message to a Handler on a dead thread");


Log.w(TAG, e.getMessage(), e);


msg.recycle();


return false;


}


}


当调用了 quit 方法之后,mQuitting为 true,消息就发不出去了,会报错。


再看看消息的处理,loop 和 next 方法:


Message next() {


for (;;) {


synchronized (this) {


if (mQuitting) {


dispose();


return null;


}


}


}


}


public static void loop() {


for (;;) {


Message msg = queue.next();


if (msg == null) {


// No message indicates that the message queue is quitting.


return;


}


}


}


很明显,当mQuitting为 true 的时候,next 方法返回 null,那么 loop 方法中就会退出死循环。


那么这个quit方法一般是什么时候使用呢?


  • 主线程中,一般情况下肯定不能退出,因为退出后主线程就停止了。所以是当 APP 需要退出的时候,就会调用 quit 方法,涉及到的消息是 EXIT_APPLICATION,大家可以搜索下。

  • 子线程中,如果消息都处理完了,就需要调用 quit 方法停止消息循环。


16、Looper.loop 方法是死循环,为什么不会卡死(ANR)?




我大致总结下:


  • 1、主线程本身


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


就是需要一只运行的,因为要处理各个 View,界面变化。所以需要这个死循环来保证主线程一直执行下去,不会被退出。


  • 2、真正会卡死的操作是在某个消息处理的时候操作时间过长,导致掉帧、ANR,而不是 loop 方法本身。

  • 3、在主线程以外,会有其他的线程来处理接受其他进程的事件,比如Binder线程(ApplicationThread),会接受 AMS 发送来的事件

  • 4、在收到跨进程消息后,会交给主线程的Hanlder再进行消息分发。所以 Activity 的生命周期都是依靠主线程的Looper.loop,当收到不同 Message 时则采用相应措施,比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终执行到 onCreate 方法。

  • 5、当没有消息的时候,会阻塞在 loop 的queue.next()中的nativePollOnce()方法里,此时主线程会释放 CPU 资源进入休眠状态,直到下个消息到达或者有事务发生。所以死循环也不会特别消耗 CPU 资源。


17、Message 是怎么找到它所属的 Handler 然后进行分发的?




在 loop 方法中,找到要处理的Message,然后调用了这么一句代码处理消息:


msg.target.dispatchMessage(msg);


所以是将消息交给了msg.target来处理,那么这个 target 是啥呢?


找找它的来头:


//Handler


private boolean enqueueMessage(MessageQueue queue,Message msg,long uptimeMillis) {


msg.target = this;


return queue.enqueueMessage(msg, uptimeMillis);


}


在使用 Hanlder 发送消息的时候,会设置msg.target = this,所以 target 就是当初把消息加到消息队列的那个 Handler。


18、Handler 的 post(Runnable) 与 sendMessage 有什么区别




Hanlder 中主要的发送消息可以分为两种:

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
27道 Handler 经典面试题,你能答出多少?,关于Android程序员最近的状况