【Android】EventBus事件总线用法浅析

【Android】EventBus事件总线用法浅析

EventBus 是一个基于发布-订阅模式的 Android 库,专注于简化组件之间的通信。提供了一种高效、灵活且解耦的方式,用于在应用程序的不同组件(如 Activity、Fragment、Service 等)之间传递消息或事件。比如请求网络,两个Fragment之间需要通过Listener通信,这些需求都可以通过EventBus实现。

EventBus 的核心功能

  • 解耦组件:
    发布者和订阅者之间不需要直接引用,降低了耦合性。
  • 跨线程通信:
    可以指定事件处理在哪个线程执行(主线程、后台线程等)。
  • 事件优先级:
    支持设置订阅者的优先级,以决定事件处理的先后顺序。

EventBus使用方法

首先来看看EventBus的三要素!

1. Event(事件)

定义:事件是消息传递的载体,可以是任意类型的对象,例如自定义的类、基本数据类型的包装类等。

作用:用来承载需要传递的数据。

使用方式:开发者可以根据需求自定义事件类,携带需要传递的信息。

public class MessageEvent {public final String message;public MessageEvent(String message) {this.message = message;}
}
Subscriber(订阅者)

定义:订阅者是接收事件的对象。在 EventBus 中,订阅者需要通过注解 @Subscribe 标记特定的方法来订阅事件。

注意:EventBus 3.0 之前,订阅的方法名称必须是固定的(如 onEventonEventMainThread 等),而 3.0 之后方法名可以自定义,只需用 @Subscribe 注解即可。

订阅者需要指定线程模型,默认是 POSTING 模式。

Publisher(发布者)

定义:事件发布者是负责发送事件的组件,可以是应用中的任意部分。

作用:将事件发送到事件总线,由总线分发给匹配的订阅者。

使用方式:通过调用 EventBus.getDefault().post(Object) 发送事件。

EventBus.getDefault().post(new MessageEvent("Hello EventBus!"));

再来看看 EventBus 的 4 种 ThreadMode 线程模型!

POSTING(默认):如果使用事件处理函数指定了线程模型为POSTING,那么该事件是在哪个线程发布出来的,事件处理函数就会在哪个线程中运行,也就是说发送事件和接收事件在同一个线程中。在线程模型为POSTING的事件处理函数中尽量避免执行耗时操作,因为它会阻塞事件的传递,甚至有可能会引起ANR(Application Not Responding)问题。

MAIN:事件的处理会在切线程中执行。事件处理的时间不能太长,长了会引起ANR问题。

BACKGROUND:如果事件是在U线程中发布出来的,那么该事件处理函数就会在新的线程中运行;如果事件本来就是在子线程中发布出来的,那么该事件处理函数直接在发送事件的线程中执行。在此事件处理函数中禁止进行UI更新操作。

ASYNC:无论事件在哪个线程中发布,该事件处理函数都会在新建的子线程中执行,同样,此事件处理函数中禁止进行UI更新操作。

EventBus 基本用法

EventBus 使用起来分为以下 5 个步骤:

(1)自定义一个事件类

public class MessageEvent {...
}

(2)在需要订阅事件的地方注册事件

EventBus.getDefault().register(this);

(3)发送事件

EventBus.getDefault().post(messageEvent);

(4)处理事件

@Subscribe(threadMode = ThreadMode.MAIN)
public void XXX(MessageEvent messageEvent) {...
}

(5)取消事件订阅

EventBus.getDefault().unregister(this);

EventBus 的黏性事件

黏性事件(Sticky Event) 是 EventBus 提供的一种特殊事件传递机制,即在事件发送之后,即使没有订阅该事件,稍后订阅者也能接收到这个事件。主要用于处理订阅发生在事件发布之后的场景。

黏性事件的原理

当使用 postSticky() 方法发布事件时,EventBus 会将该事件保存到内部的缓存中(StickyEvent Map)。当订阅者订阅时,会立即检查是否有符合条件的黏性事件,如果有则立即分发给订阅者。这种机制特别适合需要保存全局状态或后续动态注册的场景。

