Android-Framework-03-Handler-问题

173 阅读28分钟

01:Handler 机制中,存在哪些角色?各自承担了什么功能?

*   1. Handler:消息辅助类 & 对外的接口 & 向 MQ 投递消息 & 消息的目标处理者;

*   2. Message:消息的载体 & 被 Handler 投递 & 自带 Handler 处理 & 自带消息池;

*   3. Looper:循环器 & 持有 MQ & 循环从 MQ 中获取消息 & TLS 线程唯一;

*   4. MessageQueue:基于时间的优先级队列 & 链表结构 & Java 与 C++ 层的纽带;

02:Handler 机制中,Message 和 Runnable 的区别?

*   1. 本质上无区别,MQ 只接受 Message,Runnable 会被转换成 Message 入队;

*   2. Runnable 通过 getPostMessage() 方法转换成 Message 对象;

*   3. Runnable 转换的 Message,Runnable 会被记录在 MSG 的 callback 属性上,在处理消息时,优先被处理;

03:Handler 分发事件优先级,是否可拦截?拦截的优先级如何?

Handler 中,通过 dispatchMessage() 处理消息,其中存在优先级策略;

*   1. 优先级1:msg,callback,run() - 独占;

*   2. 优先级2:mCallback.handleMessage(msg) - 返回值决定是否拦截该消息;
    mCallback 属于 Handler,是消息的统一拦截机制。
*   3. 优先级3:handle.handleMessage();

04:Handler 在处理 Message 的时候,可以对消息统一拦截吗?如何做?有什么缺点?

*   1. 可以;

*   2. 实现:使用 Handler 的 Callback,通过 Handler 的构造方法传递 Callback 对象,并实现其 handlerMessage() 方法用于 Message 的统一处理,返回值表示是否拦截;

*   3. 缺点:无法拦截 Runnable 消息;

05:Handler 发送延迟消息,涉及哪些方法?原理是什么?会被开机时间影响吗?

*   1. 涉及方法:xxxDelayed() 和 xxxAtTime(),最终都会调用到 enqueueMessage() 入队;

*   2. 原理:延迟时间记录在 msg.when 中,参与 MessageQueue 的优先级排序,即 MQ.enqueueMessage() 插入消息时,会基于 when 计算消息在 MessageQueue 中的位置并插入;

*   3. 不受开机时间影响,因其延迟消息基于 SystemClock,此为设备开机时间,不受时钟影响;

06 :主线程 Looper 和子线程 Looper 有什么区别?

*   1. prepare 不同

    *   主线程:prepareMainLooper()

    *   子线程:prepare()

*   2. 是否允许退出

    *   主线程:不允许

    *   子线程:允许

*   3. 构造方不同

    *   主线程:由 ActivityThread 启动时,main() 方法中构造,开发者无需关心;

    *   子线程:开发者执行构造,prepare() → loop()

07:Looper 如何保证线程唯一?线程内多次 prepare() 不同的 Looper 会怎样?

*   1. 原理:基于 TLS 机制,Looper 首次 prepare() 时,会将 Looper 存入 ThreadLocal 中,再次 prepare() 时检查 ThreadLocal 是否已存储;

*   2. 多次调用 Looper prepare() 会抛出异常;

08:Looper 如何判断是否允许退出?如何设置 Looper 是否允许退出?主线程 Looper 允许退出吗?

*   1. 判断:Looper 持有的 MessageQueue 的 mQuitAllowed 状态,标识了是否允许退出;

*   2. 设置:通过 Looper.prepare(quitAllowed) 的入参 quitAllowed 判断是否允许退出;
    此方法为私有,外部无法直接调用;
*   3. 规律:主线程 Looper 不允许退出,子线程 Looper 允许退出;

    *   1. 主线程:prepareMainLooper() → prepare(false) - 不允许退出;

    *   2. 其他线程:prepare() → prepare(true) - 允许退出;

09:Looper 的退出,涉及哪些方法,原理是什么?有什么限制?

*   1. Api:直接退出 quit() & 安全退出 quitSafely();

*   2. 原理:都会调用 MessageQueue 的 quit() 方法,清理 MessageQueue 持有的消息,并标记 MQ 退出,驱动其 next() 方法返回 null,初始 Looper 退出 loop() 循环,进而使得 Looper 退出;

    *   quit:非安全退出,直接移除所有消息;

    *   quitSafely:安全退出,仅移除尚未触发的消息;

*   3. 限制:只有子线程 Looper 才允许退出,主线程 Looper 尝试退出会抛出异常;

*   4. Tips:MQ 退出 → `MQ.next`返回 null → Looper.loop() 收到 null → 跳出while 循环 → Looper 退出;

10:Looper.loop() 方法中的循环内,调用 MessageQueue 的 next(),若 next() 返回 null 则直接退出循环,合理吗?

*   1. 合理;

*   2. 正常情况,MQ,next() 必然会返回待处理的消息,没有则会通过 nativePollOnce() 休眠;

