Android event bus framework design: EventBus3.0 source details and architecture analysis (in)

Keywords: Android Java github

The first half is mainly about the analysis of EventBus3.0 architecture. Next, we start the source code analysis of EventBus3.0.

We start with the source code analysis of EventBus3.0 usage, and first analyze the registration events~

1. Analysis of registered source code

To register an event:

EventBus.getDefault().register(this);

getDefault() of EventBus is a single instance, ensure that there is only one instance of EventBus object

    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;
    }

What does the EventBus constructor do?

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

In the EventBus nonparametric construction method, a construction method with one parameter is called. The parameter is passed in the EventBusBuilder object, which configures the default properties related to 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;
    }

These properties can be changed by configuring EventBusBuilder

      EventBus.builder()
            .logNoSubscriberMessages(false)
            .sendNoSubscriberEvent(false)
            .installDefaultEventBus();

Next, we will analyze an important method of EventBus registration, register, and first look at its source code

   public void register(Object subscriber) {
        // Get the Class object of the current registered Class
        Class<?> subscriberClass = subscriber.getClass();
        // Find all methods marked by @ Subscribe annotation in the current registered class
        List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
        synchronized (this) {
            // Traverse all subscription methods to complete registration
            for (SubscriberMethod subscriberMethod : subscriberMethods) {
                subscribe(subscriber, subscriberMethod);
            }
        }
    }

From the source code of the register method, we can see that the main task is to find and register the subscription method. The search of the subscription method is done by findSubscriberMethods, and the registration is done by subscribe.
First look at the source code implementation of findSubscriberMethods

   // Cache subscription method, key: class value of current registered class: subscription method collection
   private static final Map<Class<?>, List<SubscriberMethod>> METHOD_CACHE = new ConcurrentHashMap<>();

    List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
        // First, get the subscription method of the current registered class from the cache. If found, directly return
        List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
        if (subscriberMethods != null) {
            return subscriberMethods;
        }

        // Ignore generated index defaults to false
        // Main function: whether to ignore the subscription index generated by apt (annotation processor)
        if (ignoreGeneratedIndex) {
            // If ignored, use reflection technology to find all subscription methods marked by @ Subscribe annotation
            subscriberMethods = findUsingReflection(subscriberClass);
        } else {
            // By default, it is not ignored, but if the subscription index generated by apt (annotation processor) is not used, it is still searched by reflection technology
            subscriberMethods = findUsingInfo(subscriberClass);
        }
        // If there is no subscription method in the current registered class, an exception is thrown
        if (subscriberMethods.isEmpty()) {
            throw new EventBusException("Subscriber " + subscriberClass
                    + " and its super classes have no public methods with the @Subscribe annotation");
        } else {
            // There is a subscription method in the current registration class, which is added to the cache for direct use when registering again
            METHOD_CACHE.put(subscriberClass, subscriberMethods);
            return subscriberMethods;
        }
    }

The main task of findSubscriberMethods is very clear. First, get the subscription method of the current registered class from the cache. If it is found, return it directly. If it is not found, find all the subscription methods of the current registered class through apt (annotation processor) or reflection technology. If it is not found, throw an exception. Otherwise, cache it in memory and return it to the current registered class All subscription methods.

The findUsingInfo method is used to find all the subscription methods in the current registered class

    private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
        // FindState
        FindState findState = prepareFindState();
        findState.initForSubscriber(subscriberClass);
        while (findState.clazz != null) {
            // If the apt (annotation processor) is not used to generate the subscription method index and null is returned, it will enter the else statement
            findState.subscriberInfo = getSubscriberInfo(findState);
            if (findState.subscriberInfo != null) {
                SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
                for (SubscriberMethod subscriberMethod : array) {
                    if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
                        findState.subscriberMethods.add(subscriberMethod);
                    }
                }
            } else {
                // Use reflection technology to find all subscription methods in the current registered class
                findUsingReflectionInSingleClass(findState);
            }
            // Continue searching from the parent until the parent is null
            findState.moveToSuperclass();
        }
        // Return all subscription methods in the registration class, release the state in findState, and put the findState object back into the cache pool
        return getMethodsAndRelease(findState);
    }

The findUsingInfo method mainly looks up all subscription methods from the current registered class and parent class. First, it looks up from the index of subscription methods generated through apt (annotation processor). If not, it looks up through reflection technology.