详细实现步骤

1. 定义事件类
public class MessageEvent {private String message;public MessageEvent(String message) {this.message = message;}public String getMessage() {return message;}
}
2. 订阅黏性事件

MainActivity 中定义一个处理黏性事件的方法,并用 @Subscribe 注解标记,同时指定 sticky = true

@Subscribe(threadMode = ThreadMode.POSTING, sticky = true)
public void onStickyEvent(MessageEvent messageEvent) {tv_message.setText(messageEvent.getMessage());
}

threadMode = ThreadMode.POSTING:订阅者运行在事件发布的线程。

sticky = true:表示该方法订阅的是黏性事件。

@Override
protected void onStart() {super.onStart();EventBus.getDefault().register(this); // 注册订阅者
}@Override
protected void onStop() {super.onStop();EventBus.getDefault().unregister(this); // 注销订阅者
}
3. 发布黏性事件

SecondActivity 中,通过 postSticky() 方法发布黏性事件:

bt_subscription.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {EventBus.getDefault().postSticky(new MessageEvent("黏性事件"));finish(); // 返回到 MainActivity}
});

MainActivity 中不要立即注册订阅者(不点击“注册事件”按钮)。

跳转到 SecondActivity 并点击“发送黏性事件”按钮,这会发布一个黏性事件并返回到 MainActivity

回到 MainActivity,点击“注册事件”按钮,这时订阅方法会接收到之前发送的黏性事件,更新界面内容。

EventBus使用示例

你是西安邮电大学的一名正在学习安卓的带学生,此时你需要开发一个应用,其中包含如下内容:

  1. 登录页面: 用户输入账号和密码,点击登录。
  2. 首页: 显示用户的个性化内容,如推荐文章、好友动态等。
  3. 个人中心: 显示用户的昵称、头像和其他信息。

当用户成功登录后,首页和个人中心需要同时刷新数据以展示用户的最新状态。例如,首页需要显示用户的推荐内容,个人中心需要更新用户的昵称和头像。


常规实现的问题
  1. 接口直接调用: 登录成功后,直接调用首页和个人中心的刷新接口。这种方法需要登录页面直接依赖首页和个人中心的逻辑,导致强耦合。
  2. 广播机制: Android 的广播可以用来解决,但实现过程较复杂,还需要注册和过滤广播。

用 EventBus 实现
  1. 解耦: 登录页面不需要知道首页和个人中心的具体实现,只需发布一个登录成功的事件。
  2. 异步处理: 首页和个人中心可以根据需要选择在主线程或后台线程中处理事件。

实现步骤

1. 定义事件类

创建一个表示用户登录成功的事件:

public class LoginSuccessEvent {private final String userId;public LoginSuccessEvent(String userId) {this.userId = userId;}public String getUserId() {return userId;}
}
2. 发布事件

在登录成功的逻辑中,发布事件通知其他组件:

// 假设登录成功后获取到用户ID
String userId = "12345";
EventBus.getDefault().post(new LoginSuccessEvent(userId));
3. 订阅事件

在需要响应登录成功事件的页面(如首页和个人中心),订阅事件:

首页代码:

@Subscribe(threadMode = ThreadMode.MAIN)
public void onLoginSuccess(LoginSuccessEvent event) {// 刷新推荐内容refreshContent(event.getUserId());
}@Override
protected void onStart() {super.onStart();EventBus.getDefault().register(this);
}@Override
protected void onStop() {super.onStop();EventBus.getDefault().unregister(this);
}

个人中心代码:

@Subscribe(threadMode = ThreadMode.MAIN)
public void onLoginSuccess(LoginSuccessEvent event) {// 更新用户头像和昵称updateUserInfo(event.getUserId());
}@Override
protected void onStart() {super.onStart();EventBus.getDefault().register(this);
}@Override
protected void onStop() {super.onStop();EventBus.getDefault().unregister(this);
}

模块解耦: 登录页面只负责发布事件,其他页面自行订阅并处理。

线程管理: 首页和个人中心可以指定线程模式,轻松处理 UI 更新或后台操作。

