手写 EventBus:从零到一实现自己的事件总线库

简介:在本文中,我们将详细介绍如何从头开始实现一个轻量级的 EventBus 库。我们将以 XEventBus 为例,阐述整个实现过程,以及在实现过程中遇到的关键问题和解决方法。
XEventBus 地址:https://github.com/LucasXu01/XBus

一 引言

什么是 EventBus?

EventBus 是一个基于发布/订阅模式的事件总线库,用于在组件之间进行解耦的异步通信。它允许组件(如 Activity、Fragment、Service 等)之间相互发送和接收事件,而无需显式地引用和调用彼此的实例。这样,组件之间的耦合性降低,代码更易于维护和扩展。

XBus 简介及设计目标

为了学习和掌握 EventBus 的实现原理和关键技术,我们将通过本文带领大家从零开始实现一个自己的轻量级的事件总线库 XBus。

XBus 是一个轻量级的事件总线库,旨在提供简单、高效、易用的事件通信机制。我们的设计目标如下:

  1. 易用性:XBus 的 API 设计简洁明了,易于集成和使用。
  2. 高性能:提供过反射和注解处理器(APT)生成订阅者方法索引,后者可提高事件查找和调用的性能。
  3. 可扩展性:XEventBus 具有良好的可扩展性,可以根据需求添加更多功能,如优先级控制、延时处理等。

二 XBus 设计与实现

在这一节中,我们将结合代码详细介绍 XEventBus 的设计与实现。

XEventBus 的实现思路包括以下几个方面:

  1. 使用 XEventBus 类作为事件总线的核心,管理订阅者与订阅方法之间的关系。
  2. 通过 SubscribedMethod 类封装订阅者方法的信息,包括所在类、参数类型、线程模式、优先级和方法名。
  3. 使用 Subscription 类表示订阅关系,包含订阅者对象和订阅方法,方便在事件发布时找到对应的订阅者方法并执行。
  4. 利用subscriptionsByEventType和typesBySubscriber两个集合分别存储EventType类与所有注册方法、Subscriber和其注册的所有event的,并基于这两个集合进行事件订阅的查询和订阅者的注册反注册。
  5. 利用注解处理器(APT)在编译期间生成订阅者方法的查找和调用逻辑,避免运行时使用反射,提高性能。

通过这种设计,XEventBus 实现了一个简单、高效、易用的事件总线,可以方便地在不同组件之间进行事件通信。

订阅事件和订阅者:

手写 EventBus:从零到一实现自己的事件总线库_第1张图片 手写 EventBus:从零到一实现自己的事件总线库_第2张图片

subsciption的结构

手写 EventBus:从零到一实现自己的事件总线库_第3张图片

XEventBus

XEventBus 类是该库的核心类,它维护了一个 Map, List> 结构,用于存储订阅者和它们对应的订阅方法列表。通过 register 和 unregister 方法,实现订阅和取消订阅的功能。

public class XEventBus {
    private static final Map<Class<?>, List<Subscription>> subscriptionsByEventType = new ConcurrentHashMap<>();

    public void register(Object subscriber) {
        //...
    }

    public void unregister(Object subscriber) {
        //...
    }

    public void post(Object event) {
        //...
    }
}

SubscribedMethod

SubscribedMethod 类封装了订阅者方法的相关信息,包括所在类、参数类型、线程模式、优先级和方法名。在事件发布时,EventBus 将根据这些信息找到并调用订阅者方法。

public class SubscribedMethod {
    private final Class<?> subscriberClass;
    private final Class<?> eventType;
    private final ThreadMode threadMode;
    private final int priority;
    private final String methodName;

    //... constructor and getters
}

Subscription

Subscription 类表示一个订阅关系,包括订阅者对象(Subscriber)和订阅方法(SubscribedMethod)。当事件发布时,EventBus 会遍历所有的 Subscription,根据事件类型找到匹配的订阅者方法并执行。

public class Subscription {
    private final Object subscriber;
    private final SubscribedMethod subscribedMethod;

    //... constructor and getters
}

Subscriber

Subscriber 类表示订阅者对象,包含了订阅者实例和其订阅的事件方法。Subscriber 类主要用于在 EventBus 内部管理订阅关系。

public class Subscriber {
    private final Object subscriberInstance;
    private final List<SubscribedMethod> subscribedMethods;

    //... constructor and getters
}

MethodHandle

