请点赞关注,你的支持对我意义重大。

Hi,我是小彭。本文已收录到 GitHub · AndroidFamily 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] 带你建立核心竞争力。

前言

大家好,我是小彭。2 年前,我们在 为了组件化改造学习十几家大厂的技术博客 这篇文章里收集过各大厂的组件化方案。其中,有美团收银团队分享的组件化总线框架 modular-event 让我们印象深刻。然而,美团并未将该框架开源,我们只能望梅止渴。

在学习和借鉴美团 modular-event 方案中很多优秀的设计思想后,我亦发现方案中依然存在不一致风险和不足,故我决定对方案进行改进并向社区开源。项目主页为 Github · ModularEventBus,演示 Demo 可直接下载:

Demo apk

欢迎提 Issue 帮助修复缺陷,欢迎提 Pull Request 增加新的 Feature,有用请点赞给 Star,给小彭一点创作的动力,谢谢。


这篇文章是 组件化系列文章第 5 篇,相关 Android 工程化专栏完整文章列表:

一、Gradle 基础:

二、AGP 插件:

三、组件化开发:

四、AOP 面向切面编程:

五、相关计算机基础


1. 认识事件总线

1.1 事件总线的优点

事件总线框架最大的优点是 ”解耦“,即事件发布者与事件订阅者的解耦,事件的发布者不需要关心是否有人订阅该事件,也不需要关心是谁订阅该事件,代码耦合度较低。因此,事件总线框架更适合作为全局的事件通信方案,或者组件间通信的辅助方案。

1.2 事件总线的缺点

然而,成也萧何败萧何。有人觉得事件总线好用,亦有人觉得事件总线不好用,归根结底还是因为事件总线太容易被滥用了,用时一时爽,维护火葬场。我将事件总线框架存在的问题概括为以下 5 种常见问题:

  • 1、消息难溯源: 在阅读源码的过程中,如果需要查找发布事件或订阅事件的地方,只能通过查找事件引用的方式进行溯源,增大了理解代码逻辑的难度。特别是当项目中到处是临时事件时,难度会大大增加;

  • 2、临时事件滥用: 由于框架对事件定义没有强制约束,开发者可以随意地在项目的各个角落定义事件。导致整个项目都是临时事件飞来飞去,增大后期维护的难度;

  • 3、数据类型转换错误: LiveDataBus 等事件总线框架需要开发者手动输入事件数据类型,当订阅方与发送方使用不同的数据类型时,会发生类型转换错误。在发生事件命名冲突时,出错的概率会大大增加,存在隐患;

  • 4、事件命名重复: 由于框架对事件命名没有强制约束,不同组件有可能定义重名的事件,产生逻辑错误。如果重名的事件还使用了不同的数据类型,还会出现类型转换错误,存在隐患;

  • 5、事件命名疏忽: 与 ”事件命名重复“ 类似,由于框架对事件命名没有检查,有可能出现开发者复制粘贴后忘记修改事件变量值的问题,或者变量值拼写错误(例如 login_success 拼写为 login_succese),那么订阅方将永远收不到事件。

1.3 ModularEventBus 的解决方案

ModularEventBus 组件化事件总线框架的优点是: 在保持发布者与订阅者的解耦的优势下,解决上述事件总线框架中存在的通病。 具体通过以下 5 个手段实现:

  • 1、事件声明聚合: 发布者和订阅者只能使用预定义的事件,严格禁止使用临时事件,事件需要按照约定聚合定义在一个文件中(解决临时事件滥用问题);

  • 2、区分不同组件的同名事件: 在定义事件时需要指定事件所属 moduleName,框架自动使用 "[moduleName]$$[eventName]" 作为最终的事件名(解决事件命名重复问题);

  • 3、事件数据类型声明: 在定义事件时需要指定事件的数据类型,框架自动使用该数据类型发送和订阅事件(解决数据类型转换错误问题);

  • 4、接口强约束: 运行时使用事件类发布和订阅事件,框架自动使用事件定义的事件名和数据类型,而不需要手动输入事件名和数据类型(解决事件命名命名错误);

  • 5、APT 生成接口类: 框架在编译时使用 APT 注解处理器自动生成事件接口类。