扩展性强: 以后如果有更多页面需要响应登录成功事件,只需订阅 LoginSuccessEvent,无需修改现有代码。

源码解析 EventBus

前面讲到了 EventBus 的用法,本节将讲解 EventBus 的源码。

EventBus 的构造方法

当我们要使用 EventBus 时,首先会调用 EventBus.getDefault() 来获取 EventBus 实例。现在看看 getDefault 方法做了什么,如下所示:

public static EventBus getDefault() {if (defaultInstance == null) {synchronized (EventBus.class) {if (defaultInstance == null) {defaultInstance = new EventBus();}}}return defaultInstance;
}

这是一个单例模式,采用了双重检查锁定(DCL)。

双重检查锁定(DCL,Double-Checked Locking) 是一种常见的线程安全优化方案,通常用于实现单例模式或延迟初始化。它的核心目的是在保证线程安全的前提下,尽量减少加锁操作对性能的影响。

双重检查锁定的目的

提供一种 懒加载 的机制:资源在需要时才被初始化。

减少同步带来的性能开销:避免每次调用都进入同步块。

双重检查锁定的核心思想是:

外层检查:如果对象已经初始化,则直接返回,不需要进入同步代码块。

内层检查:如果对象未初始化,则加锁,确保只有一个线程能进行初始化操作。

同步块外的检查 提高性能,因为大多数情况下对象已经被初始化。

接下来看看 EventBus 的构造方法:

public EventBus() {this.DEFAULT_BUILDER;
}private static final EventBusBuilder DEFAULT_BUILDER = new EventBusBuilder();

这里 DEFAULT_BUILDER 是默认的 EventBusBuilder,用来构造 EventBus。

this.init(EventBusBuilder.builder());
EventBus(EventBusBuilder builder) {subscriptionsByEventType = new HashMap<>();typesBySubscriber = new HashMap<>();stickyEvents = new ConcurrentHashMap<>();mainThreadPoster = new HandlerPoster(this, Looper.getMainLooper(), 10);backgroundPoster = new BackgroundPoster(this);asyncPoster = new AsyncPoster(this);indexCount = 1;subscriberMethodFinder = new SubscriberMethodFinder(builder,builder.strictMethodVerification,builder.ignoreGeneratedIndex,builder.logSubscriberExceptions,builder.sendNoSubscriberEvent,builder.sendSubscriberExceptionEvent,builder.throwSubscriberException);eventInheritance = builder.eventInheritance;
}

我们可以通过构造一个 EventBusBuilder 来对 EventBus 进行配置,这里采用了建造者模式。

订阅者注册

获取 EventBus 后,便可以将订阅者注册到 EventBus 中。下面来看一下 register 方法:

public void register(Object subscriber) {Class<?> subscriberClass = subscriber.getClass();List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);synchronized (this) {for (SubscriberMethod subscriberMethod : subscriberMethods) {subscribe(subscriber, subscriberMethod);}}
}

可以看出register 方法做了两件事:一件事是查找订阅者的订阅方法,另一件事是订阅者的注册。在 SubscriberMethod 类中,主要用来保存订阅方法的 Method 对象、线程模式、事件类型、优先级、是否是黏性事件等属性。

(1)查找订阅者的订阅方法

上面代码通过 findSubscriberMethods 方法,反射获取订阅者中所有被 @Subscribe 标注的方法:

List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass); // 1if (subscriberMethods != null) {return subscriberMethods;}if (ignoreGeneratedIndex) {subscriberMethods = findUsingReflection(subscriberClass);} else {subscriberMethods = findUsingInfo(subscriberClass); // 3}if (subscriberMethods.isEmpty()) {throw new EventBusException("Subscriber " + subscriberClass +" and its super classes have no public methods with the @Subscribe annotation");} else {METHOD_CACHE.put(subscriberClass, subscriberMethods); // 2return subscriberMethods;}
}

从缓存中查找是否有订阅方法的集合,如果找到了就立即返回。如果缓存中没有,则根据 ignoreGeneratedIndex 属性的值来选择采用何种方法查找订阅方法的集合。

