Handler 系列一:Message

282 阅读4分钟

Message

  • Message:线程之间通讯的数据单元,其中存储着通信信息。

成员变量

  • next:Message
    • 指向消息列表中,当前消息对象的下一个消息。
  • callback:Runnable
    • 当通过 Handler.post(Runnable) 系列的方法发送消息时,则会将参数 Runnable 保存到 Message.callback 成员变量中。
    • 可能为空。当通过 Handler.sendMessage() 系列的方法发送消息时,该成员变量为空。
  • target:Handler
    • 保存的是发送这条消息的 Handler 对象。最终也会由该对象来处理这个消息。
      • 无论通过 Handle.post(Runnable)、Handler.sendMessage() 发送消息,最终都会调用 Handler.enqueueMessage() 私有方法进行处理。
      • Handler.enqueueMessage():首先会对 Message 消息对象进行处理,将当前 Handler 对象赋值给 Message.target 成员变量进行保存,最后调用 MessageQueue.enqueueMessgae() 将处理后的消息对象插入到消息队列中。
      • 由 Looper.loop() 循环中可知,当 Looper 从 MessageQueue.next() 方法获取到可用的可立即消费的消息时,会将消息交给该消息对象的成员变量 target:Handler.dispatchMessage() 方法进行处理。
    • 可能为空。同步屏障的消息,target 成员变量为空。

Message 分类

  • Handler 发送的消息类型分为三种:普通消息(同步消息)、异步消息、同步屏障。
    • 默认情况下,大多数使用的都是普通消息,即同步消息。
  • 不同点 ①:【Message】同步消息和异步消息的区别在于,通过 Message.setAsynchronous(boolean async) 设置为异步消息。
  • 不同点 ②:【Message】同步消息和异步消息,Message.target 不能为空;同步屏障,Message.target 为空。
    • 这是因为同步消息和异步消息,最终需要将消息分发给消息对象的成员变量 target 所指向的 Handler 对象进行处理;而同步屏障不需要处理。
    • 同步屏障的作用只是为了协调消息队列中同步消息和异步消息的分发,屏蔽掉屏障后的所有同步消息,只处理屏障后的所有异步消息;目的是为了让异步消息(即特殊消息)优先执行。
  • 不同点 ③:【消息处理】在没有设置同步屏障之前,同步消息和异步消息在处理上没有任何区别;只有在设置同步屏障后,才会出现差异。即,异步消息需要同步屏障的协助才能发挥作用。
  • 不同点 ④:【消息插入】同步消息和异步消息,插入消息时 MessageQueue.enqueueMessage() 会根据需要去唤醒消息队列;同步屏障,插入消息时 MessageQueue.postSyncBarrier() 不会去唤醒消息队列的。

同步消息

  • 通过 Handler.post(Runnable)/sendMessage() 等系列方法发送消息,该方法的具体实现就是往与其关联的消息队列中去插入一个消息对象 MessageQueue.enqueueMessage()。
  • 通常情况下,发送的消息都是同步消息。

异步消息

  • 方式1:构造 Message 时,通过 Message.setAsynchronous(boolean async) 将消息对象设置为异步消息。
  • 方式2:构造 Handler 时,构造一个用于“发送异步消息”的异步类型 Handler 对象,此时 Handler 的成员变量 mAsynchronous 为 true(默认情况下为 false)。
    • Handler的消息发送过程,最终都会走到 Handler.enqueueMessage() 方法,该方法中会判断 Handler 的成员变量 mAsynchronous 是否会 true?如果为 true,则会将该插入的消息对象 Message 设置为异步消息,调用 Message.setAsynchronous(true) 方法。
  • 构造异步类型的 Handler 对象,这里需要区分 sdk 版本。
    • 如果 sdk > 28,可以通过静态方法 Handler.createAsync() 来创建 Handler 。
    • 如果 sdk <= 28,则需要通过反射,或者直接使用方式1来构造异步消息对象 Message。
  • 异步消息的发送,与同步消息的发送一致。