1.4 与美团 modular-event 对比有哪些什么不同?

  • modular-event 使用静态常量定义事件,为什么 ModularEventBus 用接口定义事件?

    美团 modular-event 使用常量引入了重复信息,存在不一致风险。例如开发者复制一行常量后,只修改常量名但忘记修改值,这种错误往往很难被发现。而 ModularEventBus 使用方法名作为事件名,方法返回值作为事件数据类型,不会引入重复信息且更加简洁。

modular-event 事件定义

  • modular-event 使用动态代理,为什么 ModularEventBus 不需要?

    美团 modular-event 使用动态代理 API 统一接管了事件的发布和订阅,但考虑到这部分代理逻辑非常简单(获取事件名并交给 LiveDataBus 完成后续的发布和订阅逻辑),且框架本身已经引入了编译时 APT 技术,完全可以在编译时生成这部分代理逻辑,没必要使用动态代理 API。

  • 更多特性支持:

    此外 ModularEventBus 还支持生成事件文档、空数据拦截、泛型事件、自动清除空闲事件等特性。


2. ModularEventBus 能做什么?

ModularEventBus 是一款帮助 Android App 解决事件总线滥用问题的框架,亦可作为组件化基础设施。 其解决方案是通过注解定义事件,由编译时 APT 注解处理器进行合法性检查和自动生成事件接口,以实现对事件定义、发布和订阅的强约束。

2.1 常见事件总线框架对比

以下从多个维度对比常见的事件总线框架( 良好支持、️ 支持、 不支持):

事件总线 ModularEventBus modular-event SmartEventBus LiveEventBus LiveDataBus EventBus RxBus
开发者 @彭旭锐 @美团 @JeremyLiao @JeremyLiao / @greenrobot /
Github Star 0 未开源 146 3.4k / 24.1k /
生成事件文档
空数据拦截
无数据事件
泛型事件
自动清除空闲事件
事件强约束
生命周期感知
延迟发送事件
有序接收事件
订阅 Sticky 事件
清除 Sticky 事件
移除事件
线程调度
跨进程 / 跨 App (可支持)
关键原理 APT+静态代理 APT+动态代理 APT+静态代理 LiveData LiveData APT RxJava

2.2 ModularEventBus 特性一览

1、事件强约束

支持零配置快速使用;

支持 APT 注解处理器自动生成事件接口类;

支持编译时合法性校验和警告提示;

支持生成事件文档;

支持增量编译;

2、Lifecycle 生命周期感知

内置基于 LiveData 的 LiveDataBus;

支持自动取消订阅,避免内存泄漏;

支持安全地发送事件与接收事件,避免产生空指针异常或不必要的性能损耗;

支持永久订阅事件;

支持自动清除没有关联订阅者的空闲 LiveData 以释放内存;

3、更多特性支持

支持 Java / Kotlin;

支持 AndroidX;

支持订阅 Sticky 粘性事件,支持移除事件;

支持 Generic 泛型事件,如 List<String> 事件;

支持拦截空数据;

支持只发布事件不携带数据的无数据事件;

支持延迟发送事件;

支持有序接收事件。


3. ModularEventBus 快速使用

  • 1、添加依赖

模块级 build.gradle

  1. plugins {
  2. id 'com.android.application' // 或 id 'com.android.library'
  3. id 'org.jetbrains.kotlin.android'
  4. id 'kotlin-kapt'
  5. }
  6. dependencies {
  7. // 替换成最新版本
  8. implementation 'io.github.pengxurui:modular-eventbus-api:1.0.4'
  9. kapt 'io.github.pengxurui:modular-eventbus-compiler:1.0.4'
  10. ...
  11. }
  • 2、定义事件数据类型(可选): 定义事件关联的数据类型,对于只发布事件而不需要携带数据的场景,可以不定义事件类型。

UserInfo.kt

  1. data class UserInfo(val userName: String)
  • 3、定义事件: 使用接口定义事件名和事件数据类型,并使用 @EventGroup 注解修饰该接口:

LoginEvents.kt

  1. @EventGroup
  2. interface LoginEvents {
  3. // 事件名:login
  4. // 事件数据类型:UserInfo
  5. fun login(): UserInfo
  6. // 事件名:logout
  7. fun logout()
  8. }
  • 4、执行注解处理器: 执行 Make ProjectRebuild Project 等多种方式都可以触发注解处理器,处理器将根据事件定义自动生成相应的事件接口。例如,LoginEvents 对应的事件类为:

EventDefineOfLoginEvents.java

  1. /**
  2. * Auto generate code, do not modify!!!
  3. * @see com.pengxr.sampleloginlib.events.LoginEvents
  4. */
  5. @SuppressWarnings("unchecked")
  6. public class EventDefineOfLoginEvents implements IEventGroup {
  7. private EventDefineOfLoginEvents() {
  8. }
  9. public static IEvent<UserInfo> login() {
  10. return (IEvent<UserInfo>) (ModularEventBus.INSTANCE.createObservable("com.pengxr.sampleloginlib.events.LoginEvents$$login", UserInfo.class, false, true));
  11. }
  12. public static IEvent<Void> logout() {
  13. return (IEvent<Void>) (ModularEventBus.INSTANCE.createObservable("com.pengxr.sampleloginlib.events.LoginEvents$$logout", Void.class, true, false));
  14. }
  15. }
  • 5、订阅事件: 使用 EventDefineOfLoginEvents 事件类提供的静态方法订阅事件:

订阅者示例

  1. // 以生命周期感知模式订阅事件(不需要手动注销订阅)
  2. EventDefineOfLoginEvents.login().observe(this) { value: UserInfo? ->
  3. // Do something.
  4. }
  5. // 以永久模式订阅事件(需要手动注销订阅)
  6. EventDefineOfLoginEvents.logout().observeForever { _: Void? ->
  7. // Do something.
  8. }
  • 6、发布事件: 使用 EventDefineOfLoginEvents 提供的静态方法发布事件:

发布者示例

  1. EventDefineOfLoginEvents.login().post(UserInfo("XIAOPENG"))
  2. EventDefineOfLoginEvents.logout().post(null)
  • 7、添加混淆规则(如果使用了 minifyEnabled true):
  1. -dontwarn com.pengxr.modular.eventbus.generated.**
  2. -keep class com.pengxr.modular.eventbus.generated.** { *; }
  3. -keep @com.pengxr.modular.eventbus.facade.annotation.EventGroup class * {*;} # 可选

4. 完整使用文档

4.1 定义事件

  • 使用注解定义事件:

    • @EventGroup 注解: @EventGroup 注解用于定义事件组,修饰于 interface 接口上,在该类中定义的每个方法均视为一个事件定义;

    • @Event 注解: @Event 注解用于事件组中的事件定义,亦可省略。

模板程序如下:

com.pengxr.sample.events.MainEvents.kt

  1. // 事件组
  2. @EventGroup
  3. interface MainEvents {
  4. // 事件
  5. // @Event 可以省略
  6. @Event
  7. fun open(): String
  8. }

提示: 以上即定义了一个 MainEvents 事件组,其中包含一个 com.pengxr.sample.events.MainEvents$$open 事件且数据类型为 String 类型。

亦兼容将 @EventGroup 修饰于 class 类而非 interface 接口,但会有编译时警告: Annotated @EventGroup on a class type [IllegalEvent], expected a interface. Is that really what you want?

错误示例

  1. @EventGroup
  2. class IllegalEvent {
  3. fun illegalEvent() {
  4. }
  5. }
  • 使用 @Ignore 注解忽略定义: 使用 @Ignore 注解可以排除事件类或事件方法,使其不被视为事件定义。

示例程序

  1. // 可以修饰于事件组
  2. @Ignore
  3. @EventGroup
  4. interface IgnoreEvent {
  5. // 亦可修饰于事件
  6. @Ignore
  7. fun ignoredMethod()
  8. fun method()
  9. }
  • 使用 @Deprecated 注解提示过时: 使用 @Deprecated 注解可以标记事件为过时。与 @Ignore 不同是,@Deprecated 修饰的类或方法依然是有效的事件定义。