ignoreGeneratedIndex 属性表示是否忽略注解器生成的 MyEventBusIndex

ignoreGeneratedIndex 的默认值是 false,可以通过 EventBusBuilder 来设置它的值。

在注释 2 处找到订阅方法的集合后,放入缓存,以免下次继续查找。我们在项目中经常通过 EventBus 单例模式来获取默认的 EventBus 对象,也就是 ignoreGeneratedIndexfalse 的情况。

什么是 Generated Index?

EventBus 提供了一种优化机制,通过 预生成订阅方法索引 来减少在运行时查找订阅方法所花费的时间和资源。

默认行为:EventBus 使用 反射 动态查找类中的订阅方法(即标记了 @Subscribe 注解的方法)。

优化机制:开发者可以生成一个静态的索引类,包含订阅者的所有订阅方法信息,避免运行时使用反射。

通过生成的索引类,EventBus 可以直接通过这个索引快速定位订阅方法,从而提升性能。

假设你在 EventBus 初始化时希望禁用索引:

EventBus eventBus = EventBus.builder().ignoreGeneratedIndex(true) // 忽略索引.build();

或者启用索引(默认值):

EventBus eventBus = EventBus.builder().ignoreGeneratedIndex(false) // 使用索引.addIndex(new MyEventBusIndex()) // 添加生成的索引类.build();

这种情况调用了注释 3 处的 findUsingInfo 方法。