Let's first look at how to search through reflection technology? The source code of the key method findusingreflectioningsingleclass is as follows:

    private void findUsingReflectionInSingleClass(FindState findState) {
        Method[] methods;
        try {
            // This is faster than getMethods, especially when subscribers are fat classes like Activities
            methods = findState.clazz.getDeclaredMethods();
        } catch (Throwable th) {
            // Workaround for java.lang.NoClassDefFoundError, see https://github.com/greenrobot/EventBus/issues/149
            methods = findState.clazz.getMethods();
            findState.skipSuperClasses = true;
        }
        for (Method method : methods) {
            int modifiers = method.getModifiers();
            // Method must be public and cannot be ABSTRACT or STATIC
            if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
                Class<?>[] parameterTypes = method.getParameterTypes();
                // Method must have one parameter
                if (parameterTypes.length == 1) {
                    Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
                    // Method must be marked with @ Subscribe annotation
                    if (subscribeAnnotation != null) {
                        // Method parameter type
                        Class<?> eventType = parameterTypes[0];
                        // Used to determine whether the method has been added
                        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");
            }
        }
    }

The search of the subscription method has been analyzed, and then the source code of the subscribe method has been analyzed

 private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
        // Subscription event type, that is, the parameter type of subscription method
        Class<?> eventType = subscriberMethod.eventType;
        // Subscription encapsulates the object and subscription method of the current registered class
        Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
        // subscriptionsByEventType is a Map, key is the Subscription event type, and value is the Subscription collection
        CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
        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);
            }
        }

        // Add new subscription to the subscriptions collection
        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 is also a Map. key is the object of the registration class, and value is the parameter type collection of all the subscription methods in the current registration class
        List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
        if (subscribedEvents == null) {
            subscribedEvents = new ArrayList<>();
            typesBySubscriber.put(subscriber, subscribedEvents);
        }
        subscribedEvents.add(eventType);

        // By default, sticky is false, which means it's not a sticky event. The following code doesn't look at it first, and then analyzes it when it comes to sticky events
        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);
            }
        }
    }

Two maps of subscriptionsByEventType and typesBySubscriber in the subscribe method. Used in event Publishing
The subscriptionsByEventType is used to process events. When canceling the registration, the two maps, subscriptionsByEventType and typesBySubscriber, will be used. Later, we will analyze them in detail

2. Unregister source code analysis

Deregistration method:

EventBus.getDefault().unregister(this);

Unregister calls the unregister method of EventBus. Here is its source code

    public synchronized void unregister(Object subscriber) {
        // typesBySubscriber is a Map. When registering, it has put the key as the object of the current registered class and the value as the parameter type of all subscription methods of the current registered class into the current Map
        List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);
        // If subscribedTypes is not null, the current class is registered
        if (subscribedTypes != null) {
            for (Class<?> eventType : subscribedTypes) {
                // Unsubscribe according to eventType
                unsubscribeByEventType(subscriber, eventType);
            }
            // Remove registered objects from Map
            typesBySubscriber.remove(subscriber);
        } else {
            logger.log(Level.WARNING, "Subscriber to unregister was not registered before: " + subscriber.getClass());
        }
    }

From the unregister method source code, first find the event types of all subscription methods from the typesBySubscriber cache according to the objects of the current unregistered class, and then cancel the subscription according to the event types. Next, look at the unsubscribeByEventType source code:

    private void unsubscribeByEventType(Object subscriber, Class<?> eventType) {
        // subscriptionsByEventType is also a Map. When registering, you have put the key as the Subscription event type and the value as the Subscription object collection into the current Map
        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;
                    // Remove Subscription
                    subscriptions.remove(i);
                    i--;
                    size--;
                }
            }
        }
    }

From the analysis of unregistered source code, we can see that it is mainly to remove the registered class object and remove the subscription method from the two maps of typesBySubscriber and subscriptionsByEventType.

3. Publish common events

You can publish common events in the following ways:

EventBus.getDefault().post("hello, eventbus!");

You can see that the publishing event is completed through the post method

    public void post(Object event) {
        // currentPostingThreadState is a ThreadLocal of type PostingThreadState
        // PostingThreadState maintainer event queue and thread model
        PostingThreadState postingState = currentPostingThreadState.get();
        List<Object> eventQueue = postingState.eventQueue;
        // Add the event to be sent to the event queue first
        eventQueue.add(event);

        // isPosting defaults to false
        if (!postingState.isPosting) {
            // Is it the main thread
            postingState.isMainThread = isMainThread();
            postingState.isPosting = true;
            if (postingState.canceled) {
                throw new EventBusException("Internal error. Abort state was not reset");
            }
            try {
                // Loop through the event queue and publish events from the head of the queue
                while (!eventQueue.isEmpty()) {
                    postSingleEvent(eventQueue.remove(0), postingState);
                }
            } finally {
                postingState.isPosting = false;
                postingState.isMainThread = false;
            }
        }
    }

