Handwritten EventBus: Implement your own event bus library from zero to one

Summary: In this article, we'll detail how to implement a lightweight EventBus library from scratch. We will take XEventBus as an example to explain the whole implementation process, as well as the key problems and solutions encountered in the implementation process.
XEventBus address: https://github.com/LucasXu01/XBus

I. Introduction

What is EventBus?

EventBus is a publish/subscribe-based event bus library for decoupled asynchronous communication between components. It allows components (such as Activity, Fragment, Service, etc.) to send and receive events between each other without explicitly referencing and calling each other's instances. In this way, the coupling between components is reduced, and the code is easier to maintain and expand.

XBus Introduction and Design Goals

In order to learn and master the implementation principles and key technologies of EventBus, we will lead you to implement a lightweight event bus library XBus from scratch through this article.

XBus is a lightweight event bus library designed to provide a simple, efficient, and easy-to-use event communication mechanism. Our design goals are as follows:

  1. Ease of use: The API design of XBus is concise and clear, which is easy to integrate and use.
  2. High Performance: Provides reflection and Annotation Processor (APT) generated subscriber method indexing, which improves event lookup and invocation performance.
  3. Scalability: XEventBus has good scalability, and more functions can be added according to requirements, such as priority control, delay processing, etc.

Two XBus design and implementation

In this section, we will introduce the design and implementation of XEventBus in detail with the code.

The implementation ideas of XEventBus include the following aspects:

  1. Use the XEventBus class as the core of the event bus to manage the relationship between subscribers and subscription methods.
  2. The information of the subscriber method is encapsulated through the SubscribedMethod class, including the class, parameter type, thread mode, priority and method name.
  3. Use the Subscription class to represent the subscription relationship, including the subscriber object and subscription method, so that it is convenient to find and execute the corresponding subscriber method when the event is published.
  4. Use two collections of subscriptionsByEventType and typesBySubscriber to store the EventType class and all registration methods, Subscriber and all events registered by it, and perform event subscription query and subscriber registration and de-registration based on these two collections.
  5. Use the annotation processor (APT) to generate the lookup and call logic of the subscriber method during compilation, avoiding the use of reflection at runtime and improving performance.

Through this design, XEventBus implements a simple, efficient, and easy-to-use event bus, which can facilitate event communication between different components.

Subscribe to events and subscribers:

subscriptionsByEventType image-20230505171338467

The structure of subscription

image-20230505165450269

XEventBus

The XEventBus class is the core class of the library, which maintains a Map<Class<?>, List<Subscription>>structure for storing the list of subscribers and their corresponding subscription methods. Through the register and unregister methods, the functions of subscription and unsubscription are realized.

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

The SubscribedMethod class encapsulates information about the subscriber method, including its class, parameter type, thread mode, priority, and method name. When an event is published, EventBus will find and call the subscriber method based on this information.

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

The Subscription class represents a subscription relationship, including a subscriber object (Subscriber) and a subscription method (SubscribedMethod). When an event is published, EventBus will traverse all Subscriptions, find the matching subscriber method according to the event type and execute it.

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

    //... constructor and getters
}

Subscriber

The Subscriber class represents the subscriber object, including the subscriber instance and the event method it subscribes to. The Subscriber class is mainly used to manage the subscription relationship inside the EventBus.

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

    //... constructor and getters
}

MethodHandle

The MethodHandle interface is used to handle the lookup and invocation of subscriber methods. The specific implementation class is generated through the annotation processor (APT), so that the subscriber method information can be obtained during compilation, and the runtime performance is improved.

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

    void invokeMethod(Subscription subscription, Object event);
}

In the implementation of XEventBus, the annotation processor MyEventBusAnnotationProcessor is responsible for finding all methods annotated with @Subscribe during compilation, and generating the AptMethodFinder class to implement the MethodHandle interface. In this way, at runtime, XEventBus does not need to use reflection, and can directly call the subscriber method to improve performance.

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

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

