我们开发项目的时候,为了方便组件之间的通信,使代码更加简洁,耦合性更低,需要引入事件总线。事件总线的库我们通常会选择EventBus或者基于Rxjava的RxBus,现在随着jetpack里LiveData的推出,也出现了基于LiveData实现的事件总线库。
那么,除了这些,还有没有其他的实现事件总线的方法呢?在使用协程的过程中,发现协程中的Channel用到了生产者消费者模式,那么可以使用Channel实现事件总线吗?接下来试一下。
Channel是属于kotlin协程,要使用需要在项目中引入协程库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"
Channel类似Java里的BlockingQueue,producer生产事件发送到Channel,consumer从Channel里取出事件进行消费。
kotlin官网文档提供了Channel的用法
val channel = Channel<Int>()
launch {
// this might be heavy CPU-consuming computation or async logic, we'll just send five squares
for (x in 1..5) channel.send(x * x)
}
// here we print five received integers:
repeat(5) { println(channel.receive()) }
println("Done!")
创建Channel,使用Channel.send 发送事件 ,使用Channel.receive 接收事件,要注意的是Channel事件的接收和发送都需要在协程里调用。
class ChannelBus private constructor() {
private var channel: Channel<Events> = Channel(Channel.BUFFERED)
companion object {
val instance: ChannelBus by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
ChannelBus()
}
}
}
context
表示协程执行时的上下文;event
是一个挂起的方法,用来消费事件;jobList
用来保存消费事件的job。 data class ChannelConsumer(
val context: CoroutineContext,
val event: suspend (event: Events) -> Unit,
var jobList: MutableList<Job> = mutableListOf()
)
receive
方法中,key表示存储该消费者时的键,onEvent是一个挂起函数,表示当我们接收到事件时的处理方法,是一个lambda函数,而context为协程上下文,表示这个lambda函数在哪个线程执行,这地方默认在Dispatchers.Main
主线程执行。我们将传入的参数构造成一个ChannelConsumer
对象然后保存在Map中。private val consumerMap = ConcurrentHashMap<String, ChannelConsumer>()
fun receive(
key: String,
context: CoroutineContext = Dispatchers.Main,
onEvent: suspend (event: Events) -> Unit
) {
consumerMap[key] = ChannelConsumer(context, onEvent)
}
Channel
发送出去。 fun send(event: Events) {
GlobalScope.launch {
channel.send(event)
}
}
Channel
为空时会挂起,当有新事件时,事件会从Channel
中取出,我们在这里进行分发。我们遍历Map,得到每个ChannelConsumer
,于是就可以处理事件e
,这里直接通过launch
方法启动协程,协程的上下文 it.value.context
就是receive
方法传入的context
,it.value.event(e)
就是receive
方法传入的lambda函数,e
是 send
方法传入的event
,launch
方法返回一个job
,我们把这个job添加到ChannelConsumer
的jobList
里。 init {
GlobalScope.launch {
for (e in channel) {
consumerMap.entries.forEach {
it.value.jobList.add(launch(it.value.context) {
it.value.event(e)
})
}
}
}
}
ChannelConsumer
,然后循环jobList
并取消每一个job,避免内存泄漏,最后移除消费者。 fun remove(key: String) {
consumerMap[key]?.jobList?.forEach {
it.cancel()
}
consumerMap.remove(key)
}
所以我们在项目里可以这么用
override fun onCreate(savedInstanceState: Bundle?) {
......
ChannelBus.instance.receive("key",Dispatchers.Main,{
activity_main_text.text = it.name
})
......
}
可以简写成如下方式
override fun onCreate(savedInstanceState: Bundle?) {
......
ChannelBus.instance.receive("key") {
activity_main_text.text = it.name
}
......
}
因为传入的是suspend函数,所以如果要进行耗时操作,可以直接执行,只需要把context参数传入IO线程Dispatchers.IO
就行了,然后使用withContext函数切回主线程,再进行UI操作。
override fun onCreate(savedInstanceState: Bundle?) {
......
ChannelBus.instance.receive("key", Dispatchers.IO) {
val s = httpRequest() //IO线程,耗时操作
withContext(Dispatchers.Main) { //切回UI线程
activity_sticky_text.text = s //更改UI
}
}
}
//网络请求
private fun httpRequest(): String {
val url = URL("https://api.github.com/users/LGD2009")
val urlConnection = url.openConnection() as HttpURLConnection
urlConnection.let {
it.connectTimeout = 5000
it.requestMethod = "GET"
}
urlConnection.connect()
if (urlConnection.responseCode != 200) {
return "请求url失败"
} else {
val inputStream: InputStream = urlConnection.inputStream
return inputStream.bufferedReader().use { it.readText() }
}
}
ChannelBus.instance.send(Events.EVENT_1)
override fun onDestroy() {
......
ChannelBus.instance.remove("key")
}
上面的方法中,注册消费者之后每次都需要手动取消,那么可不可以自动取消呢?这里就需要用到Lifecycle了。
我们使 ChannelBus 继承 LifecycleObserver
,并重载receive
方法和remove
方法。
重载receive
方法,其中key
换成LifecycleOwner
,然后调用lifecycleOwner.lifecycle.addObserver(this)
将当前的ChannelBus作为观察者添加进去。
class ChannelBus private constructor() : LifecycleObserver {
private val lifecycleOwnerMap = ConcurrentHashMap<LifecycleOwner, ChannelConsumer>()
......
fun receive(
lifecycleOwner: LifecycleOwner,
context: CoroutineContext = Dispatchers.Main,
onEvent: suspend (event: Events) -> Unit
) {
lifecycleOwner.lifecycle.addObserver(this)
lifecycleOwnerMap[lifecycleOwner] = ChannelConsumer(context, onEvent)
}
}
然后重载remove
方法,key
换成了LifecycleOwner
,添加注解。因为现在Activity和Fragment都继承了LifecycleOwner
,当Activity和Fragment运行destroy
销毁时,当前观察者就会观察到并调用这个方法。
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun remove(lifecycleOwner: LifecycleOwner) {
lifecycleOwnerMap[lifecycleOwner]?.jobList?.forEach {
it.cancel()
}
lifecycleOwnerMap.remove(lifecycleOwner)
}
所以,我们在Activity或Fragment里注册消费者时只需
override fun onCreate(savedInstanceState: Bundle?) {
......
ChannelBus.instance.receive(this) {
activity_main_text.text = it.name
}
......
}
当Activity或Fragment销毁时会自动取消注册。
有时候我们可能需要在消费者订阅的时候能收到之前发送的某些事件,这时候就需要用到粘性事件。简单的实现思路是保存事件,在注册消费者时候发送事件。
创建List保存粘性事件,并添加移除粘性事件的方法。
private val stickyEventsList = mutableListOf<Events>()
fun removeStickEvent(event: Events) {
stickyEventsList.remove(event)
}
改造send方法,增加一个 Boolean 类型的参数,用来指明是否是粘性的,当然,默认值是false。如果是true,则把事件存入List。
fun send(event: Events, isSticky: Boolean = false) {
GlobalScope.launch {
if (isSticky) {
stickyEventsList.add(event)
}
channel.send(event)
}
}
添加接收粘性事件的消费者方法。
fun receiveSticky(
lifecycleOwner: LifecycleOwner,
context: CoroutineContext = Dispatchers.Main,
onEvent: suspend (event: Events) -> Unit
) {
lifecycleOwner.lifecycle.addObserver(this)
lifecycleOwnerMap[lifecycleOwner] = ChannelConsumer(context, onEvent)
stickyEventsList.forEach { e ->
lifecycleOwnerMap[lifecycleOwner]?.jobList?.add(GlobalScope.launch(context) {
onEvent(e)
})
}
}
上面的文章中,同一个事件只能取一次,为了发送到多个消费者,所以使用Map保存,然后依次发送。而BroadcastChannel则可以有多个接收端。
所以,如果用BroadcastChannel来实现则更为简单。
创建BroadcastChannel对象
@ExperimentalCoroutinesApi
class BroadcastChannelBus private constructor() : LifecycleObserver {
private val broadcastChannel: BroadcastChannel<Events> = BroadcastChannel(Channel.BUFFERED)
private val lifecycleOwnerMap = ConcurrentHashMap<LifecycleOwner, ChannelConsumer>()
companion object {
val instance: BroadcastChannelBus by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
BroadcastChannelBus()
}
}
数据类jobList改成job,再增加一个receiveChannel
data class ChannelConsumer(
val context: CoroutineContext,
val event: suspend (event: Events) -> Unit,
val job: Job?,
val receiveChannel: ReceiveChannel<Events>
)
发送方法不需要改变,receive方法需要更改。
通过val receiveChannel = broadcastChannel.openSubscription()
订阅,job和receiveChannel保存到数据类。
fun receive(
lifecycleOwner: LifecycleOwner,
context: CoroutineContext = Dispatchers.Main,
onEvent: suspend (event: Events) -> Unit
) {
lifecycleOwner.lifecycle.addObserver(this)
val receiveChannel = broadcastChannel.openSubscription()
val job = GlobalScope.launch(context) {
for (e in receiveChannel) {
onEvent(e)
}
}
lifecycleOwnerMap[lifecycleOwner] = ChannelConsumer(context, onEvent,job,receiveChannel)
}
所以,最后取消订阅时,关闭receiveChannel并取消任务。
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun remove(lifecycleOwner: LifecycleOwner) {
lifecycleOwnerMap[lifecycleOwner]?.receiveChannel?.cancel()
lifecycleOwnerMap[lifecycleOwner]?.job?.cancel()
lifecycleOwnerMap.remove(lifecycleOwner)
}
不过,需要注意的是这个api现在是实验性功能,在后续版本的更新中可能会改变。
这篇文章主要是抛砖引玉,换一种思路,使用Channel实现一个简易的事件总线。实现功能只有一个文件,Demo已上传到github——ChannelBus,大家可以在使用时根据具体的需求进行修改。
kotlin Channel
破解 Kotlin 协程(9) - Channel 篇
Implementing an Event Bus With RxJava - RxBus