Loading

重新学习Android —— EventBus

EventBus基本示例

定义消息,消息就是一个最基本的pojo类。

data class MessageEvent(val message: String, val createdTime: Date)

编写消息发布者(Publisher)

class LeftFragment : Fragment() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 点击按钮 同步发送消息
        sendButton.setOnClickListener {
            EventBus.getDefault().post(MessageEvent(message, Date()))
        }

        // 点击按钮 异步发送消息
        sendAsyncButton.setOnClickListener {
            thread {
                Thread.sleep(1000)
                EventBus.getDefault().post(MessageEvent(message, Date()))
            }
        }

    }
}

编写消息订阅者(Subscriber)

class RightFragment : Fragment() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // bindview...
        // 当ViewCreated时,注册订阅者
        EventBus.getDefault().register(this)
    }

    override fun onDestroyView() {
        super.onDestroyView()
        // 当View销毁时,取消订阅
        EventBus.getDefault().unregister(this)
    }

    // 订阅的回调,无论如何该回调会在主线程执行,所以可以直接更新UI
    @Subscribe(threadMode = ThreadMode.MAIN)
    fun onMessageReceived(messageEvent: MessageEvent) {
        messageText.text = messageEvent.message
    }

}

EventBus事件处理模型

EventBus中有如下概念

  1. 事件(Event):EventBus中传递的数据
  2. 发布者(Publisher):将事件传递给EventBus
  3. 订阅者(Subscriber):当EventBus接收到相应事件时,会发送给订阅者

所以EventBus是一个基于发布-订阅者模式的事件总线。使用它可以让组件(如Activity、Fragment)之间解耦,消除组件之间的依赖关系,并简化组件间通信。

像上面的例子,如果不用EventBus,那么要么两个Fragment按照某种方式建立通信,要么它们两个与所在的Activity建立通信,反正怎么都逃不过组件间的依赖

递送线程(ThreadMode)

EventBus事件的发送者可以将事件Post到与发送时不同的线程中,一个最基本的应用场景就是上面的sendAsync,因为android中的UI操作都必须在UI线程执行,而像网络、延时任务等耗时操作都必须在其他线程执行,EventBus可以帮你完成子线程到UI线程的转换。

比如上面的代码,使用@Subscribe(threadMode = ThreadMode.MAIN)指定了该方法会在UI线程中被回调,而无论事件的发送者在哪个线程中。下面介绍几种其它的ThreadMode

注意,下面我们讨论的都是站在订阅者视角,是订阅者在哪个线程接收发布者的事件。

ThreadMode.POSTING

订阅者的回调会在发布者发布消息的同一线程中被调用,这代表其间没有线程转换,订阅者会被同步调用。

如果发布者在非UI线程,订阅者使用POSTING模式,它就不能直接处理UI任务。这个模式也是EventBus中的默认模式,因为没有线程切换就意味着没有这部分的开销。

也正是因为它和发布者在同一线程,所以使用该模式的订阅者应让回调可以快速返回,以免阻塞发布者线程。

ThreadMode.MAIN

订阅者的回调会在主线程中被调用,如果发布者也在主线程,那么不会进行线程切换。

为了避免阻塞主线程,该模式的订阅者回调也应该快速返回。

ThreadMode.MAIN_ORDERED

订阅者的回调会在主线程中被调用。不同的是,发布者post事件到一个队列中,然后立即返回去做自己的事,不会阻塞发布者线程。这些队列中的数据在稍后会按顺序发布给订阅者。这给事件带来更加严格的顺序。

ThreadMode.BACKGROUND

订阅者会在一个后台线程(Background Thread)中被回调。如果发布者不在UI线程,那么就会直接在发布者线程中回调,如果在UI线程,那么EventBus会使用一个后台线程,该线程会顺序的交付所有事件。为了避免阻塞这个后台线程,回调也应该迅速的返回。

官方文档说的有点迷惑,看看具体是咋回事,我们在上面的例子中添加了如下订阅者,它只是在接到消息时打印当前线程名:

@Subscribe(threadMode = ThreadMode.BACKGROUND)
fun onMessageReceived2(messageEvent: MessageEvent) {
    Log.i("onMessageReceived2", "接到消息,当前线程:"+ Thread.currentThread().name)
}

然后我们尝试使用非UI线程发送事件:

回顾上面我们的非UI线程发送事件的代码,我们是每次使用thread重新创建了一个线程,这其中没有线程的复用,所以根据官方文档所说,非UI线程发布事件,BACKGROUND模式的订阅者直接在发布者线程被回调,那么应该每次订阅者都在一个新线程中被回调。

下面我们尝试使用UI线程进行发送事件,官方文档所说,如果发布者在UI线程,BACKGROUND模式的订阅者在一个后台线程顺序的被回调。那么订阅者应该始终在同一个线程中被回调,并且这个还不是UI线程,应该是EventBus提供的一个什么可复用的后台线程。

可以看到确实是这样,而且这个线程的名字也暗示了EventBus维护了线程池。

ThreadMode.ASYNC

订阅者接收到的线程永远和发布者不同,并且也不是主线程。这让发布者线程和UI线程可以不用等待订阅者回调执行完毕。如果你有一些比较繁重的任务,可以使用这个模式。同时因为EventBus维护了线程池,所以不用担心频繁的触发该订阅者会达到最大并发数限制。

ThreadMode总结

