这一次,让EventBus纯粹一些

2,771 阅读4分钟

大部分安卓开发者应该都用过大名鼎鼎的greenrobot/EventBus,既然他已经封装好了,为什么还有很多人要再封装?

可能是因为Kotlin Flow用的越来越多,人们发现用Flow可以轻松封装EventBus,而且可以和协程更好的配合使用。

为什么这么多人用Flow封装EventBus,我还要再写一个?因为我想要一个纯粹的,只有事件通知的EventBus

封装

1.0版本

object FEvent {
  private val _map = mutableMapOf<Class<*>, MutableSharedFlow<*>>()

  // 发射事件
  suspend fun <T : Any> emit(
    event: T,
    key: Class<T> = event.javaClass,
  ) {
    val flow = _map[key] as? MutableSharedFlow<T>
    flow?.emit(event)
  }

  // 收集事件
  suspend fun <T> collect(
    key: Class<T>,
    block: suspend (T) -> Unit,
  ) {
    val flow = _map.getOrPut(key) { MutableSharedFlow<T>() } as MutableSharedFlow<T>
    flow.collect { block(it) }
  }
}

Class当作key,和事件流MutableSharedFlow做关联,emit发射事件,collect收集事件,整体代码比较简单。

这是一个比较粗略的版本,它还有可以改进的地方,例如:collect结束时,如果flow没有收集者了,应该要把它释放掉。

让我们优化一下,看看下面的版本。

1.1版本

object FEvent {
  // ......

  suspend fun <T> collect(
    key: Class<T>,
    block: suspend (T) -> Unit,
  ) {
    val flow = _map.getOrPut(key) { MutableSharedFlow<T>() } as MutableSharedFlow<T>
    try {
      flow.collect { block(it) }
    } finally {
      if (flow.subscriptionCount.value == 0) {
        // 当前flow没有收集者了,可以移除
        _map.remove(key)
      }
    }
  }
}

省略掉其他非关键代码,在flow.collect时使用try finally,并在finally中判断,如果当前flow没有收集者了,则把它从_map中移除。

这个版本的基本逻辑没什么问题,但它不是线程安全的,让我们继续优化。

1.2版本

在协程中处理并发问题,通常会用单线程调度器或者Mutex

单线程调度器

把可能并发的代码都限制在同一个线程中执行,这样就不会有并发问题了,代码如下:

object FEvent {
  private val _map = mutableMapOf<Class<*>, MutableSharedFlow<*>>()

  suspend fun <T : Any> emit(
    event: T,
    key: Class<T> = event.javaClass,
  ) {
    // 主线程执行
    withContext(Dispatchers.Main) {
      val flow = _map[key] as? MutableSharedFlow<T>
      flow?.emit(event)
    }
  }

  suspend fun <T> collect(
    key: Class<T>,
    block: suspend (T) -> Unit,
  ) {
    // 主线程执行
    withContext(Dispatchers.Main) {
      val flow = _map.getOrPut(key) { MutableSharedFlow<T>() } as MutableSharedFlow<T>
      try {
        flow.collect { block(it) }
      } finally {
        if (flow.subscriptionCount.value == 0) {
          _map.remove(key)
        }
      }
    }
  }
}

通过withContext(Dispatchers.Main)emitcollect都限制在主线程执行。

实际开发中,collect大多在主线程调用,选择主线程作为调度器,block也在主线程触发,比较符合直觉。

Mutex

如果用Mutex,该如何实现呢?

object FEvent {
  // ......
  private val _mutex = Mutex()

  suspend fun <T : Any> emit(
    event: T,
    key: Class<T> = event.javaClass,
  ) {
    _mutex.withLock {
      // ......
    }
  }

  suspend fun <T> collect(
    key: Class<T>,
    block: suspend (T) -> Unit,
  ) {
    _mutex.withLock {
      // ......
      flow.collect { block(it) }
      // ......
    }
  }
}

直接把withContext替换为_mutex.withLock可以吗?

答案是不行!

因为flow.collect是一个挂起函数,在它还没结束之前,其他操作都做不了了,withLock已经被flow.collect占有。

那什么时候调用withLock

object FEvent {
  // ......
  private val _mutex = Mutex()

  suspend fun <T> collect(
    key: Class<T>,
    block: suspend (T) -> Unit,
  ) {
    // 1
    val flow = _mutex.withLock {
      _map.getOrPut(key) { MutableSharedFlow<T>() } as MutableSharedFlow<T>
    }
    try {
      // 2
      flow.collect { block(it) }
    } finally {
      // 3
      _mutex.withLock {
        if (flow.subscriptionCount.value == 0) {
          _map.remove(key)
        }
      }
    }
  }
}

注释1注释3处调用可以吗?

也不行!因为_mutex.withLock是一个挂起函数,当finally被执行时,当前协程可能已经被取消。

要让一个已经取消的协程正常调用挂起函数,需要使用NonCancellable

suspend fun <T> collect(
    key: Class<T>,
    block: suspend (T) -> Unit,
  ) {
    val flow = _mutex.withLock {
      _map.getOrPut(key) { MutableSharedFlow<T>() } as MutableSharedFlow<T>
    }
    try {
      flow.collect { block(it) }
    } finally {
      // 使用NonCancellable
      withContext(NonCancellable) {
        _mutex.withLock {
          if (flow.subscriptionCount.value == 0) {
            _map.remove(key)
          }
        }
      }
    }
  }

对比之下,我更倾向第一种方案主线程调度,既解决了并发问题,又符合直觉。

扩展

扩展一

有时候需要在非协程环境中发射事件,扩展一个非挂起函数:

@JvmOverloads
fun <T : Any> FEvent.post(
  event: T,
  key: Class<T> = event.javaClass,
) {
  GlobalScope.launch(Dispatchers.Main) {
    emit(event, key)
  }
}

这里直接使用GlobalScope,不再单独创建一个CoroutineScope

你可能会有疑问,emit函数已经使用Dispatchers.Main,为什么launch时还要再指定Dispatchers.Main

其实这里指定Dispatchers.Main只是为了按调用顺序发射事件

有些库会单独创建一个CoroutineScope,并指定调度器为Dispatchers.IO或者Dispatchers.Default

实际上这是不安全的,这两个调度器,会通过线程池执行,可能存在并发,不能保证调用顺序和发射顺序一致。

例如:依次调用post函数发射A,B,C,实际上emit函数发射时顺序不一定是A,B,C

扩展二

有时候我们希望收到事件之后,使用Flow操作符做一些变换,扩展一下:

fun <T> FEvent.flowOf(key: Class<T>): Flow<T> = channelFlow {
  collect(key) { send(it) }
}

使用channelFlow{}创建冷流,通过flowOf函数获取事件流。

这里不能使用flow{},因为collect中使用withContext(Dispatchers.Main)来切换调度器,这在使用flow{}时是不允许的。

关于sticky

是个EventBus都逃不过sticky,有时候我们把某个事件设置为sticky,允许它重播。

实际上我们需要的是状态!

在早期现代化架构还没普及时,这个所谓的状态,没有它的容身之处,而它刚好从事件而来,所以EventBus理所应当就成了状态容器。

如果不是为了兼容老项目代码,我觉得它应该在Repository中以状态流的形式对外暴露比较合适,例如Flow

写在最后

这一次,不想让EventBus那么累了,做一个纯粹的事件总线或许才是它真正的使命!

完整代码在这里:

github.com/zj565061763…