示例程序

  1. // 虽然过时,但依然是有效的事件定义
  2. @Deprecated("Don't use it.")
  3. @EventGroup
  4. interface DeprecatedEvent {
  5. @Deprecated("Don't use it.")
  6. fun deprecatedMethod()
  7. }
  • 定义事件数据类型: 事件方法返回值即表示事件数据类型,支持泛型(如 List<String>),支持不携带数据的无数据事件。以下均为合法定义:

Java 示例程序

  1. // 事件数据类型为 String
  2. String stringEventInJava();
  3. // 事件数据类型为 List<String>
  4. List<String> listEventInJava();
  5. // 以下均视为无数据事件
  6. void voidEventInJava1();
  7. Void voidEventInJava2();

Kotlin 示例程序

  1. // 事件数据类型为 String
  2. fun stringEventInKotlin(): String
  3. // 事件数据类型为 List<String>
  4. fun listEventInKotlin(): List<String>
  5. // 以下均视为无数据事件
  6. fun voidEventInKotlin1()
  7. fun voidEventInKotlin2(): Unit
  8. fun voidEventInKotlin3(): Unit?
  • 定义事件数据可空性: 使用 @Nullable@NonNull 注解表示事件数据可空性,默认为可空类型。以下均为合法定义:

Java 示例程序

  1. @NonNull
  2. String nonNullEventInJava();
  3. @Nullable
  4. String nullableEventInJava();
  5. // 默认视为 @Nullable
  6. String eventInJava();

Kotlin 示例程序

  1. fun nonNullEventInKotlin(): String
  2. // 提示:Kotlin 编译器将返回类型上的 ? 号视为 @org.jetbrains.annotations.Nullable
  3. fun nullableEventInKotlin(): String?

以下为支持的可空性注解:

  1. org.jetbrains.annotations.Nullable
  2. android.annotation.Nullable
  3. androidx.annotation.Nullable
  4. org.jetbrains.annotations.NotNull
  5. android.annotation.NonNull
  6. androidx.annotation.NonNull
  • 定义自动清除事件: 支持配置在事件没有关联的订阅者时自动被清除(以释放内存),默认值为 false。可以使用 @EventGroup 注解或 @Event 注解进行修改,以 @Event 的取值优先。

示例程序

  1. @EventGroup(autoClear = true)
  2. interface MainEvents {
  3. @Event(autoClear = false)
  4. fun normalEvent(): String
  5. // 继承 @EventGroup 中的 autoClear 取值
  6. fun autoClearEvent(): String
  7. }
  • 定义事件所属组件名: 为避免不同组件中的事件名重复,框架自动使用 "[moduleName]$$[eventName]" 作为最终的事件名。默认使用事件组的 [全限定类名] 作为 moduleName,可以使用 @EventGroup 注解进行修改。

示例程序

com.pengxr.sample.events.MainEvents.kt

  1. @EventGroup(moduleName = "main")
  2. interface MainEvents {
  3. fun open(): String
  4. }

提示: 以上即定义了一个 MainEvents 事件组,其中包含一个 main$$open 事件且数据类型为 String 类型。

4.2 执行注解处理器

在完成事件定义后,执行 Make ProjectRebuild Project 等多种方式都可以触发注解处理器,处理器将根据事件定义自动生成相应的事件接口。例如, MainEvents 对应的事件接口为:

com.pengxr.modular.eventbus.generated.events.com.pengxr.sample.events.EventDefineOfMainEvents.java

  1. /**
  2. * Auto generate code, do not modify!!!
  3. * @see com.pengxr.sample.events.MainEvents
  4. */
  5. @SuppressWarnings("unchecked")
  6. public class EventDefineOfMainEvents implements IEventGroup {
  7. private EventDefineOfMainEvents() {
  8. }
  9. public static IEvent<String> open() {
  10. return (IEvent<String>) (ModularEventBus.INSTANCE.createObservable("main$$open", String.class, false, false));
  11. }
  12. }