MethodHandle 接口用于处理订阅者方法的查找和调用。通过注解处理器(APT)生成具体的实现类,实现在编译期间就能获取订阅者方法信息,提高运行时性能。

public interface MethodHandle {
    List<SubscribedMethod> getAllSubscribedMethods(Object subscriber);

    void invokeMethod(Subscription subscription, Object event);
}

在 XEventBus 的实现中,注解处理器 MyEventBusAnnotationProcessor 负责在编译期间查找所有使用 @Subscribe 注解的方法,并生成 AptMethodFinder 类,实现 MethodHandle 接口。这样,在运行时,XEventBus 无需使用反射,可以直接调用订阅者方法,提高性能。

// MyEventBusAnnotationProcessor
public class MyEventBusAnnotationProcessor extends AbstractProcessor {
    //... processing logic
}

// Generated AptMethodFinder
public class AptMethodFinder implements MethodHandle {
    //... implementation
}

通过上述代码实现,XEventBus 可以在不同组件之间进行高效的事件通信。在编译期间,MyEventBusAnnotationProcessor 注解处理器会自动生成 AptMethodFinder 类,实现了 MethodHandle 接口。这使得 XEventBus 在运行时能够直接调用订阅者方法,而不需要使用反射,从而提高了性能。

三 实现订阅者方法查找与注册

为了实现 XEventBus 中订阅者方法的查找与注册,我们需要使用注解处理器(APT)在编译期间生成相应的代码。这将避免在运行时使用反射,从而提高性能。

使用注解处理器(APT)实现订阅者方法查找

我们在 MyEventBusAnnotationProcessor 注解处理器中遍历所有使用 @Subscribe 注解的方法,将它们按照所属类进行分类。接着,为每个类生成一个查找订阅者方法的静态方法,该方法返回一个包含所有订阅者方法信息的 SubscribedMethod 列表。

for (Element element : elements) {
    // 省略代码...

    CreateMethod createMethod = mCachedCreateMethod.get(qualifiedName);
    if (createMethod == null) {
        createMethod = new CreateMethod(typeElement);
        mCachedCreateMethod.put(qualifiedName, createMethod);
    }

    // 省略代码...
}

注册订阅者方法到 EventBus 中

在 EventBus 类中,我们提供了 register() 方法,用于将订阅者方法注册到事件总线中。注册过程中,EventBus 会调用 AptMethodFinder 类中的 getAllSubscribedMethods() 方法,获取订阅者类中所有订阅者方法的信息,并将它们封装成 Subscription 对象,存储在事件总线的内部数据结构中。

public void register(Object subscriber) {
    List<SubscribedMethod> subscribedMethods = methodHandle.getAllSubscribedMethods(subscriber);
    // 省略代码...
}

生成订阅者方法的索引和对应的方法调用

在 MyEventBusAnnotationProcessor 注解处理器中,我们为每个订阅者类生成一个查找订阅者方法的静态方法,该方法返回一个包含所有订阅者方法信息的 SubscribedMethod 列表。同时,我们还需要为 AptMethodFinder 类生成一个 invokeMethod() 方法,用于执行具体的订阅者方法。

这样,在 EventBus 发布事件时,可以直接通过 AptMethodFinder 的 invokeMethod() 方法调用订阅者方法,而无需使用反射。

通过以上步骤,我们实现了订阅者方法的查找与注册,从而使 XEventBus 能够在不同组件之间高效地传递事件。

四 实现事件发布与订阅者方法调用

在 XEventBus 中,我们实现了事件的发布与订阅者方法的调用。接下来,我们将讨论这些关键功能的实现细节。

事件发布

在 EventBus 类中,我们提供了 post() 方法,用于发布事件。当调用此方法时,EventBus 将遍历内部数据结构中的所有订阅者,并根据事件类型找到对应的订阅者方法。

public void post(Object event) {
    List<Subscription> subscriptions = mEventTypeSubscriptions.get(event.getClass());
    if (subscriptions != null) {
        // 省略代码...
    }
}

通过 MethodHandle 接口查找订阅者方法

为了避免使用反射调用订阅者方法,我们使用 MethodHandle 接口,它允许我们在编译期生成的代码中直接调用订阅者方法。在 EventBus 类中,我们将 MethodHandle 作为一个成员变量,并在构造函数中初始化。

public EventBus() {
    this.methodHandle = new AptMethodFinder();
}

调用订阅者方法