Through the above code implementation, XEventBus can perform efficient event communication between different components. During compilation, the MyEventBusAnnotationProcessor annotation processor will automatically generate the AptMethodFinder class, which implements the MethodHandle interface. This enables XEventBus to call subscriber methods directly at runtime without using reflection, thus improving performance.

Three implementation of subscriber method lookup and registration

In order to realize the search and registration of the subscriber method in XEventBus, we need to use the annotation processor (APT) to generate the corresponding code during compilation. This will avoid using reflection at runtime and thus improve performance.

Subscriber method lookup using Annotation Processor (APT)

We traverse all methods annotated with @Subscribe in the MyEventBusAnnotationProcessor annotation processor and classify them according to their class. Next, generate a static method for each class that looks up subscriber methods, which returns a list of SubscribedMethods that contain information about all subscriber methods.

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

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

    // 省略代码...
}

Register subscriber method to EventBus

In the EventBus class, we provide the register() method for registering subscriber methods into the event bus. During the registration process, EventBus will call the getAllSubscribedMethods() method in the AptMethodFinder class to obtain the information of all subscriber methods in the subscriber class, encapsulate them into Subscription objects, and store them in the internal data structure of the event bus.

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

Generate the index of the subscriber method and the corresponding method call

In the MyEventBusAnnotationProcessor annotation processor, we generate a static method for each subscriber class to find the subscriber method, which returns a SubscribedMethod list containing all subscriber method information. At the same time, we also need to generate an invokeMethod() method for the AptMethodFinder class to execute specific subscriber methods.

In this way, when the EventBus publishes an event, the subscriber method can be invoked directly through the AptMethodFinder's invokeMethod() method without using reflection.

Through the above steps, we have realized the search and registration of subscriber methods, so that XEventBus can efficiently transfer events between different components.

Four implementation of event publication and subscriber method call

In XEventBus, we implement the publication of events and the calling of subscriber methods. Next, we discuss the implementation details of these key functions.

event release

In the EventBus class, we provide the post() method for publishing events. When this method is called, EventBus will traverse all subscribers in the internal data structure and find the corresponding subscriber method according to the event type.

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

Find subscriber methods through the MethodHandle interface

In order to avoid using reflection to call the subscriber method, we use the MethodHandle interface, which allows us to directly call the subscriber method in the code generated at compile time. In the EventBus class, we use MethodHandle as a member variable and initialize it in the constructor.

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

call the subscriber method

When we find the subscriber method, we invoke it using the invokeMethod() method of the MethodHandle interface. This way, we can avoid using reflection and thus improve performance.

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

Subscriber method calls that support different threading models

XEventBus supports multiple threading models, such as main thread, background thread, etc. We can specify threading model in @Subscribe annotation. In order to realize this function, we save the thread model of the subscriber method in the Subscription class, and perform corresponding operations according to the thread model when calling the subscriber method.

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

Through the above implementation, we enable XEventBus to call the subscriber method in different threads according to the thread model of the subscriber method, thus realizing flexible event publication and subscription.

5. Implement anti-registration of subscriber methods

To avoid memory leaks and unnecessary event reception, we need to provide anti-registration functionality to remove subscriber methods when they are no longer needed to receive events. The following are the implementation details of the anti-registration function.

Remove subscriber method

We provide a method called unregister() in the EventBus class to remove the subscriber method. When this method is called, we iterate over the subscriber methods and delete them from the internal data structure.

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) {
    
    
        // 省略代码...
    }
}

clean up resources

When unregistering a subscriber method, we need to ensure that related resources are cleaned up. First, we remove the subscriber from mSubscriberEventTypes. We then check to see if the event type has any other subscribers, and if not, remove the event type from 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);
}

By implementing the anti-registration function, we enable XEventBus to flexibly handle the life cycle of subscribers and avoid potential memory leaks. At the same time, this also helps to improve the performance of event dispatch, because we no longer need to handle subscriber methods for events that we no longer care about.

Six optimization and expansion of XEventBus