*   3. 若 next() 返回 null,则说明出现异常

    *   mPtr = 0,消息队列已被清理并退出;

    *   mQuitting = true,消息队列已经被标记为退出;

11:如何构造子线程 Looper?涉及哪些方法?

*   1. 启动新线程;

*   2. 调用 Looper.prepare() - 准备;

*   3. 调用 Looper.loop() - 进入循环;

12:主线程 Looper 何时运行?

*   1. App 启动时,会调用到 ActivityThread 中,Looper 就在其 main() 方法中被启动;

*   2. main() 中会主动调用 Looper.prepareMainLooper() 和 Looper.loop();

*   3. Tips:ActivityThread 不继承自 Thread,它只是一个运行在主线程上的对象;

13:Message 消息池的结构?最大缓存多少个 Message 对象?

*   1. Message 类的静态属性 sPool 维护;

*   2. 消息池基于链表结构,以 msg.next 串联;

*   3. sPoolSize 维护消息池大小,最大 50;

Q:Message 消息池,有什么优点?

*   1. 享元模式,避免重复构造 Message;

*   2. 回收资源,回收时清理 msg 持有的 callback 和 target,避免内存泄露;

Handler 的消息机制中,Message 对象通过 MessageQueue 传递给 Handler 进行处理。Android 通过对 Message 的池化来优化消息处理流程,具体实现如下:

  1. Message 对象的池化实现

    • Message 类本身维护了一个静态的池(通常是 sPool 或类似名称的对象),用来存储复用的 Message 对象。
    • 当创建新的 Message 时,如果池中有空闲的对象,就直接复用池中的对象,而不是创建新对象。
    • Message 对象不再使用时,会被放回池中,等待下一次复用。
  2. 消息的复用

    • Message.obtain() 方法从消息池中获取一个 Message 对象。如果池中有可用对象,就从池中取出并返回;否则,就会创建一个新的 Message 对象。
    • Message 对象处理完毕后,Message.recycle() 方法会将其放回池中,供后续使用。

14:Handler 的 Message 可以分为那 3 类?分别有什么标识?

*   1. 同步 Message:普通 Message;

*   2. 异步 Message:msg.setAsynchronous(true)

*   3. 同步屏障:msg.target == null

15:同一个 Message 对象能否重复 send?

*   1. 关键在于如何定义同一个 Message。

*   2. 角度一:Java 对象层面,可被复用;

    *   原因:Message 由消息池维护,即同一个对象被回收后会被再次复用;
        new Message & Message.obtain()
*   3. 角度二:业务层面,不能复用;

    *   原因:Message 通过 enqueueMessage() 入队时,会通过 markInUse() 标记,再次入队无法通过 isInUse() 检查,则抛出异常;

16:场景:MessageQueue 是基于触发时间 when 的优先级队列,那么什么情况下,队列中靠后的消息会优先得到执行?原理是什么?

*   1. 场景:靠前的消息是同步消息,靠后的消息是异步消息,且消息队列的队头为同步屏障;

*   2. 原理:同步屏障会阻塞 MQ 中的同步消息,优先处理异步消息;

17:Message 的同步屏障有什么用?有什么意义?如何发送一个同步屏障?

*   1. 用途:阻塞 MQ 对同步 Message 的分发,优先处理异步消息,没有异步消息时则进入休眠,直到同步屏障被移除;

*   2. 意义:允许异步消息优先于同步消息执行;

*   3. 同步屏障:特殊的 Message,target == null,无法通过 Handler 入队出队,需直接操作 MQ;

    *   入队:postSyncBarrier():返回一个屏障 token;

    *   出队:removeSyncBarrier()

18:什么是异步消息?如何发送?

*   1. 意义:需配合同步屏障使用,否者与同步消息无区别;

*   2. 异步消息:setAsynchronous(true) → 向 flags 添加 FLAG_ASYNCHRONOUS 标记

*   3. 发送方式

    *   通过异步 Handler 发送 → 构造 Handler 时,async 传递 true

    *   发送消息前,主动调用 setAsynchronous(true)

*   4. 安全起见,Android 9.0 普通开发者无法使用异步消息,所有发送方式被标记为 @hide

19:同步屏障如何配合异步消息工作?

*   1. 当 mMessage 是一个同步屏障时,会跳过所有同步消息,找到异步消息提前执行;

*   2. 若 MQ 队列中没有异步消息,会进入 nativePollOnce(mPtr, -1) 无限等待,直到同步屏障被移除,或新的异步消息入队,才会通过 nativeWake() 唤醒 MQ;

20:Handler 的 IdleHandler 机制,如何理解?有什么用途?

*   1. 接口,需实现 queueIdle() 方法 & 定义在 MQ 中 & 以 MQ mIdleHandlers 维护存储

*   2. 用途:可在 MQ 即将空闲时,处理任务;

*   3. 逻辑点:MQ.next() 中,当前无待执行消息时,执行 mIdleHandlers;