同步屏障

  • 发送同步屏障:通过 MessageQueue.postSyncBarrier() 方法,该方法会往消息队列中插入一个 target 为空的 Message 消息对象,即同步屏障消息。
    • 通过 Message.target 是否为 null,从而区分出同步屏障消息。
    • 会按照消息的执行时刻将消息插入到消息队列中适当的位置并返回一个 token:Int,用于移除同步屏障。
    • 可以建立多个同步屏障,多个同步屏障将按照指定的时刻在队列中进行排队,通过 token 进行识别。
  • 移除同步屏障:通过 MessageQueue.removeSyncBarrier(token:Int) 方法,从消息队列中移除指定 token 所对应的同步屏障消息。
    • 同步屏障使用完之后记得移除,否则后续的同步消息 Message 将被永远阻塞。
  • 使用场景:屏幕刷新。刷新任务的 Message 不希望被主线程的消息队列阻塞,所以在发送刷新任务的 Message 之前都会先建立一个同步屏障,确保刷新任务优先执行。

获取 Message 对象

  • 有两种方式。
    • 方式1:直接 new 一个 Message 对象。
    • 方式2:通过 Message.obtain()/Handler.obtainMessage() 方法获取一个消息对象。
    • Handler.obtainMessage() 方法内部也是通过 Message.obtain() 方法来获取并返回消息对象。
  • 区别
    • 方式1(直接new一个对象),每次都会去堆内存中分配一块新的内存给消息对象来使用,在使用完毕之后 JVM 会在GC的时候对废弃对象进行内存回收。
    • 方式2(消息池),通过 Message.obtain() 从消息池中获取可用的消息对象,使用消息池的方式来复用消息对象 Message,从而实现内存复用,避免频繁的分配内存和回收内存。
  • 最佳方法是方式2,从回收对象池中提取可复用的消息对象;如果没有才进行创建。

Message.obtain()

1634557208(1).png

  • 这里面涉及到三个 Message 的成员变量。
  • sPoolSync:Object:作为一个锁对象而存在。
    • 在 Message.obtain() 获取消息对象方法、Message.recycleUnchecked() 回收消息对象方法中,都是通过使用该对象来作为锁对象,以阻塞同步的方式,使用内置锁来实现线程安全。
  • sPool:Message:用于存储消息池中的可用消息对象。如果消息池中没有消息,该对象为空。
    • Message 以单向链表的形式,存储消息池中所有可用消息对象;
    • 通过 Message.next 域指向下一个消息对象。
    • 这里需要注意的是,池是静态的 private static,也就是说整个进程会共用一个消息缓存池
  • sPoolSize:Int:当前消息池中已有的可用消息对象的个数。
    • 消息池中最多存储50个消息。这在 Message.recycleUnchecked() 中可知。

1634557154(1).png

  • Message.obtain() 方法:在 synchronized 同步块中判断当前消息池中是否有可用的消息对象?如果 sPool:Message 为空,则表示消息池中没有可用的消息对象,则会通过 new 的方式直接创建 Message 消息对象。如果 sPool:Message 不为空,则说明消息池中有可用的消息对象,则会返回池中的第一个消息对象,此时还会对链表进行调整并将size减1。
  • 池中的对象是如何存在的呢?是通过消息回收而存储的,详见 Message.recycleUnchecked()

Message.recycleUnchecked()

1634557494(1).png

  • Message.recycyleUnchecked() 方法:会将消息对象中的所有成员变量置空,然后在 synchronized 同步块中判断,当前消息池所持有的可用消息对象的数量是否超过最大值50?如果没有超过,则会将该对象作为链表的第一个数据并将链表的size加1,从而将消息缓存到消息池中,以便复用。消息池中默认-最多存储50个消息。
  • 该方法会在以下两种场景使用
    • ① MessageQueue.removeXXX() 消息队列移除消息时,会将消息对象回收到消息池中。
    • ② Looper.loop(),从消息队列中获取到新的处理消息时,会将消息交给其 Message.target.dispatchMessage() 方法进行处理,最后会调用该消息对象的 recycleUnchecked() 方法将该消息对象回收到消息池中。

小结-Message