当我们找到订阅者方法时,我们使用 MethodHandle 接口的 invokeMethod() 方法来调用它。这样,我们可以避免使用反射,从而提高性能。

private void invokeSubscriber(Subscription subscription, Object event) {
    try {
        methodHandle.invokeMethod(subscription.subscriber, subscription.subscribedMethod, event);
    } catch (Exception e) {
        // 省略代码...
    }
}

支持不同线程模型的订阅者方法调用

XEventBus 支持多种线程模型,如主线程、后台线程等。我们可以在 @Subscribe 注解中指定线程模型。为了实现这一功能,我们在 Subscription 类中保存订阅者方法的线程模型,并在调用订阅者方法时根据线程模型执行相应的操作。

private void invokeSubscriber(Subscription subscription, Object event) {
    ThreadMode threadMode = subscription.subscribedMethod.threadMode;
    switch (threadMode) {
        case MAIN:
            // 省略代码...
            break;
        case BACKGROUND:
            // 省略代码...
            break;
        // 更多线程模型...
    }
}

通过以上实现,我们使 XEventBus 能够根据订阅者方法的线程模型在不同线程中调用订阅者方法,从而实现了灵活的事件发布与订阅。

五 实现订阅者方法的反注册

为了避免内存泄漏和不必要的事件接收,我们需要提供反注册功能,以便在不再需要接收事件的时候移除订阅者方法。以下是反注册功能的实现细节。

移除订阅者方法

我们在 EventBus 类中提供了一个名为 unregister() 的方法,用于移除订阅者方法。在调用此方法时,我们将遍历订阅者方法并从内部数据结构中删除它们。

public void unregister(Object subscriber) {
    List<Class<?>> subscribedEventTypes = mSubscriberEventTypes.get(subscriber);
    if (subscribedEventTypes != null) {
        for (Class<?> eventType : subscribedEventTypes) {
            removeSubscriber(subscriber, eventType);
        }
    }
}

private void removeSubscriber(Object subscriber, Class<?> eventType) {
    List<Subscription> subscriptions = mEventTypeSubscriptions.get(eventType);
    if (subscriptions != null) {
        // 省略代码...
    }
}

清理资源

在反注册订阅者方法时,我们需要确保清理相关的资源。首先,我们从 mSubscriberEventTypes 中删除订阅者。然后,我们检查事件类型是否还有其他订阅者,如果没有,则从 mEventTypeSubscriptions 中删除该事件类型。

private void removeSubscriber(Object subscriber, Class<?> eventType) {
    List<Subscription> subscriptions = mEventTypeSubscriptions.get(eventType);
    if (subscriptions != null) {
        Iterator<Subscription> iterator = subscriptions.iterator();
        while (iterator.hasNext()) {
            Subscription subscription = iterator.next();
            if (subscription.subscriber == subscriber) {
                iterator.remove();
            }
        }
        
        // 清理资源
        if (subscriptions.isEmpty()) {
            mEventTypeSubscriptions.remove(eventType);
        }
    }
    mSubscriberEventTypes.remove(subscriber);
}

通过实现反注册功能,我们使得 XEventBus 可以灵活地处理订阅者的生命周期,避免了潜在的内存泄漏问题。同时,这也有助于提高事件分发的性能,因为我们不再需要为不再关心的事件处理订阅者方法。

六 XEventBus 的优化与拓展

为了提高 XEventBus 的性能和灵活性,我们可以考虑以下几个方面:

增加缓存策略,提高性能

在 XEventBus 中,我们可以使用缓存策略来减少重复的订阅者方法查找和事件发布。例如,我们可以使用一个 HashMap 来存储已经注册过的订阅者方法,以便在需要时快速查找。同时,在事件发布时,我们可以对订阅者方法的调用结果进行缓存,避免重复计算。

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

支持优先级和延时处理的订阅者方法

在某些场景下,我们可能需要对订阅者方法的执行顺序进行控制。为此,我们可以为订阅者方法添加优先级属性。通过在 @Subscribe 注解中添加 priority 属性,我们可以实现订阅者方法的优先级控制。

@Subscribe(priority = 1)
public void onEvent(Event event) {
    // ...
}

此外,我们还可以通过添加 delay 属性来实现延时处理。例如,当 delay 设为 1000 时,订阅者方法将在事件发布后的 1000 毫秒后执行。

@Subscribe(delay = 1000)
public void onEvent(Event event) {
    // ...
}

扩展更多功能,如事件粘性等