*   4. 依据 queueIdle() 返回值分:持续回调(true) & 一次性回调(false),false 会导致执行完后,从 mIdleHandlers 中移除

21:IdleHandler 的 queueIdle() 返回 true,为什么不会死循环?

*   1. pendingIdleHandlerCount 标记控制,MQ.next() 时,初始为 -1;

*   2. -1 才会执行 mIdleHandlers,执行后置为 0 → 不会重复执行;

*   3. 每次 Looper.loop() 从 MQ.next() 取出消息后,在 Loop 循环中继续调用 MQ.next() 才会重置 pendingIdleHandlerCount,才会继续执行
  • Q:IdleHandler 执行耗时会影响正常的消息分发吗?Handler 内部如何处理?

      1. 会;
      1. IdleHandler 的耗时不可控;
      1. 执行完后会重置 nextPollTimeoutMillis = 0,重新分发最近消息

22:Handler 在 Activity 中使用,什么场景下会出现内存泄露?原因是什么?如何规避?

*   1. 现场:延迟消息 + 内部类 Handler;

*   2. 原因:主线程生命周期长于四大组件,msg,target 指向 Handler,而 Handler 作为内部类持有外部类 Activity 的引用,导致 Activity 泄露;

*   3. 解

    *   1, 静态 Handler + Activity 弱引用;

    *   2. 随 Activity 生命周期,onDestory() 会 remove 掉所有的消息;

23:移除消息的 removeMessage() 为什么需要两次循环?

*   1. 优化效率;

*   2. while-1:移除消息 & 找到下一个待处理的消息,存入 mMessages 中;

*   3. while-2:从 mMessages 开始,移除后续符合条件的消息;

24:如何理解 HandlerThread?

*   1. 继承 Thread,内部持有 Handler,并自维护子线程的 Looper;

*   2. 意义:将 Thread、Handler、Looper 封装,便于开发者使用;

25:如何实现,子线程等待主线程处理消息结束后,再继续执行?原理是什么?

*   1. 借助 Handler 的 runWithScissors()

*   2. 原理:内部通过 BlockingRunnbale 包装,通过 synchronized + wait 进入等待,在 r 执行完成后,调用 notifyAll() 唤醒等待队列,子线程收到后继续执行;

26:Handler 的 runWithScissors() 可实现 A 线程阻塞等待 B 线程处理完消息后再继续执行的功能,它为什么被标记为 hide?存在什么问题?原因是什么?

*   1. 实现:将 Runnable 包装为 BlockingRunnable,其内通过 synchronized + wait 进入等待,待 r 执行完后,调用 notifyAll() 唤醒等待队列的子线程,子线程继续执行;

*   2. 问题:在子线程 Looper 中使用,可能导致 A 线程进入 wait 等待,而永远得不到被 notify 唤醒;

*   3. 原因:子线程 Looper 允许退出,若包装的 BlockingRunnable 被执行前,MessageQueue 退出,则该 runnable 永远不会被执行,则会导致 A 线程一直处于 wait 等待,永远不会被 notify 唤醒;

27:子线程如何向主线程的 Handler 发送消息?为什么经过 Handler 就可以达到切线程的目的?

*   1. 在 Android 中,主线程主要承担更新 UI 的工作,耗时操作无法在主线程完成;

*   2. 工作线程可以通过向主线程的 Handler 发生消息,来达到与主线程通信的目的;

*   3. 主线程与工作线程之间,是共享内存地址空间的,所以是可以互相操作的,但是需要注意处理线程同步的问题;

*   4. 工作线程通过主线的 Handler,向其成员变量 MessageQueue 中添加 Message。同时主线程的 Looper 一直处于 loop() 状态,当检测到有新 Message 时,会将其取出,并通过 dispatchHandler() 分发处理消息;

28:Looper.loop 中,如果没有待处理的消息,为什么不会阻塞 UI?

*   1. 主线程在 MessageQueue 没有消息时,会阻塞在 loop 的 queue.next() 方法中的 nativePollOnce()方法里。

*   2. 此时主线程会释放 CPU 资源进入休眠状态,直到下一个消息到达或者有事务发生,通过往 pipe 管道写端写入数据的方式,来唤醒主线程。这里采用的是 epoll 机制。

*   3. epoll 机制是一种 IO 多路复用机制,可以同时监控多个描述符,在有事件发生的时候,立即通知相应程序进行读或写操作,类似一种 callback 的回调机制。

*   4. 主线程在大多数时候是处于休眠状态,并不会消耗大量的 CPU 资源。当有新的消息或事务到达时,会立即唤醒主线程进行处理,所以对用户来说是无感知的。

29:Handle MQ 无消息时,为什么不出现 ANR?

*   1. ANR 机制有独特的覆盖场景,通常原因为处理消息不及时;

*   2. MQ 无消息时,会进入 nativePollOnce() 休眠,此时无消息,处于休眠状态;

*   3. MQ 有消息时,会立即通过 nativeWake() 唤醒去处理消息;