In order to improve the performance and flexibility of XEventBus, we can consider the following aspects:

Increase caching strategy to improve performance

In XEventBus, we can use caching strategies to reduce repeated subscriber method lookups and event publications. For example, we can use a HashMap to store registered subscriber methods for quick lookup when needed. At the same time, when the event is published, we can cache the call result of the subscriber method to avoid double calculation.

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

Subscriber methods that support priority and delayed processing

In some scenarios, we may need to control the execution order of subscriber methods. To do this, we can add a priority attribute to the subscriber method. By adding the priority attribute to the @Subscribe annotation, we can implement priority control for subscriber methods.

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

In addition, we can also implement delayed processing by adding the delay attribute. For example, when delay is set to 1000, the subscriber method will be executed 1000 milliseconds after the event is published.

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

Expand more functions, such as event stickiness, etc.

In addition to the optimizations mentioned above, we can also add more features to XEventBus, such as support for sticky events. Sticky events mean that after subscribers register, they can still receive events published before registration. We can do this by adding a sticky attribute to the @Subscribe annotation.

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

When implementing sticky events, we need to maintain a collection of sticky events in XEventBus. When a subscriber registers, if the subscription method sets the sticky property, it will receive the corresponding type of sticky event saved in the collection.

In this way, through the optimization and expansion of XEventBus, we can realize an event bus with more functions and better performance.

Seven XBus Combat Demo

The open source address of XBus . You can refer to the specific use of XBus. A brief description is as follows.

Quick use:

Add the warehouse source address in the root build.gradle

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

Add dependencies in app project level 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'

Register the bus in the onCreate method of Antiactivity:

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

Define an own Event event:

public class WorkEvent {
    
    
    private int num;

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

    public int getNum() {
    
    
        return num;
    }
}

The registration method in the corresponding Activity

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

Send an event to call

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

Other functions

If you want to use the apt method instead of annotations, you can register as follows during bus registration:

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

Eight other implementations of the message bus in Android

Briefly introduce the technical bus implemented by other technical solutions.

Based on RxJava

RxBus is implemented based on RxJava, and additional libraries such as RxJava RxAndroid need to be imported, so the library size is still relatively large. To use RxBus, you have to understand the principle of rxjava. For projects that do not use rxjava, the cost is too high, and it is easy to leak memory. If you want to know the specific implementation details, please refer to "EventBus Implemented Using RxJava" .

It is roughly used as follows:

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-based

ASM (Abstract Syntax Machine) is a Java bytecode manipulation and analysis framework. ASM is used to dynamically generate, transform or manipulate Java bytecode. In Android, ASM is usually used in performance optimization, code injection, AOP (aspect-oriented programming) and other scenarios. However, it is not a typical implementation of an event bus. The time buses based on ASM are: BusUtils ;

If you really want to implement an event bus using ASM, you can try the following:

  1. Use ASM to scan compiled Java bytecode to identify classes and methods that contain specific annotations (such as @Subscribe).
  2. For the scanned classes and methods, use ASM to dynamically modify the bytecode at runtime, and inject the logic code of the event bus, such as registering, unregistering and sending events.
  3. On the event sending side, call the corresponding method through reflection or other methods to trigger the event.

This implementation can be very complex and problem-prone in practice, and can lead to performance and compatibility issues. Therefore, we recommend that when implementing the event bus, give priority to using more mature and convenient solutions, such as EventBus, LiveData and ViewModel or RxJava. These methods perform well in communication and decoupling, are simpler, more efficient, and easier to use.

Based on LiveData

LiveData is part of the Android Architecture Components library. It is an observable data holding class that can notify subscribers when data changes. Using LiveData to implement the event bus can ensure that the communication is performed on the main thread and tightly coupled with the life cycle of the application, thereby avoiding memory leaks.