private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {FindState findState = prepareFindState();findState.initForSubscriber(subscriberClass);while (findState.clazz != null) {findState.subscriberInfo = getSubscriberInfo(findState); // 1if (findState.subscriberInfo != null) {SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods(); // 2for (SubscriberMethod subscriberMethod : array) {if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {findState.subscriberMethods.add(subscriberMethod);}}} else {findUsingReflectionInSingleClass(findState); // 3}findState.moveToSuperClass();}return getMethodsAndRelease(findState);
}private void findUsingReflectionInSingleClass(FindState findState) {Method[] methods;try {methods = findState.clazz.getDeclaredMethods();} catch (Throwable th) {methods = findState.clazz.getMethods();findState.skipSuperClasses = true;}for (Method method : methods) {int modifiers = method.getModifiers();if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & Modifiers.STATIC) == 0) {// IGNORE) == 0Class<?>[] parameterTypes = method.getParameterTypes();if (parameterTypes.length == 1) {Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);if (subscribeAnnotation != null) {Class<?> eventType = parameterTypes[0];if (findState.checkAdd(method, eventType)) {ThreadMode threadMode = subscribeAnnotation.threadMode();findState.subscriberMethods.add(new SubscriberMethod(method,eventType,threadMode,subscribeAnnotation.priority(),subscribeAnnotation.sticky()));}}}}}
}

通过 getSubscriberInfo 方法来获取订阅者信息。在我们开始查找订阅方法的时候并没有忽略注解器为我们生成的索引 MyEventBusIndex。如果我们通过 EventBusBuilder 配置了 MyEventBusIndex,便会获取 subscriberInfo

调用 subscriberInfogetSubscriberMethods 方法便可得到订阅方法的相关信息。

如果没有配置 MyEventBusIndex,便会执行 findUsingReflectionInSingleClass 方法,将订阅方法保存到 findState 中。

通过反射来获取订阅者中所有的方法,并根据方法的类型、参数和注解来找到订阅方法。找到订阅方法后将订阅方法的相关信息保存到 findState 中。

(2)订阅者的注册过程

在查找完订阅者的订阅方法以后,便开始对所有的订阅方法进行注册。我们再看 register 方法中,调用了 subscribe 方法来对订阅方法进行注册,如下所示:

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {Class<?> eventType = subscriberMethod.eventType;//为当前订阅者创建一个新的 Subscription 对象,包含订阅者和对应的订阅方法。Subscription 代表了订阅者与事件类型之间的关系。Subscription newSubscription = new Subscription(subscriber, subscriberMethod); //这是通过 eventType 从 subscriptionsByEventType(事件类型与订阅者列表的映射)中获取的现有订阅者列表。如果某事件类型已经有订阅者,这个列表将包含所有的订阅者。CopyOnWriteArrayList 是线程安全的。CopyOnWriteArrayList<Subscription> subscriptions =subscriptionsByEventType.get(eventType);//如果当前 eventType 没有订阅者,则创建一个新的 CopyOnWriteArrayList 并将其与事件类型映射;如果已经有订阅者列表,则检查新订阅者是否已经被注册。如果已经注册,则抛出异常,避免重复注册。if (subscriptions == null) {subscriptions = new CopyOnWriteArrayList<>();subscriptionsByEventType.put(eventType, subscriptions);} else {// 判断订阅者是否已经被注册if (subscriptions.contains(newSubscription)) {throw new EventBusException("Subscriber " + subscriber.getClass() +" already registered to event " + eventType);}}// 订阅者有一个优先级 (priority),如果优先级较高,则会被插入到列表前面。这里遍历现有订阅者列表,找到正确的位置插入新订阅者,使得订阅者列表按照优先级从高到低排序。i == size 表示新订阅者的优先级最低,应该放在列表末尾。int size = subscriptions.size();for (int i = 0; i <= size; i++) {if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {subscriptions.add(i, newSubscription);break;}}// 维护一个 typesBySubscriber 映射,记录每个订阅者订阅的事件类型。每当订阅者订阅一个新的事件类型时,将该事件类型添加到订阅者的事件类型列表中。List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);if (subscribedEvents == null) {subscribedEvents = new ArrayList<>();typesBySubscriber.put(subscriber, subscribedEvents);}subscribedEvents.add(eventType);if (subscriberMethod.sticky) {// 黏性事件的处理Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet();for (Map.Entry<Class<?>, Object> entry : entries) {Class<?> candidateEventType = entry.getKey();if (eventType.isAssignableFrom(candidateEventType)) {Object stickyEvent = entry.getValue();checkPostStickyEventToSubscription(newSubscription, stickyEvent);}}} else {Object stickyEvent = stickyEvents.get(eventType);if (stickyEvent != null) {checkPostStickyEventToSubscription(newSubscription, stickyEvent);}}
}

黏性事件的处理

如果当前订阅者方法被标记为 sticky(黏性事件),则 EventBus 会检查当前事件类型是否有黏性事件与之匹配。黏性事件是指在事件发布后,仍然可以将事件发送给新的订阅者。

stickyEvents 是一个存储黏性事件的映射集合,符合 eventType 的事件将会被发送给新的订阅者。调用 checkPostStickyEventToSubscription 来将黏性事件发送给新的订阅者。

如果 subscriberMethod 没有标记为 sticky,则直接检查当前事件类型是否有已存在的黏性事件,如果有,仍然会将事件发送给新订阅者。

  1. 将订阅者与其订阅的事件类型注册到 subscriptionsByEventType 中。
  2. @Subscribe 中的 priority(优先级)排序。
  3. 处理黏性事件,检查该事件类型是否有未处理的黏性事件。

事件的发送

在获取 EventBus 对象以后,可以通过 post 方法来进行对事件的提交。post 方法的源码如下所示:

public void post(Object event) {// PostingThreadState 保存有事件队列和线程状态信息PostingThreadState postingState = currentPostingThreadState.get();// 获取事件队列,并将当前事件插入事件队列List<Object> eventQueue = postingState.eventQueue;eventQueue.add(event);if (postingState.isPosting) {postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper();postingState.isPosting = true;if (postingState.canceled) {throw new EventBusException("Internal error. Abort state was not reset");}try {// 处理队列中的所有事件while (!eventQueue.isEmpty()) {postSingleEvent(eventQueue.remove(0), postingState);}} finally {postingState.isPosting = false;postingState.isMainThread = false;}}
}

首先从 PostingThreadState 对象中取出事件队列,然后再将当前的事件插入事件队列。最后将队列中的事件依次交由 postSingleEvent 方法进行处理,并移除该事件。看看 postSingleEvent 方法里做了什么。

private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {Class<?> eventClass = event.getClass();boolean subscriptionFound = false;// eventInheritance 表示是否向上查找事件的父类,默认为 trueif (eventInheritance) {List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);int countTypes = eventTypes.size();for (int h = 0; h < countTypes; h++) {Class<?> clazz = eventTypes.get(h);subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);}} else {subscriptionFound |= postSingleEventForEventType(event, postingState, eventClass);}// 找不到该事件时的异常处理if (!subscriptionFound) {if (logSubscriberExceptions) {Log.d(TAG, "No subscribers registered for event " + eventClass);}if (sendNoSubscriberEvent && eventClass != NoSubscriberEvent.class) {post(new NoSubscriberEvent(this, event));}}
}

eventinheritance 表示是否向上查找事件的父类,它的默认值为tue,可以通过 EventBusBuilder 进行配置。当 eventlnheritance为true 时,则通过 lookupAllEventTypes 找到所有的父类事件并存在 List 中,然后通过 postSingleEventForEventType 方法对事件逐一处理。

postSingleEventForEventType方法的源码如下所示:

private boolean postSingleEventForEventType(Object event, PostingThreadState postingState,Class<?> eventClass) {CopyOnWriteArrayList<Subscription> subscriptions;synchronized (this) {subscriptions = subscriptionsByEventType.get(eventClass);}if (subscriptions != null && !subscriptions.isEmpty()) {for (Subscription subscription : subscriptions) {postingState.event = event;postingState.subscription = subscription;boolean aborted = false;try {postToSubscription(subscription, event, postingState);aborted = postingState.canceled;} finally {postingState.event = null;postingState.subscription = null;postingState.canceled = false;}if (aborted) {break;}}return true;}return false;
}

同步取出该事件对应的 Subscriptions(订阅对象集合)。遍历 Subscriptions,将事件 event 和对应的 Subscription(订阅对象)传递给 postingState 并调用 postToSubscription 方法对事件进行处理。接下来看看 postToSubscription 方法。

private void postToSubscription(Subscription subscription, Object event,boolean isMainThread) {switch (subscription.subscriberMethod.threadMode) {case POSTING:invokeSubscriber(subscription, event);break;case MAIN:if (isMainThread) {invokeSubscriber(subscription, event);} else {mainThreadPoster.enqueue(subscription, event);}break;case BACKGROUND:if (!isMainThread) {backgroundPoster.enqueue(subscription, event);} else {invokeSubscriber(subscription, event);}break;case ASYNC:asyncPoster.enqueue(subscription, event);break;default:throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);}
}

