当前位置: 首页 > news >正文

【EventBus】EventBus源码浅析

二、EventBus源码解析

目录

  • 1、EventBus的构造方法
  • 2、订阅者注册
    • 2.1 订阅者方法的查找过程
    • 2.2 订阅者的注册过程
    • 1. subscriptionsByEventType 映射:
    • 2. typesBySubscriber 映射:
    • 2.3 总结订阅者的注册过程
  • 3、事件的发送
    • 3.1 使用Post提交事件
    • 3.2 使用postSingleEventForEventType处理事件的分发
    • 3.2 总结事件的发送过程
  • 4、订阅者的取消

1、EventBus的构造方法

我们在使用Eventbus时首先会调用Eventbus.getDefault(),用于获取Eventbus实例,我们可以看见Eventbus.getDefault()使用了DCL模式,下面简单解释一个这个模式。

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

在这里对instance进行了两次判空处理:

  • 第一次判空的作用是为了减少不必要的同步开销

假设没有第一次检查,每次调用 getDefault 方法时都会进入同步块,即使实例已经被创建。这会导致在多线程环境中,多个线程频繁地竞争同步块,造成性能开销。

  • 第二次判空的作用是为了防止多次创建实例

假设没有第二次检查,那么在进入同步块之前,如果有多个线程同时通过了第一次检查,它们都会进入同步块,然后按顺序创建实例。这样就违反了单例模式的初衷,因为会创建多个实例。

注意这句话:instance = EventBus.defaultInstance;

这个双重检查的模式是为了保证在高并发环境下仍能正确实现懒加载的单例模式。虽然在某些情况下可能看起来多余,但是在并发编程中,确保正确性是至关重要的。

接下来看Eventbus构造方法做了什么事情:

public EventBus() {this(DEFAULT_BUILDER);
}

它使用了一个默认的构造器来构造Eventbus

private static final EventBusBuilder DEFAULT_BUILDER = new EventBusBuilder();

这里的this通过调用另一个Eventbus的构造方法使用了建造者模式来创建

  EventBus(EventBusBuilder builder) {logger = builder.getLogger();subscriptionsByEventType = new HashMap<>();typesBySubscriber = new HashMap<>();stickyEvents = new ConcurrentHashMap<>();mainThreadSupport = builder.getMainThreadSupport();mainThreadPoster = mainThreadSupport != null ? mainThreadSupport.createPoster(this) : null;backgroundPoster = new BackgroundPoster(this);asyncPoster = new AsyncPoster(this);indexCount = builder.subscriberInfoIndexes != null ? builder.subscriberInfoIndexes.size() : 0;subscriberMethodFinder = new SubscriberMethodFinder(builder.subscriberInfoIndexes,builder.strictMethodVerification, builder.ignoreGeneratedIndex);logSubscriberExceptions = builder.logSubscriberExceptions;logNoSubscriberMessages = builder.logNoSubscriberMessages;sendSubscriberExceptionEvent = builder.sendSubscriberExceptionEvent;sendNoSubscriberEvent = builder.sendNoSubscriberEvent;throwSubscriberException = builder.throwSubscriberException;eventInheritance = builder.eventInheritance;executorService = builder.executorService;}
支持
不支持
存在
不存在
流程结束
EventBus 构造函数
获取日志记录器
设置日志记录器
初始化事件类型订阅集合
初始化订阅者类型集合
初始化黏性事件集合
获取主线程支持
创建主线程Poster
主线程Poster为空
创建BackgroundPoster
创建AsyncPoster
获取订阅者信息索引数量
检查是否存在索引
设置索引数量
索引数量为0
创建SubscriberMethodFinder
设置索引信息
设置方法验证选项
设置忽略生成的索引选项
设置是否记录订阅者异常
设置是否记录无订阅者消息
设置是否发送订阅者异常事件
设置是否发送无订阅者事件
设置是否抛出订阅者异常
设置是否考虑事件继承
获取线程池
设置线程池
构造函数执行完成

2、订阅者注册

首先我们明确四个名词的关系:

2.1 订阅者方法的查找过程

当获取Eventbus以后就可以将订阅者注册到Eventbus中了,接下来看一下register方法:

public void register(Object subscriber) {if (AndroidDependenciesDetector.isAndroidSDKAvailable() && !AndroidDependenciesDetector.areAndroidComponentsAvailable()) {// Crash if the user (developer) has not imported the Android compatibility library.throw new RuntimeException("It looks like you are using EventBus on Android, " +"make sure to add the \"eventbus\" Android library to your dependencies.");}Class<?> subscriberClass = subscriber.getClass();//1、List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);//2、synchronized (this) {for (SubscriberMethod subscriberMethod : subscriberMethods) {subscribe(subscriber, subscriberMethod);}}
}

我们可以看出register做了两件事,一件是查找订阅者的订阅方法,另一件事是订阅者的注册。

在第一件事中,SubscribeMethod类中,主要用来保存Method对象,线程模式、事件类型、优先级、是否为黏性事件等等,接下来我们看一下findSubscribeMethod方法。

List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {//1、List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);if (subscriberMethods != null) {return subscriberMethods;}//2、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);return subscriberMethods;}
}

第一步首先在缓存中查找是否存在订阅方法的集合,如果找到了直接返回即可。

第二步是根据ignoreGeneratedIndex的属性选择用何种方法查找订阅集合,

ignoreGeneratedIndex的默认值是false,使用索引的方式用于更高效地查找订阅者方法。

第三步是将找的的订阅集合放入缓存(METHOD_CACHE)中,以免下次继续查找。

顺便说一下这个缓存是什么,这个缓存是一个Map:

private static final Map<Class<?>, List<SubscriberMethod>> METHOD_CACHE = new ConcurrentHashMap<>();

ConcurrentHashMap** 是 Java 中的一个特殊的 Map 实现,它提供了一种线程安全的方式来存储键值对。**它主要采用了分段锁(Segment)的机制。其核心思想是将整个数据结构分成多个独立的段,每个段上都有一个独立的锁。这样,不同的线程可以同时访问不同的段,从而提高并发性能。

📌我们在项目中经常使用EventBus单例模式获取默认的EventBus对象,也就是ignoreGeneratedIndex为fasle的情况,这种情况就是调用了索引的方法查找

此时我们分析索引查找的这个findUsingInfo()方法:

private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {FindState findState = prepareFindState();findState.initForSubscriber(subscriberClass);while (findState.clazz != null) {//1、获取订阅者信息findState.subscriberInfo = getSubscriberInfo(findState);if (findState.subscriberInfo != null) {//2、得到订阅方法的相关信息SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();for (SubscriberMethod subscriberMethod : array) {if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {findState.subscriberMethods.add(subscriberMethod);}}} else {//3、使用反射的方法查找,将信息放入findState中findUsingReflectionInSingleClass(findState);}findState.moveToSuperclass();}//对findState做回收处理并返回订阅方法的List集合return getMethodsAndRelease(findState);
}

我们对源码分析可以看出来findUsingInfo主要做了三件事。

  • 第一件事是获取了获取了订阅者的信息,FindState是这个SubscriberMethodFinder的内部类,包含了订阅者的信息。
  • 第二件事是获取了订阅方法的相关信息,获取了包含订阅方法信息的数组,然后遍历数组存入findState中。
  • 第三件事是如果订阅者信息没有正常获取那么则通过反射的方法查找,这个具体实现在后面会介绍

当完成这三件事情以后就可以返回订阅方法的list集合,在返回之前先注销了订阅者。

现在我们看一下findUsingReflectionInSingleClass方法做了什么事情。

private void findUsingReflectionInSingleClass(FindState findState) {Method[] methods;try {// This is faster than getMethods, especially when subscribers are fat classes like Activities//1、通过反射的方法获取订阅者中的所有方法methods = findState.clazz.getDeclaredMethods();} catch (Throwable th) {// Workaround for java.lang.NoClassDefFoundError, see https://github.com/greenrobot/EventBus/issues/149try {methods = findState.clazz.getMethods();} catch (LinkageError error) { // super class of NoClassDefFoundError to be a bit more broad...String msg = "Could not inspect methods of " + findState.clazz.getName();if (ignoreGeneratedIndex) {msg += ". Please consider using EventBus annotation processor to avoid reflection.";} else {msg += ". Please make this class visible to EventBus annotation processor to avoid reflection.";}throw new EventBusException(msg, error);}findState.skipSuperClasses = true;}for (Method method : methods) {int modifiers = method.getModifiers();if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {Class<?>[] parameterTypes = method.getParameterTypes();if (parameterTypes.length == 1) {Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);if (subscribeAnnotation != null) {Class<?> eventType = parameterTypes[0];//2、保存订阅方法if (findState.checkAdd(method, eventType)) {ThreadMode threadMode = subscribeAnnotation.threadMode();findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,subscribeAnnotation.priority(), subscribeAnnotation.sticky()));}}} else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {String methodName = method.getDeclaringClass().getName() + "." + method.getName();throw new EventBusException("@Subscribe method " + methodName +"must have exactly 1 parameter but has " + parameterTypes.length);}} else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {String methodName = method.getDeclaringClass().getName() + "." + method.getName();throw new EventBusException(methodName +" is a illegal @Subscribe method: must be public, non-static, and non-abstract");}}
}

在最上面的注释1中,通过反射的方法获取了订阅者的所有方法,然后根据方法的类型,参数、注解来找到订阅方法。在注释2中将找到的订阅方法保存在分findState中。

2.2 订阅者的注册过程

在查找完订阅者的订阅方法以后,对所有的订阅方法进行注册。使用流程图理解这个方法的运行过程:

subscribe 方法
集合为null
集合不为null
已注册
未注册
插入到集合
集合为null
集合不为null
处理黏性事件
事件继承
满足条件
不满足条件
不是事件继承
创建订阅对象
获取方法的事件类型
获取订阅对象的集合
创建订阅对象集合
检查是否已注册
抛出异常: EventBusException
遍历订阅对象集合
比较优先级
插入订阅对象
获取订阅者的事件类型集合
创建订阅者的事件类型集合
添加事件类型
处理黏性事件
考虑所有子类的黏性事件
检查并发布黏性事件
继续考虑下一个子类的黏性事件
检查并发布黏性事件

subscribe源码分析如下:

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {//获取方法的事件类型Class<?> eventType = subscriberMethod.eventType;//1、根据订阅者信息和订阅者方法创建一个订阅对象Subscription newSubscription = new Subscription(subscriber, subscriberMethod);//2、获取订阅对象的集合CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);if (subscriptions == null) {   //如果订阅对象集合为null则重新创建并保存subscriptions = new CopyOnWriteArrayList<>();subscriptionsByEventType.put(eventType, subscriptions);} else {if (subscriptions.contains(newSubscription)) {throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "+ eventType);}}int size = subscriptions.size();for (int i = 0; i <= size; i++) {if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {//3、更具订阅方法的优先级插入到订阅对象集合中完成注册subscriptions.add(i, newSubscription);break;}}//4、subscribedEvents(事件类型集合)List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);if (subscribedEvents == null) {subscribedEvents = new ArrayList<>();typesBySubscriber.put(subscriber, subscribedEvents);}subscribedEvents.add(eventType);if (subscriberMethod.sticky) {if (eventInheritance) {// Existing sticky events of all subclasses of eventType have to be considered.// Note: Iterating over all events may be inefficient with lots of sticky events,// thus data structure should be changed to allow a more efficient lookup// (e.g. an additional map storing sub classes of super classes: Class -> List<Class>).//如果是黏性事件的处理方法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);checkPostStickyEventToSubscription(newSubscription, stickyEvent);}}
}

我们更具四个注解对这个方法进行分析:

  • 在注释1通过subscriber(订阅者信息)和SubscriberMethod(订阅者方法)创建一个订阅对象
  • 在注释2中根据eventType(方法事件类型)获取subscriptions(订阅对象集合),如果订阅对象集合为null则重新创建集合并保存到subscriptionsByEventType
  • 在注释3中根据订阅方法的优先级插入到订阅对象集合中完成注册
  • 在注解4中根据subscriber获取subscribedEvents(事件类型集合),如果为事件类型集合null则重新创建并保存到typesBySubscriber,接下来eventType(方法事件类型)添加到subscribedEvents(事件类型集合)

如果是黏性事件则从stickyEvents事件保存队列中取出该事件类型发送给当前订阅者。

总结来说这个方法做了两件事,第一件事情是将subscriptions根据eventType封装到subscriptionsByEventType中,将subscribedEvents根据subscriber封装到typesBySubscriber中。

📌subscriptionsByEventType与typesBySubscriber的作用?

1. subscriptionsByEventType 映射:

  • 类型: Map<Class<?>, CopyOnWriteArrayList<Subscription>>
  • 作用:
    • 维护事件类型订阅者列表的映射。
    • 允许快速查找对特定事件类型感兴趣的订阅者。
  • 详细说明:
    • 这个映射的键是事件类型(Class<?> 表示)——eventType = subscriberMethod.eventType。
    • 值是 CopyOnWriteArrayList<Subscription>,每个 Subscription 包含了订阅该事件类型的订阅者的相关信息——subscriptions。
  • 用途:
    • 在事件发布时,通过这个映射可以快速找到对应事件类型的订阅者,以便通知它们处理事件。

2. typesBySubscriber 映射:

  • 类型: Map<Object, List<Class<?>>>
  • 作用:
    • 维护订阅者到其关注的事件类型列表的映射。
    • 允许快速检索特定订阅者感兴趣的事件类型。
  • 详细说明:
    • 这个映射的键是订阅者对象(Object 表示)——subscriber。
    • 值是 List<Class<?>>,包含了订阅者关注的事件类型——subscribedEvents。
  • 用途:
    • 在订阅者注册和取消注册时,通过这个映射可以快速查找订阅者关注的事件类型,以便更新订阅者的事件类型列表。

总的来说,typesBySubscriber就是用来管理订阅机制、subscriptionsByEventType用于管理事件发送机制。

2.3 总结订阅者的注册过程

3、事件的发送

3.1 使用Post提交事件

在获取EventBus对象以后,可以通过post方法对事件进行提交。可以先通过流程图了解一下Post具体是做了什么

post 方法
获取事件队列
将事件添加到队列
检查是否正在发布
检查是否取消
移除并发布单个事件
队列非空
队列为空
事件队列
当前 PostingThreadState
添加事件到队列
postingState.isPosting
设置 isMainThread 和 isPosting
最终处理-finally
postingState.canceled
抛出异常: EventBusException
处理队列中的事件
postSingleEvent
处理下一个事件

post源码如下:

public void post(Object event) {//PostingThreadState保存事件队列和线程状态信息PostingThreadState postingState = currentPostingThreadState.get();//获取事件队列,并将当前事件插入事件队列List<Object> eventQueue = postingState.eventQueue;eventQueue.add(event);if (!postingState.isPosting) {postingState.isMainThread = isMainThread();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 (logNoSubscriberMessages) {logger.log(Level.FINE, "No subscribers registered for event " + eventClass);}if (sendNoSubscriberEvent && eventClass != NoSubscriberEvent.class &&eventClass != SubscriberExceptionEvent.class) {post(new NoSubscriberEvent(this, event));}}
}

eventInheritance表示是否向上查找事件的父类,默认为true,可以通过EventBusBuider进行配置。

当eventInheritance为true时,使用lookupAllEventTypes找到父类的所有事件,将这些事件放入一个List中,然后通过postSingleEventForEventType方法对事件逐一处理。

📌为什么要向上查找事件的父类?

查找事件类型的所有父类是为了支持事件类型的继承关系。在事件总线系统中,有时候我们可能定义了一些事件类型的继承关系。这种情况下,如果某个订阅者订阅了父类的事件,那么它也应该能够接收到子类的事件。

3.2 使用postSingleEventForEventType处理事件的分发

现在我们分析一下postSingleEventForEventType方法是如何处理每一个事件的。

首先通过流程图分析方法思路:

postSingleEventForEventType 方法
subscriptions非空且非空集合
canceled为true
canceled为false
subscriptions为空或空集合
初始化subscriptions
同步块: 获取事件对应的subscriptions (订阅对象集合)
遍历subscriptions
设置postingState的event和subscription属性
调用postToSubscription方法
检查postingState的canceled属性
中断循环
循环继续
清除postingState的event,subscription和canceled属性
返回true
清除postingState的event,subscription和canceled属性
循环结束
返回false

通过postSingleEventForEventType源码分析:

private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {CopyOnWriteArrayList<Subscription> subscriptions;synchronized (this) {//1、取出事件对应的subscriptions(订阅对象集合)subscriptions = subscriptionsByEventType.get(eventClass);}if (subscriptions != null && !subscriptions.isEmpty()) {//2、遍历subscriptionsfor (Subscription subscription : subscriptions) {postingState.event = event;postingState.subscription = subscription;boolean aborted;try {postToSubscription(subscription, event, postingState.isMainThread);aborted = postingState.canceled;} finally {postingState.event = null;postingState.subscription = null;postingState.canceled = false;}if (aborted) {break;}}return true;}return false;
}

首先在注释1的同步块中取出该事件对应的Subscriptions(订阅对象集合)。然后在注释2位置遍历Subscriptions,将事件Event和Subscription(订阅对象)传递给postingState并调用postToSubscription方法对事件处理

接下来我们查看postToSubscription方法做了什么事情,这个方法做的事情就很简单了一个Switch语句处理不同线程状态,流程图如下:

postToSubscription 方法
POSTING
MAIN
MAIN_ORDERED
非空
BACKGROUND
ASYNC
结束
获取Subscription的threadMode
直接调用invokeSubscriber方法
检查是否为主线程
直接调用invokeSubscriber方法
将事件加入主线程队列
检查mainThreadPoster是否为空
将事件加入主线程队列
直接调用invokeSubscriber方法
检查是否为主线程
将事件加入后台线程队列
直接调用invokeSubscriber方法
将事件加入异步线程队列
结束处理

然后我们现在看一下源码是怎么样做的:

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 MAIN_ORDERED:if (mainThreadPoster != null) {mainThreadPoster.enqueue(subscription, event);} else {// temporary: technically not correct as poster not decoupled from subscriberinvokeSubscriber(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分别处理。如果是MAIN线程则直接通过反射运行订阅方法,如果不是主线程则需要mainThreadPoster添加到主线程队列中。

mainThreadPoster是HandlerPoster类型的,继承自Handler,通过Handler调用订阅方法切换到主线程执行。

3.2 总结事件的发送过程

invokeSubscriber 方法基本流程
postToSubscription 方法基本流程
postSingleEventForEventType 方法基本流程
postSingleEvent 方法基本流程
post 方法基本流程
调用
调用
调用
调用
结束
调用订阅方法
获取 threadMode
根据 threadMode 调用不同的处理方式
获取订阅对象集合
遍历订阅对象集合
调用 postToSubscription 方法
获取事件的 Class
处理事件继承关系
异常处理
获取 PostingThreadState
将事件加入事件队列
处理事件队列
post 方法
postSingleEvent 方法
postSingleEventForEventType 方法
postToSubscription 方法
invokeSubscriber 方法
结束处理

4、订阅者的取消

订阅者的注销需要使用到unregister方法。如下:

public synchronized void unregister(Object subscriber) {//1、通过subscriber找到事件类型集合List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);if (subscribedTypes != null) {//2、遍历subscribedTypes事件类型集合,并且调用unsubscribeByEventTypefor (Class<?> eventType : subscribedTypes) {unsubscribeByEventType(subscriber, eventType);}//3、移除对应的subscriber对应的eventTypetypesBySubscriber.remove(subscriber);} else {logger.log(Level.WARNING, "Subscriber to unregister was not registered before: " + subscriber.getClass());}
}

这里用到了在注册中使用的typesBySubscriber,这是一个Map集合。在注释1找到这个事件类型集合,然后在注释2遍历事件类型集合,调用unsubscribeByEventType。最后在注释3将subscriber对应的eventType。

我们看一下注释2的unsubscribeByEventType方法做了什么事情:

private void unsubscribeByEventType(Object subscriber, Class<?> eventType) {//1、通过eventType获取对应的subscriptions(订阅对象集合)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--;}}}
}

通过eventType获取对应的subscriptions(订阅对象集合),通过一个for移除对应的subscriber即可。

相关文章:

【EventBus】EventBus源码浅析

二、EventBus源码解析 目录 1、EventBus的构造方法2、订阅者注册 2.1 订阅者方法的查找过程2.2 订阅者的注册过程1. subscriptionsByEventType 映射&#xff1a;2. typesBySubscriber 映射&#xff1a;2.3 总结订阅者的注册过程 3、事件的发送 3.1 使用Post提交事件3.2 使用p…...

Buck电源设计常见的一些问题(二)MOS管炸机问题

MOS管炸机问题 1.概述2.MOS管的相关参数3.过电压失效4.过电流失效5.静电放电和热失效1.概述 在我们做电源产品或者电机控制器时候,经常会坏MOS管。我相信90%以上的硬件工程师在职场生涯中都会遇到这类问题。然而这类问题也总是让人防不胜防。经常我们都会开玩笑的说,没烧过管…...

Javascript高频面试题

系列文章目录 文章目录 系列文章目录前言1.JavaScript常见数据类型null 和 undefind区别symbol&#xff08;ES6新增&#xff09;、bigInt&#xff08;ES10新增&#xff09; 2.JavaScript判断数据类型的方式3. 和 区别&#xff0c;分别在什么情况使用&#xff1f;4.变量声明 va…...

锁--07_2---- index merge(索引合并)引起的死锁

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 案例分析生产背景死锁日志表结构执行计划 EXPLAN为什么会用 index_merge&#xff08;索引合并&#xff09;为什么用了 index_merge就死锁了解决方案注&#xff1a;M…...

后端打印不了trace等级的日志?-SpringBoot日志打印-Slf4j

在调用log变量的方法来输出日志时&#xff0c;有以上5个级别对应的方法&#xff0c;从不太重要&#xff0c;到非常重要 调用不同的方法&#xff0c;就会输出不同级别的日志。 trace&#xff1a;跟踪信息debug&#xff1a;调试信息info&#xff1a;一般信息warn&#xff1a;警告…...

声明式编程Declarative Programming

接下来要介绍第五种编程范式 -- 声明式编程。分别从它的优缺点、案例分析和适用的编程语言这三个方面来介绍这个歌编程范式。 声明式编程是一种编程范式&#xff0c;其核心思想是通过描述问题的性质和约束&#xff0c;而不是通过描述解决问题的步骤来进行编程。这与命令式编程…...

人工智能与天文:技术前沿与未来展望

人工智能与天文&#xff1a;技术前沿与未来展望 一、引言 随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;在各个领域的应用越来越广泛。在天文领域&#xff0c;AI也发挥着越来越重要的作用。本文将探讨人工智能与天文学的结合&#xff0c;以及这种结合带…...

JeecgBoot 框架升级至 Spring Boot3 的实战步骤

JeecgBoot 框架升级 Spring Boot 3.1.5 步骤 JEECG官方推出SpringBoot3分支&#xff1a;https://github.com/jeecgboot/jeecg-boot/tree/springboot3 本次更新由于属于破坏式更新&#xff0c;有几个生态内的组件&#xff0c;无法进行找到平替或无法升级&#xff0c;目前尚不完…...

论文阅读——Semantic-SAM

Semantic-SAM可以做什么&#xff1a; 整合了七个数据集&#xff1a; 一般的分割数据集&#xff0c;目标级别分割数据集&#xff1a;MSCOCO, Objects365, ADE20k 部分分割数据集&#xff1a;PASCAL Part, PACO, PartImagenet, and SA-1B The datasets are SA-1B, COCO panopt…...

gitlab下载,离线安装

目录 1.下载 2.安装 3.配置 4.启动 5.登录 参考&#xff1a; 1.下载 根据服务器操作系统版本&#xff0c;下载对应的RPM包。 gitlab官网&#xff1a; The DevSecOps Platform | GitLab rpm包官网下载地址: gitlab/gitlab-ce - Results in gitlab/gitlab-ce 国内镜像地…...

【SpringBoot篇】Interceptor拦截器 | 拦截器和过滤器的区别

文章目录 &#x1f339;概念⭐作用 &#x1f384;快速入门⭐入门案例代码实现 &#x1f6f8;拦截路径&#x1f354;拦截器interceptor和过滤器filter的区别&#x1f386;登录校验 &#x1f339;概念 拦截器&#xff08;Interceptor&#xff09;是一种软件设计模式&#xff0c;…...

conan入门(三十六):在set_version方法中从pom.xml中读取版本号实现动态版本定义

一般情况下&#xff0c;我们通过self.version字段定义conan 包的版本号如下&#xff1a; class PkgConan(ConanFile):name "pkg"version "1.7.3"因为版本号是写死的&#xff0c;所以这种方式有局限性&#xff1a; 比如我的java项目中版本号是在pom.xml中…...

为什么 GAN 不好训练

为什么 GAN 不好训练&#xff1f;先看 GAN 的损失&#xff1a; 当生成器固定时&#xff0c;堆D(x)求导&#xff0c;推理得到&#xff08;加号右边先对log求导&#xff0c;再对负项求导&#xff09; 然后在面对最优Discriminator时&#xff0c;Generator的优化目标就变成了&…...

select、poll、epoll 区别有哪些

文章目录 select、poll、epoll 区别有哪些&#xff1f;select&#xff1a;poll&#xff1a;epoll&#xff1a; select、poll、epoll 区别有哪些&#xff1f; select&#xff1a; 它仅仅知道了&#xff0c;有 I/O 事件发生了&#xff0c;却并不知道是哪那几个流&#xff08;可…...

大模型下开源文档解析工具总结及技术思考

1 基于文档解析工具的方法 pdf解析工具 导图一览&#xff1a; PyPDF2提取txt&#xff1a; import PyPDF2 def extract_text_from_pdf(pdf_path):with open(pdf_path, rb) as file:pdf_reader PyPDF2.PdfFileReader(file)num_pages pdf_reader.numPagestext ""f…...

【华为数据之道学习笔记】5-4 数据入湖方式

数据入湖遵循华为信息架构&#xff0c;以逻辑数据实体为粒度入湖&#xff0c;逻辑数据实体在首次入湖时应该考虑信息的完整性。原则上&#xff0c;一个逻辑数据实体的所有属性应该一次性进湖&#xff0c;避免一个逻辑实体多次入湖&#xff0c;增加入湖工作量。 数据入湖的方式…...

Vue3-03-reactive() 响应式基本使用

reactive() 的简介 reactive() 是vue3 中进行响应式状态声明的另一种方式&#xff1b; 但是&#xff0c;它只能声明 【对象类型】的响应式变量&#xff0c;【不支持声明基本数据类型】。reactive() 与 ref() 一样&#xff0c;都是深度响应式的&#xff0c;即对象嵌套属性发生了…...

OpenAI开源超级对齐方法:用GPT-2,监督、微调GPT-4

12月15日&#xff0c;OpenAI在官网公布了最新研究论文和开源项目——如何用小模型监督大模型&#xff0c;实现更好的新型对齐方法。 目前&#xff0c;大模型的主流对齐方法是RLHF&#xff08;人类反馈强化学习&#xff09;。但随着大模型朝着多模态、AGI发展&#xff0c;神经元…...

TeeChart.NET 2023.11.17 Crack

.NET 的 TeeChart 图表控件提供了一个出色的通用组件套件&#xff0c;可满足无数的图表需求&#xff0c;也针对重要的垂直领域&#xff0c;例如金融、科学和统计领域。 数据可视化 数十种完全可定制的交互式图表类型、地图和仪表指示器&#xff0c;以及完整的功能集&#xff0c…...

计算机网络常见的缩写

计算机网络常见缩写 通讯控制处理机&#xff08;Communication Control Processor&#xff09;CCP 前端处理机&#xff08;Front End Processor&#xff09;FEP 开放系统互连参考模型 OSI/RM 开放数据库连接&#xff08;Open Database Connectivity&#xff09;ODBC 网络操作系…...

MongoDB学习和应用(高效的非关系型数据库)

一丶 MongoDB简介 对于社交类软件的功能&#xff0c;我们需要对它的功能特点进行分析&#xff1a; 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具&#xff1a; mysql&#xff1a;关系型数据库&am…...

8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂

蛋白质结合剂&#xff08;如抗体、抑制肽&#xff09;在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上&#xff0c;高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术&#xff0c;但这类方法普遍面临资源消耗巨大、研发周期冗长…...

从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路

进入2025年以来&#xff0c;尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断&#xff0c;但全球市场热度依然高涨&#xff0c;入局者持续增加。 以国内市场为例&#xff0c;天眼查专业版数据显示&#xff0c;截至5月底&#xff0c;我国现存在业、存续状态的机器人相关企…...

CentOS下的分布式内存计算Spark环境部署

一、Spark 核心架构与应用场景 1.1 分布式计算引擎的核心优势 Spark 是基于内存的分布式计算框架&#xff0c;相比 MapReduce 具有以下核心优势&#xff1a; 内存计算&#xff1a;数据可常驻内存&#xff0c;迭代计算性能提升 10-100 倍&#xff08;文档段落&#xff1a;3-79…...

[ICLR 2022]How Much Can CLIP Benefit Vision-and-Language Tasks?

论文网址&#xff1a;pdf 英文是纯手打的&#xff01;论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误&#xff0c;若有发现欢迎评论指正&#xff01;文章偏向于笔记&#xff0c;谨慎食用 目录 1. 心得 2. 论文逐段精读 2.1. Abstract 2…...

linux arm系统烧录

1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 &#xff08;忘了有没有这步了 估计有&#xff09; 刷机程序 和 镜像 就不提供了。要刷的时…...

Frozen-Flask :将 Flask 应用“冻结”为静态文件

Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是&#xff1a;将一个 Flask Web 应用生成成纯静态 HTML 文件&#xff0c;从而可以部署到静态网站托管服务上&#xff0c;如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...

Keil 中设置 STM32 Flash 和 RAM 地址详解

文章目录 Keil 中设置 STM32 Flash 和 RAM 地址详解一、Flash 和 RAM 配置界面(Target 选项卡)1. IROM1(用于配置 Flash)2. IRAM1(用于配置 RAM)二、链接器设置界面(Linker 选项卡)1. 勾选“Use Memory Layout from Target Dialog”2. 查看链接器参数(如果没有勾选上面…...

新能源汽车智慧充电桩管理方案:新能源充电桩散热问题及消防安全监管方案

随着新能源汽车的快速普及&#xff0c;充电桩作为核心配套设施&#xff0c;其安全性与可靠性备受关注。然而&#xff0c;在高温、高负荷运行环境下&#xff0c;充电桩的散热问题与消防安全隐患日益凸显&#xff0c;成为制约行业发展的关键瓶颈。 如何通过智慧化管理手段优化散…...

用docker来安装部署freeswitch记录

今天刚才测试一个callcenter的项目&#xff0c;所以尝试安装freeswitch 1、使用轩辕镜像 - 中国开发者首选的专业 Docker 镜像加速服务平台 编辑下面/etc/docker/daemon.json文件为 {"registry-mirrors": ["https://docker.xuanyuan.me"] }同时可以进入轩…...