EventDefineOfMainEvents 中的静态方法与 MainEvent 事件组中的每个事件一一对应,直接通过静态方法即可获取事件实例,而不再通过手动输入事件名字符串或事件数据类型,故可避免事件名错误或数据类型错误等问题。

所有的事件实例均是 IEvent 泛型接口的实现类,例如 open 事件属于 IEvent<String> 类型的事件实例。发布事件和订阅事件需要用到 IEvent 接口中定义的一系列 post 方法和 observe 方法,IEvent 接口的完整定义如下:

IEvent.kt

  1. interface IEvent<T> {
  2. /**
  3. * 发布事件,允许在子线程发布
  4. */
  5. @AnyThread
  6. fun post(value: T?)
  7. /**
  8. * 延迟发布事件,允许在子线程发布
  9. */
  10. @AnyThread
  11. fun postDelay(value: T?, delay: Long)
  12. /**
  13. * 延迟发布事件,在准备发布前会检查 producer 处于活跃状态,允许在子线程发布
  14. *
  15. * @param producer 发布者的 LifecycleOwner
  16. */
  17. @AnyThread
  18. fun postDelay(value: T?, delay: Long, producer: LifecycleOwner)
  19. /**
  20. * 发布事件,允许在子线程发布,确保订阅者按照发布顺序接收事件
  21. */
  22. @AnyThread
  23. fun postOrderly(value: T?)
  24. /**
  25. * 以生命周期感知模式订阅事件(不需要手动注销订阅)
  26. */
  27. @AnyThread
  28. fun observe(consumer: LifecycleOwner, observer: Observer<T?>)
  29. /**
  30. * 以生命周期感知模式粘性订阅事件(不需要手动注销订阅)
  31. */
  32. @AnyThread
  33. fun observeSticky(consumer: LifecycleOwner, observer: Observer<T?>)
  34. /**
  35. * 以永久模式订阅事件(需要手动注销订阅)
  36. */
  37. fun observeForever(observer: Observer<T?>)
  38. /**
  39. * 以永久模式粘性订阅事件(需要手动注销订阅)
  40. *
  41. * @param observer Event observer.
  42. */
  43. @AnyThread
  44. fun observeStickyForever(observer: Observer<T?>)
  45. /**
  46. * 注销订阅者
  47. */
  48. @AnyThread
  49. fun removeObserver(observer: Observer<T?>)
  50. /**
  51. * 移除事件,关联的订阅者关系也会被解除
  52. */
  53. @AnyThread
  54. fun removeEvent()
  55. }

4.3 订阅事件

使用 IEvent 接口定义的一系列 observe() 接口订阅事件,使用示例:

示例程序

  1. // 以生命周期感知模式订阅(不需要手动注销订阅)
  2. EventDefineOfMainEvents.open().observe(this) {
  3. // do something.
  4. }
  5. // 以生命周期感知模式、且粘性模式订阅(不需要手动注销订阅)
  6. EventDefineOfMainEvents.open().observeSticky(this) {
  7. // do something.
  8. }
  9. val foreverObserver = Observer<String?> {
  10. // do something.
  11. }
  12. // 以永久模式订阅(需要手动注销订阅)
  13. EventDefineOfMainEvents.open().observeForever(foreverObserver)
  14. // 以永久模式,且粘性模式订阅(需要手动注销订阅)
  15. EventDefineOfMainEvents.open().observeStickyForever(foreverObserver)
  16. // 移除观察者
  17. EventDefineOfMainEvents.open().removeObserver(foreverObserver)

4.4 发布事件

使用 IEvent 接口定义的一系列 post() 接口发布事件,使用示例:

示例程序

  1. // 发布事件,允许在子线程发布
  2. EventDefineOfMainEvents.open().post("XIAO PENG")
  3. // 延迟发布事件,允许在子线程发布
  4. EventDefineOfMainEvents.open().postDelay("XIAO PENG", 5000)
  5. // 延迟发布事件,在准备发布前会检查 producer 处于活跃状态,允许在子线程发布。
  6. EventDefineOfMainEvents.open().postDelay("XIAO PENG", 5000, this)
  7. // 发布事件,允许在子线程发布,确保订阅者按照发布顺序接收事件
  8. EventDefineOfMainEvents.open().postOrderly("XIAO PENG")
  9. // 移除事件
  10. EventDefineOfMainEvents.open().removeEvent()