取出订阅方法的 threadMode(线程模式),之后根据 threadMode 来分别处理。如果 threadMode 是 MAIN,那么,若是发送事件的线程是主线程,则通过反射直接运行订阅的方法;若其不是主线程,则需要通过 MainThreadPoster 将我们的订阅事件添加到主线线程队列中。mainThreadPoster 是一个内部类,继承自 Handler,确保在主线程中执行事件处理。

订阅者取消注册

取消注册则需要调用 unregister 方法,如下所示:

public synchronized void unregister(Object subscriber) {List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);if (subscribedTypes != null) {for (Class<?> eventType : subscribedTypes) {unsubscribeByEventType(subscriber, eventType);}typesBySubscriber.remove(subscriber);} else {Log.w(TAG, "Subscriber to unregister was not registered before: " +subscriber.getClass());}
}

我们在订阅者注册的过程中讲过 typesBySubscriber,它是一个 Map 集合。通过 subscriber 找到 subscribedTypes(事件类型集合)。将 subscriber 对应的 eventType 从 typesBySubscriber 中移除。最后遍历 subscribedTypes,并调用 unsubscribeByEventType 方法:

private void unsubscribeByEventType(Object subscriber, Class<?> eventType) {List<Subscription> subscriptions = subscriptionsByEventType.get(eventType);if (subscriptions != null) {int size = subscriptions.size();for (int i = 0; i < size; i++) {Subscription subscription = subscriptions.get(i);if (subscription.subscriber == subscriber) {subscription.active = false;subscriptions.remove(i);i--;size--;}}}
}