消息缓存池的实现(与消息队列对比)

  • 消息缓存池:通过 Message 类中的一个数据类型为 Message 的私有静态变量 sPool:Message 来实现,用于保存消息缓存池中的第一个消息对象,同样依赖Message的单向链表来维护消息缓存池中的所有可用的消息对象。
    • 该单链表是静态的,属于进程独有,即进程中的所有线程共享同一个消息缓存池,用于管理进程中所有可复用的消息对象。
    • 当消息池中没有可用消息对象时,该成员变量为空;当该成员变量不为空,则表示消息池中有可复用的消息对象。
  • 消息队列:MessageQueue 通过持有一个成员变量 mMessage:Message 来存储消息队列中所有需要交给Looper调度执行的消息对象。
    • 该单链表是非静态的,只供与其关联的 Looper 所绑定的线程独有,用于管理与其关联的线程中所有需要处理的消息对象。
  • Message 内部通过单链表的方式来存储所有消息对象,按照消息的执行时刻 Message.when 进行入队和出队。
    • 消息缓存池:通过 Message.obtain() 向单链表中获取节点,Message.recycle() 向单链表中插入节点。
    • 消息队列:通过 MessageQueue.enqueueMessage() 向单链表插入节点,MessageQueue.next() 向单链表获取节点。

消息缓存池如何保证线程安全?

  • 消息池中实现线程安全的方式是阻塞同步:synchronized (sPoolSync)。
    • Message 的静态变量 sPoolSync:Object:作为一个锁对象而存在。
    • 在 Message.obtain() 获取消息对象方法、Message.recycleUnchecked() 回收消息对象方法中,都是通过使用该对象来作为锁对象,以阻塞同步的方式实现线程安全。

消息池中的消息对象的来源和消费

  • 获取消息的处理】Message.obtain() 方法
    • ① 在 synchronized(sPoolSync) 同步块中判断当前消息池中是否有可用的消息对象?如果消息池中没有可用的消息对象,即 Message 类的私有静态变量 sPool:Message 为空,则会通过 new 的方式直接创建 Message 消息对象。如果消息池中有可用的消息对象,即 sPool:Message 不为空,则会返回池中的第一个消息对象,然后对链表进行调整并将size减1。
  • 回收消息的处理】Message.recycyleUnchecked() 方法
    • ① 将消息对象中的所有成员变量置空,
    • ② 然后在 synchronized(sPoolSync) 同步块中判断当前消息池所持有的可用消息对象的数量是否超过最大值50?如果没有超过,则会将该对象作为链表的第一个数据,对链表进行调整并将size加1,从而将消息缓存到消息池中。消息池中默认-最多存储50个消息。
  • 回收消息对象的时机】① MessageQueue.removeXXX() 消息队列移除消息时,会将该消息对象回收到消息池中。② Looper.loop() 从消息队列中获取到需要被执行的消息对象时,会将消息交给其 Message.target.dispatchMessage() 方法进行分发处理,之后会对该消息对象进行回收,回收到消息池中以便复用。

Message.when:执行时刻

  • when = 当前时刻 + delay。
    • delay=0:并非立即执行,执行时刻取决于该消息在消息队列中的顺序以及线程当前的阻塞状态。
    • delay>0:并非时间到了就立即执行,执行时刻除了以上两个因素外,还受到唤醒时长的计算误差的影响。
  • when = 0。即插队的 Message。
    • 通过 sendMessageAtFrontOfQueue() 和 postAtFrontOfQueue() 的 API 发送的消息,该消息对象的执行时刻 when 设为 0,然后插入到队列的队首 Head。
    • 执行时刻为0的消息,也同样并非立即执行;而是将消息对象插入到队列中的队首,达到优先执行的目的。
  • 消息队列会按照消息的执行时刻when的先后顺序,按照FIFO的方式将消息插入到队列中。
    • 因此可以知道队首的消息一定是最先抵达的消息。
  • MessageQueue.next() 读取消息时,会根据 "读取的当前时刻"和"消息对象的执行时刻when" 进行比较。
    • 如果队首消息的执行时刻小于当前时间,则将消息出队;
    • 如果队首消息的执行时刻大于当前时间,则计算出当前时刻和目标 when 的差值,交由 Native 等待对应的时长然后时间到了被系统自动唤醒并继续进行 Message 的读取。
  • 事实上,无论上述哪种 Message 都不能保证在其对应的 when 时刻执行,往往都会延迟一些!因为必须等当前执行的 Message 处理完了才有机会读取队列的下一个 Message。