Handler 原理

Handler 原理

  1. Handler 被设计出来的原因?有什么用?

    提供一种方便线程间通信的解决方案

  2. Handler 的基本原理

    外部通过 Handler 往 MessageQueue 里插入任务
    Looper 在做死循环,一直从 MessageQueue 中获取任务
    如果此时任务为空或当前没有需要执行的任务,则先判断 IdleHandler,IdleHandler为空则阻塞

  3. 子线程中怎么使用 Handler,为什么我们在主线程不用做准备操作

    Looper.prepare() 创建 Looper 并添加到 ThreadLocal 中
    Looper.loop() 开始死循环从 MessageQueue 获取消息
    程序起来时在 ActivityThread.main() 方法以经初始化过,所以直接使用就行

  4. 为什么建议使用 Message.obtain() 来创建 Message 实例

    对 Message 有效复用,可以有效的解决频繁创建 Message 实例问题

  5. MessageQueue 获取消息是怎么等待

    通过 epoll 进行等待和唤醒
    在 next 方法汇中,如果当前没有需要执行的任务,则调用 nativePollOnce 进行阻塞

    nextPollTimeoutMillis 三种情况:

    • 等于0,不阻塞立即返回

    • 大于0,阻塞等待的时间

    • 等于-1,无消息时会一直阻塞

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

    • 当消息队列为空,管道的读端等待管道中有新内容可读,就会通过 epoll 机制进入阻塞状态
    • 当有消息要处理,就会通过管道的写端写入内容,唤醒主线程
  6. 为什么不用 wait 而用 epoll

    在 Android 2.2 及以前,使用的就是 wait/notify 进行等待,之后为了同时处理 native 侧的消息,改用了 pipe/epoll 机制

    关于 select 、poll 、epoll,参考:https://mp.weixin.qq.com/s/YdIdoZ_yusVWza1PU7lWaw

    【备注】在兼容 native 侧消息时,早期使用的是 select,后面才改成 epoll,参考commit:https://android.googlesource.com/platform/frameworks/base/+/46b9ac0ae2162309774a7478cd9d4e578747bfc2%5E%21/#F16

  7. 线程和 Handler Looper MessageQueue 的关系

    一个线程对应一个 MessageQueue 、一个 Looper 、多个 Handler

  8. 多个线程给 MessageQueue 发消息,如何保证线程安全

    在 enqueueMessage 里进行了加锁

  9. Handler 消息延迟是怎么处理的

    在插入任务时会将时间转换成距离开机时间的毫秒数,然后在 MessageQueue 中按时间顺序插入

    在调用 MessageQueue.next() 中获取任务,如果还没有到达执行时间,先处理 IdleHandler,处理完后再 nativePollOnce 阻塞

  10. View.post 和 Handler.post 的区别

    View.post 时会判断有没有 AttachInfo,如果有则直接调用里面的 Handler 处理
    如果没有则等待 dispatchAttachedToWindow,然后在通过 Handler 进行处理
    dispatchAttachedToWindow 函数是在 ViewRootImpl 的 performTraversals 中调用,
    这也是为什么 View.Post 能够获取到 View 的宽高的原因,因为已经执行过 performTraversals 了

  11. Handler 导致的内存泄漏

    匿名内部类持有外部类,Handler有延迟消息造成无法回收,这样就造成了外部类无法回收
    方法一:静态内部类 + 弱引用
    方法二:将该Handler的消息全部移除

  12. 非 UI 线程真的不能操作 View 吗

    大部分情况是不可以的,因为在 ViewRootImpl 中的 requestLayout 会判断线程是否为创建线程,该创建线程默认就是 UI 线程
    其它情况如:ViewRootImpl 未创建修改View,子线程创建 ViewRootImpl,或者修改 View 不会触发 requestLayout 的场景

  13. Looper 在主线程死循环,为什么不会 ANR

    ANR 的原因是有任务在进行耗时操作,让本该执行的任务无法在合适的时间内执行
    在于任务本身,而非死循环

  14. 同步屏障和异步消息是怎么实现的

    • 同步消息:就是普通消息
    • 异步消息:通过 setAsynchronous(true) 设置的消息
    • 同步屏障消息:通过 postSyncBarrier 方法添加的消息,特点是 target 为空,即没有对应的 handler

    三者关系

    • 正常情况下,同步消息和异步消息都是根据 when 来取消息,处理消息
    • 当遇到同步屏障消息时,就开始从消息队列里找异步消息,找到了再根据时间决定阻塞还是返回消息

    同步屏障和异步消息存在的意义就是用于处理 “加急消息”

    在 ViewRootImpl.scheduleTraversals() 中有具体应用

  15. Looper 安全退出和非安全退出有什么区别

    • 非安全退出:removeAllMessagesLocked,会直接清空所有未执行的消息
    • 安全退出:removeAllFutureMessagesLocked,只清空延迟消息,非延迟消息继续处理
  16. Message 如何实现插队

    • sendMessageAtTime(msg, 0) ,时间传0即可
    • 如果不一定要插入队头,可以通过异步消息,也能实现一定的插队效果 msg.setAsynchronous(true)
  17. 利用 HanIder 机制设计一个不崩溃的 App

    • 第一步:设置自己的异常捕捉处理者

      1
      Thread.setDefaultUncaughtExceptionHandler()

      此时子线程出现异常已经不会崩溃了

      但是 UI 线程触发异常会让 Looper.loop 死循环退出,接下来的消息无法处理同样会让app卡死

    • 第二步:对主线程异常进行捕获

      1
      2
      3
      4
      5
      6
      7
      8
      Handler(Looper.getMainLooper()).post {
      while (true) {
      try {
      Looper.loop()
      } catch (e: Throwable) {
      }
      }
      }

      这样,UI 线程触发异常会被捕捉并重新 Looper.loop(),这样后续的消息又能继续处理,从而解决app崩溃异常

    • 【注意:】经过上面两步确认解决了app崩溃的问题,但是如果在Activity的生命周期内触发异常,如 onCreate 中,则会出现黑屏白屏的情况,原因是生命周期内抛出异常,会导致界面无法完成, Activity 无法正确启动,就会出现黑屏白屏的情况。由于 Activity 的生命周期都是通过主线程的 Handler 进行处理的,可以通过反射 ActivityThread 中的 Handler 进行 Activity 生命周期异常捕获,然后进行 finishActivity 或者杀死进程的操作

    • 【说明】第二步中可以理解为外部 loop 一直在等待 post 里的 loop 结束

    • 【参考一:】https://juejin.cn/post/6904283635856179214

    • 【参考二:】https://github.com/android-notes/Cockroach/blob/master/%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90.md

    • 【相关库:】https://github.com/android-notes/Cockroach