4.5 更多功能

  • 生成事件文档(可选): 支持生成事件文档,需要在 Gradle 配置中开启:

模块级 build.gradle

  1. // 需要生成事件文档的模块就增加配置:
  2. android {
  3. defaultConfig {
  4. javaCompileOptions {
  5. annotationProcessorOptions {
  6. arguments = [
  7. MODULAR_EVENTBUS_GENERATE_DOC: "enable",
  8. MODULAR_EVENTBUS_MODULE_NAME : project.getName()
  9. ]
  10. }
  11. }
  12. }
  13. }

文档生成路径: build/generated/source/kapt/[buildType]/com/pengxr/modular/eventbus/generated/docs/eventgroup-of-[MODULAR_EVENTBUS_MODULE_NAME].json

  • 配置(可选):

    • debug(Boolean): 调试模式开关;
    • throwNullEventException(Boolean): 非空事件发布空数据时是否抛出 NullEventException 异常,在 release 模式默认为只拦截不抛出异常,在 debug 模式默认为拦截且抛出异常;
    • setEventListener(IEventListener): 全局监听接口。

示例程序

  1. ModularEventBus.debug(true)
  2. .throwNullEventException(true)
  3. .setEventListener(object : IEventListener {
  4. override fun <T> onEventPost(eventName: String, event: BaseEvent<T>, data: T?) {
  5. Log.i(TAG, "onEventPost: $eventName, event = $event, data = $data")
  6. }
  7. })

5. 未来功能规划

  • 支持跨进程 / 跨 App:LiveEventBus 框架支持跨进程 / 跨 App,未来根据使用反馈考虑实现该 Feature;
  • 支持替换内部 EventBus 工厂:ModularEventBus 已预设计事件总线工厂 IEventFactory,未来根据使用反馈考虑公开该 API;
  • 支持基于 Kotlin Flow 的 IEventFactory 工厂;
  • 编译时检查在不同 @EventGroup 中设置相同 modulaName 且相同 eventName,但事件数据类型不同的异常。

6. 共同成长

  • 欢迎提 Issue 帮助修复缺陷;
  • 欢迎提 Pull Request 增加新的 Feature,让 ModularEventBus 变得更加强大,你的 ID 会出现在 Contributors 中;
  • 欢迎加 作者微信 与作者交流,欢迎加入交流群找到志同道合的伙伴

参考资料

我是小彭,带你构建 Android 知识体系。技术和职场问题,请关注公众号 [彭旭锐] 私信我提问。

美团组件化事件总线方案改进:ModularEventBus的更多相关文章

  1. Atitit..组件化事件化的编程模型--(2)---------Web datagridview 服务器端控件的实现原理and总结

    Atitit..组件化事件化的编程模型--(2)---------Web datagridview 服务器端控件的实现原理and总结 1. 服务端table控件的几个流程周期 1 1.1. 确认要显示 ...

  2. atitit.组件化事件化的编程模型--服务端控件(1)---------服务端控件与标签的关系

    atitit.组件化事件化的编程模型--服务端控件(1)---------服务端控件与标签的关系 1. 服务器控件是可被服务器理解的标签.有三种类型的服务器控件: 1 1.1. HTML 服务器控件  ...

  3. Android组件化之终极方案

    Android组件化项目地址:Android组件化项目AndroidModulePattern Fragment或View如何支持组件化 如何管理组件 Fragment或View如何支持组件化 距离 ...

  4. [Android Pro] 终极组件化框架项目方案详解

    cp from : https://blog.csdn.net/pochenpiji159/article/details/78660844 前言 本文所讲的组件化案例是基于自己开源的组件化框架项目g ...

  5. vue兄弟组件传值——事件总线

    1.创建一个js文件,例如msg.js,放到合适位置,例如components中,或者其他位置也行.然后在兄弟两个组件中分别引入msg.js文件 msg.js: import Vue from 'vu ...

  6. Android组件化方案

    Android组件化项目地址:Android组件化项目AndroidModulePattern Android组件化之终极方案地址:http://blog.csdn.net/guiying712/ar ...

  7. vue.js组件化开发实践

    前言 公司目前制作一个H5活动,特别是有一定统一结构的活动,都要码一个重复的轮子.后来接到一个基于模板的活动设计系统的需求,便有了下面的内容.借油开车. 组件化 需求一到,接就是怎么实现,技术选型自然 ...

  8. .Net 事件总线之Autofac解耦

    事件总线是通过一个中间服务,剥离了常规事件的发布与订阅(消费)强依赖关系的一种技术实现.事件总线的基础知识可参考圣杰的博客[事件总线知多少] 本片博客不再详细概述事件总线基础知识,核心点放置使用Aut ...

  9. Autofac解耦事件总线

    事件总线之Autofac解耦 事件总线是通过一个中间服务,剥离了常规事件的发布与订阅(消费)强依赖关系的一种技术实现.事件总线的基础知识可参考圣杰的博客[事件总线知多少] 本片博客不再详细概述事件总线 ...