除了以上提到的优化,我们还可以为 XEventBus 添加更多功能,如支持粘性事件。粘性事件是指在订阅者注册后,仍然可以接收到在注册之前发布的事件。我们可以通过在 @Subscribe 注解中添加一个 sticky 属性来实现这个功能。

@Subscribe(sticky = true)
public void onStickyEvent(Event event) {
    // ...
}

在实现粘性事件时,我们需要在 XEventBus 中维护一个粘性事件的集合。当订阅者注册时,如果其订阅方法设置了 sticky 属性,那么将会收到集合中保存的对应类型的粘性事件。

这样,通过对 XEventBus 的优化与拓展,我们可以实现一个功能更加丰富、性能更优的事件总线。

七 XBus 实战演示

XBus的开源地址。关于XBus的具体使用可以参考。简述如下。

快速使用:

根build.gradle中添加仓库来源地址

allprojects {
    repositories {
        ...
        maven {
            url 'https://lucasxu01.github.io/maven-repository/'
        }
        
    }
}

app项目级别build.gradle中添加依赖

    implementation 'com.lucas:xbus:1.0.0'
    implementation 'com.lucas:xbus-annotations:1.0.0'
    annotationProcessor 'com.lucas:xbus-apt-processor:1.0.0'

Antivity的onCreate方法中注册bus:

XEventBus.getDefault().register(MainActivity.this);

定义一个自己的Event事件:

public class WorkEvent {
    private int num;

    public WorkEvent(int num) {
        this.num = num;
    }

    public int getNum() {
        return num;
    }
}

对应的Activity中注册方法

    @Subscribe(priority = 1)
    public void onEvent(final WorkEvent event) {
         Log.e(TAG, "onEvent: " + " Thread, WorkEvent num=" + event.getNum());
    }

发送事件进行调用

XEventBus.getDefault().post(new WorkEvent(5))

其他功能

若想使用apt方式代替注解,可在bus注册时这样注册:

AptMethodFinder aptMethodFinder = new AptMethodFinder();
XEventBus.builder().setMethodHandle(aptMethodFinder).build().register(this);

八 Android中消息总线的其他实现方式

简单介绍一下其他技术方案实现的技术总线。

基于RxJava

RxBus是基于RxJava实现的,需要额外导入RxJava RxAndroid等库,因此库体积还是较大的。使用RxBus你得了解rxjava的原理,对于不使用rxjava的项目来说,成本太高了,而且容易内存泄露。想了解具体实现细节的可参考《使用RxJava实现的EventBus》。

使用起来大概是如下:

RxBus.getInstance().toObservable(MsgEvent.class).subscribe(new Observer<MsgEvent>() {
            @Override
            public void onSubscribe(Disposable d) {
                
            }

            @Override
            public void onNext(MsgEvent msgEvent) {
                //处理事件
            }

            @Override
            public void onError(Throwable e) {
                  
            }

            @Override
            public void onComplete() {

            }
        });

        
RxBus.getInstance().post(new MsgEvent("Java"));

基于ASM

ASM(Abstract Syntax Machine)是一个Java字节码操作和分析框架。ASM用于动态生成、转换或者操作Java字节码。在Android中,ASM通常用于性能优化、代码注入、AOP(面向切面编程)等场景。然而,它并不是一个事件总线的典型实现方式。基于ASM实现的时间总线有:BusUtils;

如果你确实想使用ASM实现事件总线,可以尝试以下方法:

  1. 使用ASM扫描已编译的Java字节码,识别出包含特定注解(例如@Subscribe)的类和方法。
  2. 对于扫描到的类和方法,使用ASM在运行时动态修改字节码,注入事件总线的逻辑代码,例如注册、注销和发送事件。
  3. 在事件发送方,通过反射或者其他方式调用相应的方法来触发事件。

这种实现方式在实践中可能非常复杂,容易出现问题,而且可能导致性能和兼容性问题。因此,我们建议在实现事件总线时,优先考虑使用更为成熟、简便的方案,如EventBus、LiveData和ViewModel或RxJava,这些方法在通信和解耦方面表现出色,更为简便和高效,易于使用。

基于LiveData

LiveData是Android Architecture Components库的一部分,它是一个可观察的数据持有类,能够在数据发生变化时通知订阅者。使用LiveData实现事件总线可以确保通信在主线程上执行,而且与应用程序的生命周期紧密结合,从而避免内存泄漏。