The following are brief steps to implement a LiveData-based Bus:

  1. Create a singleton Bus class to store multiple LiveData objects, and each LiveData object is responsible for one type of event.
  2. In the Bus class, register and send methods are provided for each type of event. The registration method is used to subscribe to events, and the sending method is used to publish events.
  3. In components that need to receive events (such as Activity, Fragment, etc.), call the registration method of the Bus class to subscribe to the event. When subscribing, an Observer object needs to be passed in to process the received events.
  4. When an event occurs, call the send method of the Bus class to publish the event. Subscribers will receive notifications and process events through Observer objects.
  5. At the end of the component's lifecycle, LiveData will automatically unsubscribe without manually unregistering subscribers.

The event bus based on LiveData has the following advantages:

  • Life cycle awareness: LiveData is closely integrated with the component life cycle, and can automatically unsubscribe when the component is destroyed to avoid memory leaks.
  • Thread safety: LiveData ensures that event notifications are executed on the main thread, avoiding the problem of multi-thread synchronization.
  • Ease of use: LiveData's API is easy to use and compatible with the Android architecture component library.

Based on Flow

Flow is part of the Kotlin coroutine library, which provides a declarative, reactive programming model that makes it easier to handle asynchronous event flows. Using Flow to implement the event bus can ensure that the communication is executed on the main thread, and at the same time, it provides richer operators and combination methods, which can handle more complex scenarios.

The following are brief steps to implement a Flow-based Bus:

  1. Create a singleton Bus class to store multiple Flow objects, and each Flow object is responsible for one type of event.
  2. In the Bus class, register and send methods are provided for each type of event. The registration method is used to subscribe to events, and the sending method is used to publish events.
  3. In components that need to receive events (such as Activity, Fragment, etc.), call the registration method of the Bus class to subscribe to the event. When subscribing, a lambda expression needs to be passed in to process the received event.
  4. When an event occurs, call the send method of the Bus class to publish the event. Subscribers are notified and handle events via lambda expressions.
  5. At the end of the component's life cycle, you can cancel the subscription through the coroutine to avoid memory leaks.

Flow-based event bus has the following advantages:

  • Declarative programming model: Flow provides a declarative and responsive programming model, which can more easily process asynchronous event flows, thereby providing richer operators and combinations, and can handle more complex scenarios.
  • Thread safety: Flow ensures that event notifications are executed on the main thread, avoiding the problem of multi-thread synchronization.
  • Coroutine support: Flow is closely integrated with the Kotlin coroutine library, which can be easily used in coroutines.

Nine summary

Summary of the implementation process of this article

This article introduces a simple EventBus system - XEventBus in detail from design to implementation. We first discussed the basic concepts, application scenarios and advantages of EventBus, and explained the design goals of XEventBus. Then, by analyzing the core components and implementation details of XEventBus, we introduce the core functions of subscriber method lookup, registration, event publishing and subscriber method invocation. Finally, we also discussed the optimization and expansion of XEventBus, including caching strategy, priority control, delay processing and sticky events, etc.

Applicable scenarios and limitations of EventBus

EventBus is mainly suitable for loosely coupled communication between components, especially in Android application development, it can simplify message passing between Activity, Fragment, and Service. However, EventBus also has certain limitations, such as:

  • For large projects, too many event subscriptions can make the code difficult to maintain.
  • EventBus cannot guarantee the order of delivery of events, and sometimes it may be necessary to manually handle the order of execution of events.
  • EventBus has limitations in cross-process communication and requires additional technical support.

Prospects for the future development of EventBus

Despite its limitations, EventBus can still be a very useful tool in the right scenario. With the development of technology, we can expect that the functions of EventBus will continue to improve, such as:

  • Provides more powerful event filtering and routing mechanisms for more precise control over event delivery.
  • Supports cross-process communication, making it applicable to a wider range of scenarios.
  • Enhance event debugging and tracing capabilities to help developers locate problems more easily.

In short, this article hopes to provide readers with an introductory example of EventBus by implementing XEventBus, so as to better understand and apply EventBus, a useful tool.

reference article

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

Guess you like

Origin blog.csdn.net/LucasXu01/article/details/130513860