Handler 原理

Handler 原理

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

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

  2. Handler 的基本原理

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

  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.postHandler.post 的区别

    View.post 时会判断有没有 AttachInfo,如果有则直接调用里面的 Handler 处理
    如果没有则等待 dispatchAttachedToWindow,然后在通过 Handler 进行处理
    dispatchAttachedToWindow 函数是在 ViewRootImplperformTraversals 中调用,
    这也是为什么 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