以下是实现基于LiveData的Bus的简要步骤:

  1. 创建一个单例的Bus类,用于存放多个LiveData对象,每个LiveData对象负责一种类型的事件。
  2. 在Bus类中,为每种类型的事件提供注册和发送方法。注册方法用于订阅事件,发送方法用于发布事件。
  3. 在需要接收事件的组件(如Activity、Fragment等)中,调用Bus类的注册方法来订阅事件。订阅时,需要传入一个Observer对象,用于处理收到的事件。
  4. 当事件发生时,调用Bus类的发送方法发布事件。订阅者会收到通知,并通过Observer对象处理事件。
  5. 在组件的生命周期结束时,LiveData会自动取消订阅,无需手动注销订阅者。

基于LiveData的事件总线具有以下优点:

  • 生命周期感知:LiveData与组件生命周期紧密结合,可以在组件销毁时自动取消订阅,避免内存泄漏。
  • 线程安全:LiveData确保事件通知在主线程上执行,避免了多线程同步的问题。
  • 简单易用:LiveData的API简单易用,与Android架构组件库兼容良好。

基于Flow

Flow是Kotlin协程库的一部分,它提供了一种声明式、响应式的编程模型,能够更方便地处理异步事件流。使用Flow实现事件总线可以确保通信在主线程上执行,同时提供了更丰富的操作符和组合方式,能够处理更复杂的场景。

以下是实现基于Flow的Bus的简要步骤:

  1. 创建一个单例的Bus类,用于存放多个Flow对象,每个Flow对象负责一种类型的事件。
  2. 在Bus类中,为每种类型的事件提供注册和发送方法。注册方法用于订阅事件,发送方法用于发布事件。
  3. 在需要接收事件的组件(如Activity、Fragment等)中,调用Bus类的注册方法来订阅事件。订阅时,需要传入一个lambda表达式,用于处理收到的事件。
  4. 当事件发生时,调用Bus类的发送方法发布事件。订阅者会收到通知,并通过lambda表达式处理事件。
  5. 在组件的生命周期结束时,可以通过协程取消订阅,避免内存泄漏。

基于Flow的事件总线具有以下优点:

  • 声明式编程模型:Flow提供了一种声明式、响应式的编程模型,能够更方便地处理异步事件流,从而提供更丰富的操作符和组合方式,能够处理更复杂的场景。
  • 线程安全:Flow确保事件通知在主线程上执行,避免了多线程同步的问题。
  • 协程支持:Flow与Kotlin协程库紧密结合,可以方便地在协程中使用。

九 总结

本文实现过程的总结

本文从设计到实现,详细介绍了一个简单的 EventBus 系统 - XEventBus。我们首先讨论了 EventBus 的基本概念、应用场景和优势,并阐述了 XEventBus 的设计目标。接着,我们通过分析 XEventBus 的核心组件和实现细节,介绍了订阅者方法查找、注册、事件发布和订阅者方法调用等核心功能。最后,我们还探讨了 XEventBus 的优化与拓展,包括缓存策略、优先级控制、延时处理和粘性事件等。

EventBus 的适用场景与局限性

EventBus 主要适用于组件之间的松耦合通信,特别是在 Android 应用开发中,它可以简化 Activity、Fragment、Service 之间的消息传递。然而,EventBus 也有一定的局限性,例如:

  • 对于大型项目,过多的事件订阅可能导致代码难以维护。
  • EventBus 无法保证事件的传递顺序,有时可能需要手动处理事件的执行顺序。
  • EventBus 在跨进程通信时存在局限性,需要额外的技术支持。

对 EventBus 未来发展的展望

尽管 EventBus 具有一定的局限性,但在适当的场景下,它仍然是一个非常有用的工具。随着技术的发展,我们可以期待 EventBus 的功能将不断完善,例如:

  • 提供更强大的事件过滤和路由机制,以便更精确地控制事件的传递。
  • 支持跨进程通信,使其适用于更广泛的场景。
  • 增强事件调试和追踪能力,帮助开发者更容易地定位问题。

总之,本文通过实现 XEventBus,希望能为读者提供一个 EventBus 的入门示例,以便更好地理解和应用 EventBus 这一有用的工具。

参考文章

https://www.jianshu.com/p/a5e89082d1b9

https://blog.csdn.net/u011213403/article/details/121267330

https://juejin.cn/post/6844903896700157966#heading-15

https://www.jianshu.com/p/2a8f9ac32e13

你可能感兴趣的:(java,android)