30:如果 Java 层 MQ 中消息很少,但是响应时间却很长,是什么原因?

*   1. MQ 队列中,该 Message 前的 Message 处理较为耗时;

*   2. Native 层消息过多,Java 层 MQ 消息优先级最低,最后处理;

31:在完整的 Handler (Java & C++)架构下,消息处理的优先级?

*   1. Native Message

*   2. Native Request

*   3. Java Message

32:Native 层的 MQ,提供了哪些 JNI 方法?各有什么用途?

*   nativeInit():初始化

    *   1. 创建了 NativeMessageQueue 对象,增加其引用计数,并将 NativeMessageQueue 指针 mPtr 保存在 Java 层的 MessageQueue。

    *   2. 创建 Native Looper 对象。

    *   3. 调用 epoll 的 epoll_create()/ epoll_ctl() 来完成对 mWakeEventFd 和 mRequests 的可读事件监听。

*   nativeDestory():回收资源

    *   1. 调用 RefBase::decStrong() 来减少对象的引用计数。

    *   2. 当引用计数为 0 时,则删除 NativeMessageQueue 对象。

*   nativePollOnce():利用 epoll 机制休眠,等待被唤醒

    *   调用 Looper::pollOnce() 来完成,空闲时停留在 epoll_wait() 方法,用于等待事件发生或超时

*   nativeWake():向管道 mWakeEventFd 写入字符 1,唤醒 epoll_wait() 等待

33:如何理解 Looper 的 Painter?

*   1. 在 Looper.loop() 循环消息的时候,如果 mLogging 不为 null,都会在关键点通过 Printer 输出 Log。例如 Message 开始执行和执行完毕。

*   2. Printer 对象在 Looper 中默认为 null,可以通过 setMessageLogging() 方法从外部设置。

*   3. 性能检测工具 BlockCanary 就是利用 Printer 来检测主线程卡顿的问题。通过处理 Message 两次 Log 的时间差值,来判断是否存在卡顿。

34:Looper 的 Printer 输出的日志,有什么其他用途?依靠的原理是什么?有什么缺点?

*   1. 用途:性能监控;

*   2. 原理:通过筛选日志内存,区分 Message 的开始执行和结束执行的时间电,即可判断处理 Message 的耗时,即主线程卡顿耗时;

*   3. 缺点:Printer 存在大量字符串拼接,在消息量大时,会导致性能受损;
    实测数据:存在 Printer 时,在列表快速滑动时,平均帧率降低 5 帧;

35:如何理解 epoll?它有什么优势?

*   1. Linux 的 I/O 多路复用机制,可以同时监控多个描述符;

*   2. 多路复用机制下,可以通知内核挂起进程/线程,当 1 个 或多个 IO 事件发生后,内核再唤醒进程,将控制权返还,由应用程序自己处理事件;

*   3. epoll 优势:

    *   监控描述符数量不受限;
        监控 fd 数量何内存相关,3G 内存 → 20w~30w
    *   IO 不会随监控 fd 的增多而放大 → 得益于事件表维护 fd 与其回调的关系;

*   4. epoll 适用:

    *   适用大量空闲连接的场景

    *   没有大量空闲或死亡连接时,epoll 的效率不会比 select/poll 高 → 量小时 O(logn) 和 O(n) 差别不大;

*   5. 操作

    *   epoll_create - 创建 epoll 句柄;

    *   epoll_ctl() - 设置监听的 fd;

    *   epoll_wait() - 等待 fd 事件;

36:Handler 和管道(Pipe)的关系?

*   1. Handler 底层的休眠机制,就是利用管道 和 epoll 的 I/O 多路复用

*   2. 线程进入休眠,通过 nativePollOnce() 底层调用 epoll_wait() 进入休眠

*   3. A 线程准备好 Message 后,放入消息池,向管道写入数据 “1”,管道有数据后,会唤醒目标线程去处理消息

*   4. Tips:Android M 开始,将 Pipe 换成了 eventFd;

37:Handler 底层为什么使用管道,而不是 Binder?

*   1. 相对于同进程内的线程通信,Binder 太重了,浪费 CPU 和内存资源

*   2. 内存角度:Binder 通信涉及一次内存拷贝,而多个线程操作的 Handler 是在共享的内存中,无需拷贝,只需要通知线程有数据了

*   3. CPU 角度:Binder 底层驱动会维护线程池,比较浪费 CPU 资源

*   4. Tips:Android M 开始,将 Pipe 换成了 eventFd;

38:Handler 可以 IPC 通信吗?

*   不能;

*   Handler 只能用于共享内存地址的 2 个线程通信,即同进程的 2 个线程通信;

39:Handler 为什么需要使用底层的 epoll 来休眠?

*   1. 需要兼顾 Native 层的消息,消息可能来自底层硬件;

*   2. 如果只考虑 Java 层,notify/wait 即可实现;

40. Handler 的基本原理