随机推荐

  1. 10.Linux防火墙iptables之SNAT与DNAT

    Linux防火墙iptables之SNAT与DNAT 目录 Linux防火墙iptables之SNAT与DNAT SNAT策略及应用 SNAT策略概述 SNAT策略典型应用环境 SNAT策略原理 SN ...

  2. v-if和v-for哪个优先级更高?

    首先在实际开发阶段,不应该把v-if和v-for在同一个标签中使用, 在vue2中,v-for的优先级是高于v-if的,如果同时出现,每次渲染都会先执行循环再判断条件,无论如何循环都不可避免,浪费了性 ...

  3. RPA工单查询和下载流程机器人

    1.登录业务系统,输入用户名和密码 2.进入下载模块 3.输入下载查询条件 4.进入文件明细单 5.下载文件 视频地址:https://www.bilibili.com/video/BV1964y1D ...

  4. docker compose 部署 minio

    1.docker-compose.yaml 文件如下: version: '3' services: minio: image: minio/minio:latest # 原镜像`minio/mini ...

  5. Unsupervised Person Re-identification by Soft Multilabel Learning

    简介: 这是一篇19年CVPR的跨域无监督Re-ID论文,在Market1501和DukeMTMC-reID上分别达到了67.7%和67.1%的rank-1精度,算是一篇将准确度刷得比较高的论文了,在 ...

  6. 递归概念&分类&注意事项和练习_使用递归计算1-n之间的和

    递归:方法自己调用自己 递归的分类: 递归分为两种,直接递归和间接递归 直接递归称为方法自身调用自己 间接递归可以A方法调用B方法,B方法调用C方法,C方法调用A方法 注意事项: 递归一定要有条件限定 ...

  7. OneOS家族,LITE版小兄弟诞生了!

    号外,号外!OneOS-Lite诞生啦!前有大哥OneOS,以及一众优秀的RTOS,正所谓珠玉在前,我很难啊.但我可不能怂,大哥叫小O,我就叫小L,站在大哥的肩上,小小L也有发光发热的机会. 小L代码 ...

  8. gpg加解密异常

    在本地windows电脑和开发环境(linux) ,都不报错,但是在测试环境(linux) 上报错. 报错信息 org.bouncycastle.openpgp.PGPException: Excep ...

  9. SpringBoot数据库管理 - 用Liquibase对数据库管理和迁移?

    Liquibase是一个用于用于跟踪.管理和应用数据库变化的开源工具,通过日志文件(changelog)的形式记录数据库的变更(changeset),然后执行日志文件中的修改,将数据库更新或回滚(ro ...

  10. 总结下对我对于CSS中BFC的认知

    首先第一个,什么是BFC? BFC的全称叫Block  Formatting  Context   (块级格式化上下文)BFC是css中隐含属性,开启BFC后元素会变成一个独立的布局环. 简单来说,它 ...