在上面代码注释 1 处通过 eventType 来得到对应的 Subscriptions(订阅对象集合),并在 for 循环中判断如果 Subscription(订阅对象)的 subscriber(订阅者)属性等于传进来的 subscriber,则从 Subscriptions 中移除该 Subscription。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/473531.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

uniapp 实现tabbar分类导航及滚动联动效果

思路&#xff1a;使用两个scroll-view&#xff0c;tabbar分类导航使用scrollleft移动&#xff0c;内容联动使用页面滚动onPageScroll监听滚动高度 效果图 <template><view class"content" ><view :class"[isSticky ? tab-sticky: ]">…

Flutter中的Material Theme完全指南:从入门到实战

Flutter作为一款热门的跨平台开发框架&#xff0c;其UI组件库Material Design深受开发者喜爱。本文将深入探讨Flutter Material Theme的使用&#xff0c;包括如何借助Material Theme Builder创建符合产品需求的主题风格。通过多个场景和代码实例&#xff0c;让你轻松掌握这一工…

aws中AcmClient.describeCertificate返回值中没有ResourceRecord

我有一个需求&#xff0c;就是让用户自己把自己的域名绑定我们的提供的AWS服务器。 AWS需要验证证书 上一篇文章中我用php的AcmClient中的requestCertificate方法申请到了证书。 $acmClient new AcmClient([region > us-east-1,version > 2015-12-08,credentials>[/…

Oracle 19c PDB克隆后出现Warning: PDB altered with errors受限模式处理

在进行一次19c PDB克隆过程中&#xff0c;发现克隆结束&#xff0c;在打开后出现了报错&#xff0c;PDB变成受限模式&#xff0c;以下是分析处理过程 09:25:48 SQL> alter pluggable database test1113 open instancesall; Warning: PDB altered with errors. Elapsed: 0…

【3D Slicer】的小白入门使用指南九

定量医学影像临床研究与实践 任务 定量成像教程 定量成像是从医学影像中提取定量测量的过程。 本教程基于两个定量成像的例子构建: - 形态学:缓慢生长肿瘤中的小体积变化 - 功能:鳞状细胞癌中的代谢活动 第1部分:使用变化跟踪模块测量脑膜瘤的小体积变化第2部分:使用PET标…

二、神经网络基础与搭建

神经网络基础 前言一、神经网络1.1 基本概念1.2 工作原理 二、激活函数2.1 sigmoid激活函数2.1.1 公式2.1.2 注意事项 2.2 tanh激活函数2.2.1 公式2.2.2 注意事项 2.3 ReLU激活函数2.3.1 公式2.3.2 注意事项 2.4 SoftMax激活函数2.4.1 公式2.4.2 Softmax的性质2.4.3 Softmax的应…

VMWare虚拟机安装华为欧拉系统

记录一下安装步骤&#xff1a; 1.在vmware中创建一个新的虚拟机&#xff0c;步骤和创建centos差不多 2.启动系统 具体的看下图&#xff1a; 启动虚拟机 耐心等待 等待进度条走完重启系统就完成了

如何进入python交互界面

Python交互模式有两种&#xff1a;图形化的交互模式或者命令行的交互模式。 打开步骤&#xff1a; 首先点击开始菜单。 然后在搜索栏中输入Python&#xff0c;即可看到图形化的交互模式&#xff08;IDLE&#xff08;Python 3.7 64-bit&#xff09;&#xff09;与命令行的交互…

NVR录像机汇聚管理EasyNVR多品牌NVR管理工具视频汇聚技术在智慧安防监控中的应用与优势

随着信息技术的快速发展和数字化时代的到来&#xff0c;安防监控领域也在不断进行技术创新和突破。NVR管理平台EasyNVR作为视频汇聚技术的领先者&#xff0c;凭借其强大的视频处理、汇聚与融合能力&#xff0c;展现出了在安防监控领域巨大的应用潜力和价值。本文将详细介绍Easy…

