美文网首页Android
【学习】面试必问的Handler

【学习】面试必问的Handler

作者: Merbng | 来源:发表于2021-05-24 10:54 被阅读0次

概述

在Android中,很多地方是通过消息机制驱动的,例如线程间通信、四大组件的启动等等。
消息机制中主要涉及 的类有:Handler&Looper&MessageQueue&Message,其中Handler可以说是消息机制提供给Java层的上层接口

概念模型

Handler怎么进行线程通信,原理是什么?

消息机制其实并不是Android系统独有的设计,在很多系统设计中都可以看到消息机制的身影,
例如IOS的runLoop、Web的Ajax和Spring的消息队列等。在所有系统设计的消息机制里都会有生产者与消费者的概念,如下模型:

消息机制概念模型
其中消息缓冲区的具体实现可以是&队列,因为队列(特别是优先级队列)是最常见的, 所以很多情况都会直接将消息缓冲区称为消息队列。

架构图

【类图】

  • Looper里组合了MessageQueue消息队列,创建Looper的同事也创建了MessageQueue
  • MessageQueue里组合了待处理的Message链表
  • Message持有用于处理消息的Handler(target)
  • Handler被创建时需要聚合Looper与MessageQueue,默认使用的是当前线程的Looper

消息的享元模式

消息机制里需要频繁创建消息对象(Message),因此消息对象需要使用享元模式来缓存,以避免重复分配&回收内存,具体来说,Message使用的是有容量限制的、无头节点的单链表的对象池:



Message的数据结构

Handler核心源码分析

2.1 启动消息循环

  • Looper如何在子线程中创建?
    要在哪个线程启动消息循环, 就需要在该线程执行Looper.prepare() & Looper.loop()。只有调用Looper.loop()之后,消息循环才算真正运转起来了,具体来说,启动消息循环的分为两种情况:主线程消息循环&子线程消息循环,前者由Framework启动,而后者需要我们自己启动:
  • 主线程消息循环

    可以看到,在应用启动时,Framework已经为主线程开启了消息循环,后续我们熟悉的startActivity&startService都是通过主线程消息循环来驱动的。
  • 子线程消息循环

    在子线程开启消息循环,我们需要自己调用Looper.prepare() & Looper.loop()。可以直接创建线程,或者使用HandlerThread,后者主要考虑的多线程中获取Looper的同步问题。
    小结一下:
    创建Handler的代码需要放在Looper.prepare(); & Looper.loop();中间执行,这是因为创建Handler对象时需要聚合Looper对象(默认使用的是当前线程的Looper),而只有执行Looper.prepare()之后,才会创建该线程私有的Looper对象,否则创建Handler会抛异常。
  • Looper线程唯一性

问:说一下Looper、handler、线程间的关系,例如一个线程可以对应几个Looper、几个Handler?
问:ThreadLocal的原理,以及在Looper是如何应用的?

每个线程只允许调用一次Looper.prepare(),否则会抛异常,这样设计是因为一个Looper对应了一个消息循环,而一个线程进行多个消息循环是没有意义的(一个线程不可能同时进行两个死循环)。那么Handler是如何保证Looper线程唯一性的呢?
答:首先,Handler主要利用了ThreadLocal在每个线程单独存储副本的特性,保证了一个ThreadLocal<Looper>在不同线程存取的Looper对象相互独立;其次,ThreadLocal是Looper的一个static final变量,这样就保证了整个进程中sThreadLocal对象不可变;第三,Looper.prepare()判断在一个线程里重复调用,则会抛出异常。

消息发送

  • 问:Handler.post(Runable) 是如何执行的?
  • 问:Handler.sendMessage()和Handler.postDelay()的区别?
  • 多个Handler发消息时,消息队列如何保证线程安全?
  • 为什么MessageQueue不设置消息上限?

消息发送的API非常多,最终它们都会调用到Handler.sendMessageAtTime(Message msg,long uptimeMillis),内部会交给MessageQueue.enqueue(Message msg,long when)处理,梳理如下:

消息发送调用链
消息入队关键源码
小结一下:
  • 每个消息的处理时间(when)不一样(SystemClock.uptimeMillis() + delayMill)
  • 消息入队时,根据消息的处理时间(when)做插入排序,队头的消息就是最需要执行的消息
  • 当消息队列不为空时(一般不需要唤醒),只有当开启同步屏障后第一个异步消息需要唤醒(开启同步屏障会在队首插入一个占位消息,此时消息队列不为空,但是线程可能是阻塞的)。

消息获取

  • 问:消息队列无消息会怎么样?为什么block不会ANR?
  • 问:Looper死循环为什么不会ANR?
  • 问:Looper死循环为什么不阻塞主线程?
  • 问:Handler内存泄露的原因?
    消息入队后,Looper所在线程就会被唤醒(如果被堵塞),以继续消息循环。
    在消息循环中,Looper.loop()会死循环从MessageQueue获取队首消息,因为消息已经按照处理时间(when)排序,所以每次获取的都是when最小的消息:
    至于Looper死循环为什么不会ANR?