关于 Handler 的原理,相比不用多说了,大家都应该知道,一张图就可以说明(图片来自网络)。

图片

41. 子线程中怎么使用 Handler

除了上面 Handler 的基本原理,子线程中如何使用 Handler 也是一个常见的问题。子线程中使用 Handler 需要先执行两个操作:Looper.prepare 和 Looper.loop。为什么需要这样做呢?Looper.prepare 和 Looper.loop 都做了什么事情呢? 我们知道如果在子线程中直接创建一个 Handler 的话,会报如下的错误:

"Can't create handler inside thread xxx that has not called Looper.prepare()

我们可以看一下 Handler 的构造函数,里面会对 Looper 进行判断,如果通过 ThreadLocal 获取的 Looper 为空,则报上面的错误。

public Handler(Callback callback, boolean async) {    mLooper = Looper.myLooper();    if (mLooper == null) {        throw new RuntimeException(            "Can't create handler inside thread " + Thread.currentThread()                    + " that has not called Looper.prepare()");    }}public static @Nullable Looper myLooper() {    return sThreadLocal.get();}

那么 Looper.prepare 里做了什么事情呢?

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));}

可以看到,Looper.prepare 就是创建了 Looper 并设置给 ThreadLocal,这里的一个细节是每个 Thread 只能有一个 Looper,否则也会抛出异常。而 Looper.loop 就是开始读取 MessageQueue 中的消息,进行执行了。这里一般会引申一个问题,就是主线程中为什么不用手动调用这两个方法呢?相信大家也都明白,就是 ActivityThread.main 中已经进行了调用。通过这个问题,又可以引申到 ActivityThread 相关的知识,这里就不细说了。

1. 子线程中的 Looper 和消息队列

在子线程中,LooperMessageQueue 结合使用来实现消息循环。每个 Looper 都与一个消息队列(MessageQueue)绑定,消息会被发送到消息队列,然后由 Looper 进行循环调度,逐一处理消息。

2. 消息队列无消息时的处理方案

当子线程的消息队列没有待处理的消息时,Looper 会进入 阻塞状态,也就是说,如果没有新消息或事件需要处理,线程会被暂停,直到有新的消息或事件到来。

阻塞机制
  • Looper.loop() 是消息循环的核心方法。在调用 Looper.loop() 后,线程会一直循环处理消息,直到队列中没有消息时进入阻塞状态。

  • 具体来说,当消息队列为空时,Looper 会通过 nativePollOnce 方法调用底层的 I/O 多路复用机制(例如 epoll)来等待消息或事件的到来。

    • nativePollOnce 会使线程进入阻塞状态,并持续监听消息队列,直到有消息到达或超时,线程才会继续执行。
    • nativePollOnce 会通过底层的操作系统提供的机制(如 epoll)等待事件的到来,从而避免占用过多的 CPU 资源。
空闲回调 (IdleHandler)
  • 当消息队列空闲时,如果有注册的 IdleHandler,则会调用这些回调函数。开发者可以使用空闲回调执行一些低优先级任务或延迟的操作。

  • 空闲回调通常用于后台清理任务、缓存清理、定时任务等低优先级的操作。

    • 示例

      java
      复制代码
      MessageQueue queue = Looper.myQueue();
      queue.addIdleHandler(new MessageQueue.IdleHandler() {
          @Override
          public boolean queueIdle() {
              // 执行低优先级任务
              Log.d("IdleHandler", "Queue is idle, doing low-priority task.");
              return true; // 如果返回 true,继续保持空闲回调
          }
      });
      
空闲时的处理方案
  • 如果消息队列没有消息,线程将处于 等待状态,直到消息到达或者其他事件(如超时)唤醒线程。
  • 开发者可以使用空闲回调(IdleHandler)来执行一些不紧急的任务。
  • 阻塞机制和空闲回调使得线程能够高效地管理资源,避免无谓的 CPU 资源消耗。

3. 这种机制的用处

  1. 节省 CPU 资源

    • 通过阻塞,线程不会持续忙轮询,避免了不必要的 CPU 占用,提升了系统的整体性能。
    • 这种机制特别适合长期运行的线程,它们不需要频繁地执行任务,而是等到有新任务时才启动工作。
  2. 提高响应性

    • 在消息队列空闲时,线程进入阻塞,只有当新消息到达时才被唤醒。这提高了系统的响应性,避免了线程一直处于活跃状态,但没有任务处理的低效情况。
  3. 优雅的资源管理

    • Looper 和消息队列的设计允许后台线程高效地等待并响应事件,适合长时间运行的服务、后台任务、定时任务等场景。
    • 通过 IdleHandler,开发者可以在空闲时进行低优先级的后台操作(如缓存清理、统计等),使得应用的资源管理更加灵活。
  4. 长期任务管理

    • 子线程可以通过消息队列管理长期任务,例如定时任务、周期性任务等,而不需要频繁地创建和销毁线程。消息队列为空时,线程会进入阻塞状态,避免浪费资源。
  5. 延迟任务

    • 通过 Handler.postDelayed() 可以安排任务的延迟执行,而消息队列会在等待任务时保持阻塞,只有在任务到达时才开始执行。