The post method first adds the event to the event queue, and then circulates the event queue to the postSingleEvent method for processing

    private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
        Class<?> eventClass = event.getClass();
        boolean subscriptionFound = false;
        // eventInheritance defaults to true, indicating the inheritance class of the lookup event
        if (eventInheritance) {
            // Find this event and the collection of events it inherits
            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 not registered, subscriptionFound is false
        if (!subscriptionFound) {
            if (logNoSubscriberMessages) { // logNoSubscriberMessages defaults to true
                logger.log(Level.FINE, "No subscribers registered for event " + eventClass);
            }
            if (sendNoSubscriberEvent && eventClass != NoSubscriberEvent.class &&
                    eventClass != SubscriberExceptionEvent.class) {
                post(new NoSubscriberEvent(this, event));
            }
        }
    }

The postSingleEvent method looks up the event and its inherited events and hands them over to the postSingleEventForEventType method for processing

    private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
        CopyOnWriteArrayList<Subscription> subscriptions;
        synchronized (this) {
            // When subscriptionsByEventType is registered, it will add events and subscription methods
            subscriptions = subscriptionsByEventType.get(eventClass);
        }
        // If not registered, subscriptions will be null
        if (subscriptions != null && !subscriptions.isEmpty()) {
            for (Subscription subscription : subscriptions) {
                postingState.event = event;
                postingState.subscription = subscription;
                boolean aborted = false;
                try {
                    // event processing
                    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;
    }

In the postSingleEventForEventType, all subscription methods of the event will be found from the registry subscriptionsByEventType and handed to the postToSubscription method for processing

4. Thread switching

Finally, the event processing is finished. The thread processing will be switched according to the ThreadMode thread mode

    private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
         // According to the thread mode of the subscription method, the event needs to be processed in which thread
        switch (subscription.subscriberMethod.threadMode) {
            // In the default thread mode, events are processed in which thread when they are published
            case POSTING:
                invokeSubscriber(subscription, event);
                break;
 
            // Require events to be processed on the main thread
            case MAIN:
                // If the publishing event is in the main thread, execute directly
                if (isMainThread) {
                    invokeSubscriber(subscription, event);
                } else {
                    // If the publishing event is in the sub thread, first put the event into the queue, and then switch to the main thread for execution through the Handler
                    mainThreadPoster.enqueue(subscription, event);
                }
                break;

            // Require events to be processed on the main thread
            case MAIN_ORDERED:
                 // No matter which thread the publishing event is on, the event will be put into the queue, and then the Handler will switch to the main thread for execution
                if (mainThreadPoster != null) {
                    mainThreadPoster.enqueue(subscription, event);
                } else {
                    // temporary: technically not correct as poster not decoupled from subscriber
                    invokeSubscriber(subscription, event);
                }
                break;

            // Require events to be processed in a background thread
            case BACKGROUND:
                // If the publishing event is in the main thread, first put the event into the queue, and then drop it to the thread pool for processing, serial processing
                if (isMainThread) {
                    backgroundPoster.enqueue(subscription, event);
                } else {
                    // If the publishing event is in a sub thread, execute directly
                    invokeSubscriber(subscription, event);
                }
                break;

             // Require events to be processed on asynchronous threads
            case ASYNC:
                // No matter which thread the publishing event is on, it will be put into the queue first, and then it will be put into the thread pool for processing and parallel processing
                asyncPoster.enqueue(subscription, event);
                break;
            default:
                throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
        }
    }

In fact, thread switching is based on the thread model of subscription event method and the thread of publishing event. There are two ways to deal with it: one is to execute subscription event method directly through invokeSubscriber method in the corresponding thread, using reflection technology

    void invokeSubscriber(Subscription subscription, Object event) {
        try {
            subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
        } catch (InvocationTargetException e) {
            handleSubscriberException(subscription, event, e.getCause());
        } catch (IllegalAccessException e) {
            throw new IllegalStateException("Unexpected exception", e);
        }
    }

Another is to put the event into the queue (the bottom layer is the implementation of the two-way linked list structure), and then hand it to the Handler or thread pool for processing. Take the ASYNC thread model as an example. AsyncPoster is an instance of AsyncPoster class

class AsyncPoster implements Runnable, Poster {

    private final PendingPostQueue queue;
    private final EventBus eventBus;

    AsyncPoster(EventBus eventBus) {
        this.eventBus = eventBus;
        // PendingPostQueue is a two-way linked list
        queue = new PendingPostQueue();
    }

    public void enqueue(Subscription subscription, Object event) {      
        PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
        // Add the end of the list
        queue.enqueue(pendingPost);
        // Directly throw to thread pool for processing
        eventBus.getExecutorService().execute(this);
    }

    @Override
    public void run() {
        PendingPost pendingPost = queue.poll();
        if(pendingPost == null) {
            throw new IllegalStateException("No pending post available");
        }
        eventBus.invokeSubscriber(pendingPost);
    }
}

First of all, we will analyze viscous events, subscription indexes, AsyncExecutor, etc

Posted by Fribbles on Thu, 21 Nov 2019 03:03:33 -0800