消息队列中无消息怎么处理 block
nativePollOnce 值为-1 表示无限等待,让出cpu时间片给其它线程,本线程等待
0 表示无需等待直接返回
nativePollOnce -> epoll(linux) -> linux层的messagequeue
msg -> 5s  -> ANRmsg
ANR:
5秒内没有响应输入事件,比如按键、屏幕触摸
10秒内没有处理广播
本质:消息队列中其他消息耗时,按键或广播没有及时处理

根本原因不是线程在睡眠,而是消息队列被其他耗时消息堵塞,导致按键或广播消息没有及时处理。
Handler内存泄露的原因
MessageQueue持有Message,Message持有activity
delay多久,message就会持有activity多久
方法:静态内部类、弱引用

取到一个消息时,如果when还不到,则有限等待(nextPollTimeoutMills)nativePoll() 如果消息队列没有消息,则无限等待nativePoll(-1),而消息入队时,会执行nativeWake() ,quit也会nativeWake,唤醒Looper所在线程=> messagequeue返回null=> Looper退出

消息分发

  • 问:Message.callback与Handler.callback 哪个优先?
  • 问:Handler.callback和handlermessage()都存在,但callback返回true,handlerMessage()还会执行么?

获取需要执行的消息之后,将调用 msg.target.dispatchMessage(msg);处理消息。

public void dispatchMessage(Message msg){
  if(msg.callback !=null){//1.设置了Message.Callback(Runable)
  handleCallback(msg)
}else{
  if(mCallback !=null){//2.设置了 Handler.Callback(Callback)
 if(mCallback.handleMessage(msg)){
  return;
  }
}
//3.未设置Handler.Callback 或返回 false
handleMessage(msg);
}
}
public interface Callback{
  public boolean handleMessage(Message msg);
}

可以看到,除了在Handler.handleMessage(...)中处理消息外,Handler机制还提供了两个Callback来增加消息处理的灵活性。具体来说,若设置了Message.Callback则优先执行,否则判断Handler.Callback的返回结果,如果返回false,则最后分发到Handler.handleMessage(...)

终止消息循环

quit:
mQuitting =true;
removeAllMessage()
nativeWake()唤醒,程序从nativePollOnce(-1)开始执行

主程序Looper不允许退出quit() 抛异常 mQuitAllowed =false
ActivityThread.main Looper.loop() 之后抛异常
原因:是handler驱动的机制,所有的事件都需要Handler处理,例如LAUNCH_ACTIVITY等

Handler同步屏障机制

同步屏障(SyncBarrier)是Handler用来筛选高低优先级消息的机制,即:当开启同步屏障时,高优先级的异步消息优先处理。

同步屏障可以通过MessageQueue.postSyncBarrier函数来设置。该方法发送了一个没有target的Message到Queue中,在next方法中获取消息时,如果发现没有target的message,则在一定的时间内跳过同步消息,优先执行异步消息。换句话说,同步屏障为Handler消息机制增加了一种简单的优先级机制,异步消息的优先级要高于同步消息。在创建Handler时有一个async参数,传true表示此Handler发送的是异步消息。ViewRootImpl.scheduleTraversals方法就是使用了同步屏障,保证UI绘制优先执行。

IdleHandler 机制

问:IdleHandler是什么?怎么使用,能解决什么问题?

闲时机制,IdleHandler是一个回调接口,可以通过MessageQueue的addIdleHandler添加实现类。当MessageQueue中的任务暂时处理完了(没有新任务或者下一个任务延时在之后),这个时候会回调这个接口,
返回false,那么就会移除它,
返回true就会在下次message处理完了的时候继续回调。
相关链接 :
https://www.jianshu.com/p/1dc73c8ab6a1
https://mp.weixin.qq.com/s/mR7XIVbaKsB4q-Rxe1ip2g

Handler应用场景

  • HandlerThread
    Handler都是早Looper所在线程创建的,但是有时候我们在其他线程中创建Looper所在现场的Handler,就需要考虑同步问题,使用HandlerThread可以简化这种同步处理:
    既然设计多个线程的通信,会有同步的问题,Android为了简化Handler的创建过程,提供了HandlerThread类

Looper.loop()为什么不会阻塞主线程

Android 是基于事件驱动的,即所有Activity的生命周期都是通过Handler事件驱动的。looper方法中会调用MessageQueue的next方法获取下一个message,当没有消息时,基于Linux pipe/epoll 机制会阻塞在loop的queue.next()中的nvtivePollOnce()方法里,并不会消耗CPU
事件只会阻塞Looper,而Looper不会阻塞事件。

相关文章

网友评论

    本文标题:【学习】面试必问的Handler

    本文链接:https://www.haomeiwen.com/subject/lpkbjltx.html