下面表格是ThreadMode,和订阅者使用该ThreadMode是否阻塞发布者与UI线程的表格

模式 阻塞发布者 阻塞UI线程
POSTING 必然 可能,如果发布者在UI线程
MAIN 可能,当发布者在UI线程 必然
MAIN_ORDERED 不阻塞,即使发布者与订阅者在同一线程 必然
BACKGROUND 可能,如果发布者在非UI线程 不阻塞,但注意可能会阻塞EventBus中的后台线程
ASYNC 不阻塞 不阻塞

所以只有ASYNC模式时,订阅者可以放心大胆的执行耗时任务。

配置

之前我们使用的都是EventBus.getDefault中返回的默认共享EventBus实例,现实很复杂的业务中如果直接用这个会很乱套。

我们可以基于Builder模式创建并配置自己的EventBus

下面的EventBus在发布没有订阅者的事件时保持静默

EventBus eventBus = EventBus.builder()
    .logNoSubscriberMessages(false)
    .sendNoSubscriberEvent(false)
    .build();

下面的EventBus在订阅者抛出异常时抛出相同的异常,也就相当于结束自己了

EventBus eventBus = EventBus.builder().throwSubscriberException(true).build();

默认情况下,EventBus捕获订阅者抛出的异常并发送一个SubscriberExceptionEvent,同样可以通过订阅这个事件来做一些处理或提示用户。

如下方法可以对默认EventBus进行配置。

EventBus.builder().throwSubscriberException(BuildConfig.DEBUG).installDefaultEventBus();

粘性事件

有时订阅者可能对一些已经发布的事件感兴趣。举个例子,当用户登录成功,一个发布者发布了当前用户对象供订阅者使用,那么后来的订阅者(比如当前尚未开启的Activity)就接不到这个事件,一般来说可以使用某种缓存来处理该问题,但粘性事件更好。

粘性事件允许后来的某个事件的粘性订阅者接收到某个发布者最后发布的粘性事件。这是个双向的协定,订阅者必须是粘性订阅者,发布者必须发布粘性事件,缺一不可。

还是之前的例子,这次我们添加一个发布粘性事件的按钮

sendStickyButton.setOnClickListener {
    EventBus.getDefault().postSticky(MessageEvent(message, Date()))
    Log.i("syncMessage", "sendOver")
}

在另一个Activity中,我们分别注册两个订阅者来监听粘性和非粘性事件

@Subscribe(threadMode = ThreadMode.MAIN, sticky = false)
fun onEventNonSticky(messageEvent: MessageEvent) {
    nonstickyResult.text = messageEvent.message
}

@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onEventSticky(messageEvent: MessageEvent) {
    stickyResult.text = messageEvent.message
}

结果只有当在前一个Activity中发布了粘性事件后,第二个Activity的粘性事件订阅者才会被回调

订阅者也可以移除该粘性事件

@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onEventSticky(messageEvent: MessageEvent) {
    stickyResult.text = messageEvent.message
    EventBus.getDefault().removeStickyEvent(messageEvent)
  }

这样,一旦这个粘性事件被这个订阅者接受并移除,任何订阅者都不会再接收到该粘性事件。

优先级

可以使用priority指定订阅者的优先级,在同一ThreadMode下的订阅者优先级越高的越先接收到事件。

@Subscribe(priority = 1);

priority不会影响到不同ThreadMode订阅者接收到事件的顺序

取消事件递送

可以使用cancelEventDelivery来取消事件传递,后面的订阅者不会接收到事件。

// Called in the same thread (default)
@Subscribe
public void onEvent(MessageEvent event){
    // Process the event
    ...
    // Prevent delivery to other subscribers
    EventBus.getDefault().cancelEventDelivery(event) ;
}

订阅者索引

订阅者索引是在EventBus3起提供的特性,它的主要功能是避免运行时通过注解反射的方式寻找订阅者所带来的开销和崩溃。

索引需求

满足如下需求才会使用订阅者索引

  1. @Subscribe方法和所在类必须是public
  2. 事件类必须是public
  3. @Subscribe不在匿名内部类中

上面需求若不满足,EventBus就不会使用订阅者索引,而是回退到之前的运行时通过反射解析。

如何生成索引

Java:使用annotationProcessor

android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [ eventBusIndex : 'com.example.myapp.MyEventBusIndex' ]
            }
        }
    }
}
 
dependencies {
    def eventbus_version = '3.2.0'
    implementation "org.greenrobot:eventbus:$eventbus_version"
    annotationProcessor "org.greenrobot:eventbus-annotation-processor:$eventbus_version"
}

Kotlin:使用kapt

apply plugin: 'kotlin-kapt' // ensure kapt plugin is applied
 
dependencies {
    def eventbus_version = '3.2.0'
    implementation "org.greenrobot:eventbus:$eventbus_version"
    kapt "org.greenrobot:eventbus-annotation-processor:$eventbus_version"
}
 
kapt {
    arguments {
        arg('eventBusIndex', 'com.example.myapp.MyEventBusIndex')
    }
}

如何使用索引

至少构建一次项目,来生成特定的索引类。

然后我这里选择在Application中为默认EventBus添加这个索引

class EventBusApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        EventBus.builder().addIndex(MyEventBusIndex()).build()
    }
}
posted @ 2022-01-04 11:14  yudoge  阅读(135)  评论(0编辑  收藏  举报