【STM32】USB 简要驱动软件架构图

STM32 USB 软件架构比较复杂&#xff0c;建议去看 UM 1734 或者 st wiki STM32 USB call graph STM32 USB Device Library files organization Reference [1]: https://wiki.stmicroelectronics.cn/stm32mcu/wiki/Introduction_to_USB_with_STM32 [2]: UM1734

高翔【自动驾驶与机器人中的SLAM技术】学习笔记(十三)图优化SLAM的本质

一、直白解释slam与图优化的结合 我从b站上学习理解的这个概念。 视频的大概位置是1个小时以后&#xff0c;在第75min到80min之间。图优化SLAM是怎么一回事。 slam本身是有运动方程的&#xff0c;也就是运动状态递推方程&#xff0c;也就是预测过程。通过t1时刻&#xff0c…

Vue2教程002:Vue指令

文章目录 2、Vue指令2.1 开发者工具2.2 v-html2.3 v-show和v-if2.4 v-else和v-else-if2.5 v-on2.5.1 内联语句2.5.2 methods 2、Vue指令 2.1 开发者工具 通过谷歌应用商店安装&#xff08;需要科学上网&#xff09;通过极简插件安装 2.2 v-html Vue会根据不同的指令&#x…

使用WebSocket技术实现Web应用中的实时数据更新

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 使用WebSocket技术实现Web应用中的实时数据更新 使用WebSocket技术实现Web应用中的实时数据更新 使用WebSocket技术实现Web应用中…

单片机学习笔记 1. 点亮一个LED灯

把基础的东西都过一下&#xff0c;用来学习记录一下。 目录 1、Keil工程 2、Keil实现代码 3、烧录程序 0、实现的功能 点亮一个LED灯 1、Keil工程 打开Keil&#xff0c;Project----New uVision Project&#xff0c;工程文件命名----OK 选择单片机类型AT89C52&#xff0c;和…

使用Web Animations API实现复杂的网页动画效果

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 使用Web Animations API实现复杂的网页动画效果 使用Web Animations API实现复杂的网页动画效果 使用Web Animations API实现复杂…

计算机组成与原理(2) basic of computer architecture

Instruction Set Architecture (ISA) 和 Hardware System Architecture (HSA) 是计算机体系结构中两个重要的层次&#xff0c;它们各自的职责和作用如下&#xff1a; Instruction Set Architecture (ISA) 定义 ISA是指令集体系结构&#xff0c;是硬件和软件之间的接口。它定义…

Python Excel XLS或XLSX转PDF详解:七大实用转换设置

目录 使用工具 Python将Excel文件转换为PDF Python将Excel文件转换为带页码的PDF Python将Excel文件转换为特定页面尺寸的PDF Python将Excel文件转换为PDF并将内容适应到一页 Python将Excel文件转换为PDF/A Python将Excel文件中的工作表转换为单独的PDF Python将Excel工…

【C++】红黑树封装map—set

1 .关联式容器 C中的map是标准模板库&#xff08;STL&#xff09;中的一种关联容器&#xff0c;它存储的是键值对&#xff08;key-value pairs&#xff09;&#xff0c;其中每个键都是唯一的。 键值对&#xff1a; 用来表示具有一一对应关系的一种结构&#xff0c;该结构中一…

系统思考—结构影响行为

最近在和一些企业领导者交流时&#xff0c;发现一个共性——创始人都非常厉害&#xff01;战略清晰、点子多、方向准&#xff0c;简直就是企业的“定海神针”。但往往问题在了下一层级&#xff1a;如何把创始人的智慧传承下去&#xff0c;甚至复制到团队里&#xff0c;这成了一…

定时器简介

TIM(Timer定时器)简介 在第一部分,我们主要讲的是定时器基本定时的功能&#xff0c;也就是定一个时间&#xff0c;然后让定时器每隔这个时间产生一个中断&#xff0c;来实现每隔一个固定时间执行一段程序的目的&#xff0c;比如你要做个时钟、秒表&#xff0c;或者使用一些程序…