42. MessageQueue 如何等待消息

上面说到 Looper.loop 其实就是开始读取 MessageQueue 中的消息了,那 MessageQueue 中没有消息的时候,Looper 在做什么呢?我们知道是在等待消息,那是怎么等待的呢?通过 Looper.loop 方法,我们知道是 MessageQueue.next() 来获取消息的,如果没有消息,那就会阻塞在这里,MessageQueue.next 是怎么等待的呢?

public static void loop() {    final MessageQueue queue = me.mQueue;    for (;;) {        Message msg = queue.next(); // might block        if (msg == null) {            // No message indicates that the message queue is quitting.            return;        }    }}
Message next() {    for (;;) {        nativePollOnce(ptr, nextPollTimeoutMillis);        // ...    }}

在 MessageQueue.next 里调用了 native 方法 nativePollOnce。

// android_os_MessageQueue.cppstatic void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,        jlong ptr, jint timeoutMillis) {    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);    nativeMessageQueue->pollOnce(env, obj, timeoutMillis);}void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {    // ...    mLooper->pollOnce(timeoutMillis);    // ...}// Looper.cppint Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {    // ...    result = pollInner(timeoutMillis);    // ...}int Looper::pollInner(int timeoutMillis) {    // ...    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);}

从上面代码中我们可以看到,在 native 侧,最终是使用了 epoll_wait 来进行等待的。这里的 epoll_wait 是 Linux 中 epoll 机制中的一环,关于 epoll 机制这里就不进行过多介绍了,大家有兴趣可以参考 segmentfault.com/a/119000000…    那其实说到这里,又有一个问题,为什么不用 java 中的 wait / notify 而是要用 native 的 epoll 机制呢?

43. 为什么不用 wait 而用 epoll 呢?

**说起来 java 中的 wait / notify 也能实现阻塞等待消息的功能,在 Android 2.2 及以前,也确实是这样做的。可以参考这个 commit www.androidos.net.cn/android/2.1…   那为什么后面要改成使用 epoll 呢?通过看 commit 记录,是需要处理 native 侧的事件,所以只使用 java 的 wait / notify 就不够用了。**具体的改动就是这个 commit android.googlesource.com/platform/fr…

Sketch of Native input for MessageQueue / Looper / ViewRootMessageQueue now uses a socket for internal signalling, and is preparedto also handle any number of event input pipes, once the plumbing isset up with ViewRoot / Looper to tell it about them as appropriate.Change-Id: If9eda174a6c26887dc51b12b14b390e724e73ab3

不过这里最开始使用的还是 select,后面才改成 epoll。具体可见这个 commit android.googlesource.com/platform/fr…           至于 select 和 epoll 的区别,这里也不细说了,大家可以在上面的参考文章中一起看看。

44. 线程和 Handler Looper MessageQueue 的关系

这里的关系是一个线程对应一个 Looper 对应一个 MessageQueue 对应多个 Handler。

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

既然一个线程对应一个 MessageQueue,那多个线程给 MessageQueue 发消息时是如何保证线程安全的呢? 说来简单,就是加了个锁而已。

// MessageQueue.javaboolean enqueueMessage(Message msg, long when) {    

    synchronized (this) {        // ...    }

}

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

Handler 引申的另一个问题就是延迟消息在 Handler 中是怎么处理的?定时器还是其他方法?这里我们先从事件发起开始看起:

// 
Handler.javapublic final boolean postDelayed(Runnable r, long delayMillis){    
    return sendMessageDelayed(getPostMessage(r), delayMillis);
}
public final boolean sendMessageDelayed(Message msg, long delayMillis)// 传入的 time 是 uptimeMillis + delayMillis    
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {    
    // ...    
    return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {    
    // 用 MessageQueue.enqueueMessage    
    return queue.enqueueMessage(msg, uptimeMillis);
}

从上面的代码逻辑来看,Handler post 消息以后,一直调用到 MessageQueue.enqueueMessage 里,其中最重要的一步操作就是传入的时间是 uptimeMillis + delayMillis。

boolean enqueueMessage(Message msg, long when) {    
    synchronized (this) {        
        // ...        
        msg.when = when;       
        Message p = mMessages
        // 下一条消息        
        // 根据 when 进行顺序排序,将消息插入到其中        
        if (p == null || when == 0 || when < p.when) {            
            msg.next = p;            
            mMessages = msg;            
            needWake = mBlocked;        
        } else {            
              Message prev;            
              for (;;) {                
                  prev = p;                
                  p = p.next;                
                  if (p == null ||when < p.when) {                    
                      break;                
                  }            
              }            
              // 插入msg.next = p;
              // invariant: p == prev.next            
              prev.next = msg;        
          }        
          
        // 唤醒队列进行取消息       
        if (needWake) {           
          nativeWake(mPtr);       
        }    
      }    
  return true;
}

通过上面代码我们看到,post 一个延迟消息时,在 MessageQueue 中会根据 when 的时长进行一个顺序排序。接着我们再看看怎么使用 when 的。

Message next() {    // ...    for (;;) {        // 通过 epoll_wait 等待消息,等待 nextPollTimeoutMillis 时长        nativePollOnce(ptr, nextPollTimeoutMillis);        synchronized (this) {            // 当前时间            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) { // 说明需要延迟执行,通过;nativePollOnce 的 timeout 来进行延迟                    // 获取需要等待执行的时间                    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;            }            if (pendingIdleHandlerCount < 0                    && (mMessages == null || now < mMessages.when)) {                        // 当前没有消息要执行,则执行 IdleHandler 中的内容                pendingIdleHandlerCount = mIdleHandlers.size();            }            if (pendingIdleHandlerCount <= 0) {                // 如果没有 IdleHandler 需要执行,则去等待 消息的执行                mBlocked = true;                continue;            }            if (mPendingIdleHandlers == null) {                mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];            }            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);        }        // 执行 idle handlers 内容        for (int i = 0; i < pendingIdleHandlerCount; i++) {            final IdleHandler idler = mPendingIdleHandlers[i];            mPendingIdleHandlers[i] = null; // release the reference to the handler            boolean keep = false;            try {                keep = idler.queueIdle();            } catch (Throwable t) {                Log.wtf(TAG, "IdleHandler threw exception", t);            }            if (!keep) {                synchronized (this) {                    mIdleHandlers.remove(idler);                }            }        }        // Reset the idle handler count to 0 so we do not run them again.        pendingIdleHandlerCount = 0;        // 如果执行了 idle handlers 的内容,现在消息可能已经到了执行时间,所以这个时候就不等待了,再去检查一下消息是否可以执行, nextPollTimeoutMillis 需要置为 0        nextPollTimeoutMillis = 0;    }}

通过上面的代码分析,我们知道了执行 Handler.postDelayd 时候,会执行下面几个步骤:

  1. 将我们传入的延迟时间转化成距离开机时间的毫秒数
  2. MessageQueue 中根据上一步转化的时间进行顺序排序
  3. 在 MessageQueue.next 获取消息时,对比当前时间(now)和第一步转化的时间(when),如果 now < when,则通过 epoll_wait 的 timeout 进行等待
  4. 如果该消息需要等待,会进行 idel handlers 的执行,执行完以后会再去检查此消息是否可以执行

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

我们最常用的 Handler 功能就是 Handler.post,除此之外,还有 View.post 也经常会用到,那么这两个有什么区别呢?我们先看下 View.post 的代码。

// 
View.javapublic boolean post(Runnable action) {    
    final AttachInfo attachInfo = mAttachInfo;    
    if (attachInfo != null) {        
    return attachInfo.mHandler.post(action);    
}    
// Postpone the runnable until we know on which thread it needs to run.    
// Assume that the runnable will be successfully placed after attach.    
    getRunQueue().post(action);    
    return true;
}

通过代码来看,如果 AttachInfo 不为空,则通过 handler 去执行,如果 handler 为空,则通过 RunQueue 去执行。那我们先看看这里的 AttachInfo 是什么。这个就需要追溯到 ViewRootImpl 的流程里了,我们先看下面这段代码。

// ViewRootImpl.javafinal ViewRootHandler mHandler = new ViewRootHandler();
public ViewRootImpl(Context context, Display display) {    
    // ...   
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,context);
}
private void performTraversals() {    
    final View host = mView;    
    // ...    
    if (mFirst) {        
        host.dispatchAttachedToWindow(mAttachInfo, 0);        
        mFirst = false;    
    }    
    // ...
}

代码写了一些关键部分,在 ViewRootImpl 构造函数里,创建了 mAttachInfo,然后在 performTraversals 里,如果 mFirst 为 true,则调用 host.dispatchAttachedToWindow。
这里的 host 就是 DecorView,如果有读者朋友对这里不太清楚,可以看看前面【面试官带你学安卓-从View的绘制流程】说起这篇文章复习一下。这里还有一个知识点就是 mAttachInfo 中的 mHandler 其实是 ViewRootImpl 内部的 ViewRootHandler。然后就调用到了 DecorView.dispatchAttachedToWindow,其实就是 ViewGroup 的 dispatchAttachedToWindow。一般 ViewGroup 中相关的方法,都是去依次调用 child 的对应方法。这个也不例外,依次调用子 View 的 dispatchAttachedToWindow,把 AttachInfo 传进去,在 子 View 中给 mAttachInfo 赋值。

// ViewGroupvoid dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;    
    super.dispatchAttachedToWindow(info, visibility);   
    mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;    
    final int count = mChildrenCount;    
    final View[] children = mChildren;    
    for (int i = 0; i < count; i++) {        
        final View child = children[i];        
        child.dispatchAttachedToWindow(info,combineVisibility(visibility, child.getVisibility()));    
    }    

    final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();    
    for (int i = 0; i < transientCount; ++i) {        
        View view = mTransientViews.get(i);       
        view.dispatchAttachedToWindow(info,combineVisibility(visibility, view.getVisibility()));    
    }
  }
    // Viewvoid dispatchAttachedToWindow(AttachInfo info, int visibility) {    
        mAttachInfo = info;    
    // ...
    //}

看到这里,大家可能忘记我们开始刚刚要做什么了。我们是在看 View.post 的流程,再回顾一下 View.post 的代码:

// View.javapublic boolean post(Runnable action) {    
    final AttachInfo attachInfo = mAttachInfo;    
    if (attachInfo != null) {        
        return attachInfo.mHandler.post(action);    
    }    
    getRunQueue().post(action);    
    return true;
}

现在我们知道 attachInfo 是什么了,是 ViewRootImpl 首次触发 performTraversals 传进来的,也就是触发 performTraversals 之后,View.post 都是通过 ViewRootImpl 内部的 Handler 进行处理的。
如果在 performTraversals 之前或者 mAttachInfo 置为空以后进行执行,则通过 RunQueue 进行处理。那我们再看看 getRunQueue().post(action); 做了些什么事情。这里的 RunQueue 其实是 HandlerActionQueue。HandlerActionQueue 的代码看一下。

public class HandlerActionQueue {    
    public void post(Runnable action) {        
    postDelayed(action, 0);    
}    

public void postDelayed(Runnable action, long delayMillis) {  

    final HandlerAction handlerAction = new HandlerAction(action, delayMillis);  
    synchronized (this) {            
        if (mActions == null) {                
            mActions = new HandlerAction[4];           
        }            
                mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);       
        mCount++;       
   }    
}    
public void executeActions(Handler handler) {        
    synchronized (this) {            
        final HandlerAction[] actions = mActions;            
            for (int i = 0, count = mCount; i < count; i++) {                
                final HandlerAction handlerAction = actions[i];                
                handler.postDelayed(handlerAction.action, handlerAction.delay);            
            }           
            mActions = null;           
            mCount = 0;        
        }    
    }
}

通过上面的代码我们可以看到,执行 getRunQueue().post(action); 其实是将代码添加到 mActions 进行保存,然后在 executeActions 的时候进行执行。executeActions 执行的时机只有一个,就是在 dispatchAttachedToWindow(AttachInfo info, int visibility) 里面调用的。

void dispatchAttachedToWindow(AttachInfo info, int visibility) {  
    mAttachInfo = info;    
    if (mRunQueue != null) {    
    
        mRunQueue.executeActions(info.mHandler);        
        mRunQueue = null;    
        
    }

}

看到这里我们就知道了,View.post 和 Handler.post 的区别就是:

  1. 如果在 performTraversals 前调用 View.post,则会将消息进行保存,之后在 dispatchAttachedToWindow 的时候通过 ViewRootImpl 中的 Handler 进行调用。
  2. 如果在 performTraversals 以后调用 View.post,则直接通过 ViewRootImpl 中的 Handler 进行调用。

这里我们又可以回答一个问题了,就是为什么 View.post 里可以拿到 View 的宽高信息呢?因为 View.post 的 Runnable 执行的时候,已经执行过 performTraversals 了,也就是 View 的 measure layout draw 方法都执行过了,自然可以获取到 View 的宽高信息了。

48. Handler 导致的内存泄漏

Handler 内存泄漏的根本原因

  1. Handler 持有外部类的引用

    • 在 Android 中,Handler 是一个非静态的内部类。非静态内部类会隐式持有外部类(通常是 ActivityFragment)的引用。
    • 如果 Handler 中有未处理的消息或延迟任务,导致其生命周期超过外部类,则会阻止外部类被回收,造成内存泄漏。
  2. 消息队列未清理

    • Handler 将消息(Message)发送到消息队列(MessageQueue)后,如果外部类已销毁但消息仍未处理完,则 Handler 和外部类的引用链无法被释放。

核心解决方案

  1. 使用静态内部类。
  2. 使用弱引用(WeakReference)保存对外部类的引用。
  3. 在销毁时清理未处理的消息和任务。
  4. 使用更现代的异步任务管理工具(如 Coroutine)。

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

我们使用 Handler 最多的一个场景就是在非主线程通过 Handler 去操作 主线程的 View。那么非 UI 线程真的不能操作 View 吗?我们在执行 UI 操作的时候,都会调用到 ViewRootImpl 里,以 requestLayout 为例,在 requestLayout 里会通过 checkThread 进行线程的检查。

这里的检查,其实并不是检查主线程,是检查 mThread != Thread.currentThread,而 mThread 指的是 ViewRootImpl 创建的线程。所以非 UI 线程确实不能操作 View,但是检查的是创建的线程是否是当前线程,因为 ViewRootImpl 创建是在主线程创建的,所以在非主线程操作 UI 过不了这里的检查。