EventBus Source Notes

Keywords: Java Android Google

Preface

This is probably the latest EventBus analysis from the Eastern Hemisphere.

How long has it been since I blogged? Three months? Half a year? I can't imagine how busy I am with my business every day. I seldom have time to summarize and record it. I feel lazy. I used to work very efficiently every day, but now I can't.

These two days are idle, but learning can not stop, think of many systems and tripartite library source code have not been read and summarized, there are many online analysis articles, but after all, they are written by others.

The paper is always shallow, and I know nothing about it.

It's easy to forget when you read what others have written, even though you can understand it. It's easy to remember when you read it and forget to look at it again after you write it down.

Use

This article is based on EventBus 2.4.0 analysis.

Registration Events

According to the usual posture of using EventBus, registration events are usually

EventBus.getDefault().register(this);

register has a lot of overloading methods, and eventually comes to a method with three parameters:

    private synchronized void register(Object subscriber, boolean sticky, int priority) {
        List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriber.getClass());
        for (SubscriberMethod subscriberMethod : subscriberMethods) {
            subscribe(subscriber, subscriberMethod, sticky, priority);
        }
    }

The three parameters are described as follows:

  • Object subscriber, the object to subscribe to the broadcast
  • boolean sticky, subscribe to sticky broadcasting. The so-called sticky broadcast refers to the broadcast that has been sent before subscription and saved in sticky Map. If sticky=true is selected at the time of registration, the broadcast that was sent before will be taken out of sticky Map and distributed to the object immediately at the time of registration. (For specific reference Introduction to EventBus Viscous Broadcasting)
  • int priority, subscription priority, the larger the number, the higher the priority, the more priority other subscribers receive broadcast.

In the above code:

In line 2, all subscription methods are extracted from the subscribers first.

Lines 3 to 5, and then store the method information in memory.

Let's look at the findSubscriberMethods method:

    List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
        String key = subscriberClass.getName();
        List<SubscriberMethod> subscriberMethods;
        synchronized (methodCache) {
            subscriberMethods = methodCache.get(key);
        }
        if (subscriberMethods != null) {
            return subscriberMethods;
        }
        subscriberMethods = new ArrayList<SubscriberMethod>();
        Class<?> clazz = subscriberClass;
        HashSet<String> eventTypesFound = new HashSet<String>();
        StringBuilder methodKeyBuilder = new StringBuilder();
        while (clazz != null) {
            String name = clazz.getName();
            if (name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("android.")) {
                // Skip system classes, this just degrades performance
                break;
            }

            // Starting with EventBus 2.2 we enforced methods to be public (might change with annotations again)
            Method[] methods = clazz.getDeclaredMethods();
            for (Method method : methods) {

                //Omit the code here: Find out all subscription methods and add them to the subscriberMethods collection
            }
            clazz = clazz.getSuperclass();
        }
        if (subscriberMethods.isEmpty()) {
            throw new EventBusException("Subscriber " + subscriberClass + " has no public methods called "
                    + ON_EVENT_METHOD_NAME);
        } else {
            synchronized (methodCache) {
                methodCache.put(key, subscriberMethods);
            }
            return subscriberMethods;
        }
    }

Line 2, take the class name as key.

Lines 3-9, retrieve all the subscription methods corresponding to the key from the map of the method cache, because every time you perform the search for all the subscription methods from the class, all the methods you find are stored in the method cache, and you don't need to look again when you look for them next time.

Why would a class be searched twice? Wouldn't it throw an exception if a class was subscribed twice? In fact, I guess this is designed for traversing the parent class, because the findSubscriberMethods method will find not only all subscription methods in that class, but also all of its parent class's subscription methods.

Suppose there are base class A, which has 10 subscription methods. A has two subclasses A1,A2. If subscription broadcasts are registered in A1 and A2, all subscription methods in their parent class need to be found every time they register. That is, 10 methods of base class A should be found twice. If there is a cache, 10 methods of base class A should be found when A1 registers. Afterwards, when A2 re-registers, it does not need to look for 10 methods of base class A, and can directly return to the last search result.

That should be the case.

Lines 14-28, which is a big circle, can be simplified as follows:

        //Loop to find all onEvent methods for this class and all its parent classes
        while (clazz != null) {
            String name = clazz.getName();
            //Judging whether a system class is a system class, as I said earlier, because all the parent classes are always looked up, the system class is no longer looked up when it is looked up.
            if (name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("android.")) {
                // Skip system classes, this just degrades performance
                break;
            }

            // Starting with EventBus 2.2 we enforced methods to be public (might change with annotations again)
            Method[] methods = clazz.getDeclaredMethods();
            for (Method method : methods) {
                //The code omission here is mainly to determine whether each method is an onEvent method coexisting in the collection.
            }
            //Find the parent class
            clazz = clazz.getSuperclass();
        }

The comments are clearly written, and then look at what's in the for loop:

            // Starting with EventBus 2.2 we enforced methods to be public (might change with annotations again)
            //Remove all subscriber methods and traverse them
            Method[] methods = clazz.getDeclaredMethods();
            for (Method method : methods) {
                String methodName = method.getName();
                //If the method name begins with onEvent
                if (methodName.startsWith(ON_EVENT_METHOD_NAME)) {
                    int modifiers = method.getModifiers();
                    //If the method is public and not abstract or static, etc.
                    if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
                        //Extract the parameter type
                        Class<?>[] parameterTypes = method.getParameterTypes();
                        if (parameterTypes.length == 1) {
                            //Intercept the section after the method name onEvent
                            String modifierString = methodName.substring(ON_EVENT_METHOD_NAME.length());
                            ThreadMode threadMode;
                            if (modifierString.length() == 0) {//If the method name is onEvent only
                                threadMode = ThreadMode.PostThread;
                            } else if (modifierString.equals("MainThread")) {//onEventMainThread
                                threadMode = ThreadMode.MainThread;
                            } else if (modifierString.equals("BackgroundThread")) {//onEventBackgroundThread
                                threadMode = ThreadMode.BackgroundThread;
                            } else if (modifierString.equals("Async")) {//onEventAsync
                                threadMode = ThreadMode.Async;
                            } else {
                                if (skipMethodVerificationForClasses.containsKey(clazz)) {
                                    continue;
                                } else {
                                    throw new EventBusException("Illegal onEvent method, check for typos: " + method);
                                }
                            }
                            Class<?> eventType = parameterTypes[0];
                            methodKeyBuilder.setLength(0);
                            methodKeyBuilder.append(methodName);
                            methodKeyBuilder.append('>').append(eventType.getName());
                            String methodKey = methodKeyBuilder.toString();
                            if (eventTypesFound.add(methodKey)) {
                                // Only add if not already found in a sub class
                                //Add to the collection
                                subscriberMethods.add(new SubscriberMethod(method, threadMode, eventType));
                            }
                        }
                    } else if (!skipMethodVerificationForClasses.containsKey(clazz)) {
                        Log.d(EventBus.TAG, "Skipping method (not public, static or abstract): " + clazz + "."
                                + methodName);
                    }
                }
            }

After finding out the method, encapsulate it as a SubscriberMethod object, as follows:

final class SubscriberMethod {
    final Method method; //Specific methods
    final ThreadMode threadMode; //Subscription threads
    final Class<?> eventType; //Subscription broadcasting types
    /** Used for efficient comparison */
    String methodString; //To compare the two SubscriberMethod s, the source code is annotated: Don't use method.equals because of http://code.google.com/p/android/issues/detail?id=7811#c6
}

Then the latter operation of the findSubscriberMethods method is to store all the methods found in the method Cache as a cache and return the set of all methods as a return value.

Back to the register method,

    private synchronized void register(Object subscriber, boolean sticky, int priority) {
        List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriber.getClass());
        for (SubscriberMethod subscriberMethod : subscriberMethods) {
            subscribe(subscriber, subscriberMethod, sticky, priority);
        }
    }

After identifying all onEvent methods in the subscriber, traverse all Subscriber Methods and classify them in the subscribe() method.

The subscribe method classifies SubscriberMethod and stores relevant information in two Map s, subscriptions ByEventType and typesBySubscriber:

  • Subscriptions ByEventType, categorized by broadcast type, has a value of all subscribers subscribing to that type.
  • TypeesBySubscriber, categorized by subscriber, has a value of all broadcast types for that type of subscription.

It's a little winding. Take a chestnut.

//SimpleClass.java
public class SimpleClass {

    public void register() {
        EventBus.getDefault().register(this);
    }

    public void unregister() {
        EventBus.getDefault().unregister(this);
    }

    public void onEvent(int params) {

    }
}
//MainActivity.java
public class MainActivity extends AppCompatActivity {

    private SimpleClass simpleClass;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        simpleClass = new SimpleClass();
        simpleClass.register();
        EventBus.getDefault().register(this);
    }

    public void onEvent(String str) {

    }

    public void onEvent(int str) {

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        simpleClass.unregister();
        EventBus.getDefault().unregister(this);
    }
}

When all subscribe methods are executed, the data in subscriptionsByEventType and typesBySubscriber are as follows:

subscriptionsByEventType: key is the broadcast type and value subscribes to all methods of that broadcast type.

[
    {
        "key":"java.lang.String",
        "value":[
            {
                "active":true,
                "priority":0,
                "subscriber":"MainActivity@xxxx",
                "subscriberMethod":{
                    "eventType":"java.lang.String",
                    "method":"public void com.xiaohongshu.eventbus.MainActivity.onEvent(java.lang.String)"
                }
            }
        ]
    },
    {
        "key":"int",
        "value":[
            {
                "active":true,
                "priority":0,
                "subscriber":"MainActivity@xxxx",
                "subscriberMethod":{
                    "eventType":"int",
                    "method":"public void com.xiaohongshu.eventbus.MainActivity.onEvent(int)"
                }
            },
            {
                "active":true,
                "priority":0,
                "subscriber":"SimpleClass@xxxx",
                "subscriberMethod":{
                    "eventType":"int",
                    "method":"public void com.xiaohongshu.eventbus.MainActivity.onEvent(int)"
                }
            }
        ]
    }
]

TypeesBySubscriber: key is for all registered subscribers and value is for all subscription methods of that subscriber.

[
    {
        "key":"MainActivity",
        "value":[
            "int",
            "java.lang.String"
        ]
    },
    {
        "key":"SimpleClass",
        "value":[
            "int"
        ]
    }
]

Understand the role of these two map s, and then analyze. Look at the code:

    // Must be called in synchronized block
    private void subscribe(Object subscriber, SubscriberMethod subscriberMethod, boolean sticky, int priority) {
        Class<?> eventType = subscriberMethod.eventType;
        //First, all subscribers corresponding to the broadcast type are extracted from subscriptions ByEventType.
        CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
        Subscription newSubscription = new Subscription(subscriber, subscriberMethod, priority);
        //If the broadcast type has not been subscribed to in memory, create a new array and store it in memory
        if (subscriptions == null) {
            subscriptions = new CopyOnWriteArrayList<Subscription>();
            subscriptionsByEventType.put(eventType, subscriptions);
        } else {
            //If you have already subscribed, throw an exception
            if (subscriptions.contains(newSubscription)) {
                throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
                        + eventType);
            }
        }

        // Starting with EventBus 2.2 we enforced methods to be public (might change with annotations again)
        // subscriberMethod.method.setAccessible(true);

        //Traverse ahead of all subscribers
        int size = subscriptions.size();
        for (int i = 0; i <= size; i++) {
            //If the current subscriber has a higher priority, insert it into a lower priority subscriber
            if (i == size || newSubscription.priority > subscriptions.get(i).priority) {
                subscriptions.add(i, newSubscription);
                break;
            }
        }

        //Remove all subscription methods from the subscriber
        List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
        //If the subscriber has not subscribed to any broadcasts, create a new collection
        if (subscribedEvents == null) {
            subscribedEvents = new ArrayList<Class<?>>();
            typesBySubscriber.put(subscriber, subscribedEvents);
        }
        //Add the subscription event to the subscriber's subscription collection
        subscribedEvents.add(eventType);

        //If the subscription is sticky broadcasting, all sticky events in the cache are immediately taken out and distributed to the subscriber.
        if (sticky) {
            Object stickyEvent;
            synchronized (stickyEvents) {
                stickyEvent = stickyEvents.get(eventType);
            }
            if (stickyEvent != null) {
                // If the subscriber is trying to abort the event, it will fail (event is not tracked in posting state)
                // --> Strange corner case, which we don't take care of here.
                postToSubscription(newSubscription, stickyEvent, Looper.getMainLooper() == Looper.myLooper());
            }
        }
    }

The commentary is clearly written without explanation.

Distribution events

According to the usual posture of using EventBus, sending events is usually

EventBus.getDefault().post(this);

There are also many post methods. First, look at one of them:

    /** Posts the given event to the event bus. */
    public void post(Object event) {
        //Remove the thread state corresponding to the thread
        PostingThreadState postingState = currentPostingThreadState.get();
        //Remove the event queue corresponding to the thread and add the event to the event queue
        List<Object> eventQueue = postingState.eventQueue;
        eventQueue.add(event);

        //If the event is not currently being sent
        if (!postingState.isPosting) {
            //Is the current thread the main thread?
            postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper();
            //Set the state of the event being sent
            postingState.isPosting = true;
            //If the thread has unsubscribed, throw an exception
            if (postingState.canceled) {
                throw new EventBusException("Internal error. Abort state was not reset");
            }
            try {
            //If the thread is empty, all events occur in the dead loop
                while (!eventQueue.isEmpty()) {
                    //Send events
                    postSingleEvent(eventQueue.remove(0), postingState);
                }
            } finally {
                //The flag is being sent with false status
                postingState.isPosting = false;
                //Flags are not main threads
                postingState.isMainThread = false;
            }
        }
    }

Line 3, take out the state of the thread. ThreadLocal is used here. People familiar with Handler mechanism will not feel strange. ThreadLocal's function is to ensure that each thread can maintain different instance objects. ThreadLocal's state is related to thread, so ThreadLocal is suitable here.

Posting ThreadState is as follows:

    /** For ThreadLocal, much faster to set (and get multiple values). */
    final static class PostingThreadState {
        final List<Object> eventQueue = new ArrayList<Object>();//Event queue
        boolean isPosting;//Is the event being sent?
        boolean isMainThread;//Is it the main thread?
        Subscription subscription;//Current Subscriber
        Object event;//Events currently distributed
        boolean canceled;//Has the thread unsubscribed
    }

Back in the post method, the postSingleEvent method is the one that actually sends events.

    private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
        Class<?> eventClass = event.getClass();
        boolean subscriptionFound = false;
        //Whether to find inheritance relationships, EvetInheritance defaults to true in EventBusBuilder
        if (eventInheritance) {
            //Find out all the parent classes and interfaces of this class
            List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);
            int countTypes = eventTypes.size();
            for (int h = 0; h < countTypes; h++) {
                //Distribute the event to all classes or interfaces found
                Class<?> clazz = eventTypes.get(h);
                subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);
            }
        } else {
            //If you don't need to find inheritance relationships, distribute events directly to this class
            subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);
        }
        //If an event is not received by a subscriber, send an event without a subscriber
        if (!subscriptionFound) {
            if (logNoSubscriberMessages) {
                Log.d(TAG, "No subscribers registered for event " + eventClass);
            }
            if (sendNoSubscriberEvent && eventClass != NoSubscriberEvent.class &&
                    eventClass != SubscriberExceptionEvent.class) {
                post(new NoSubscriberEvent(this, event));
            }
        }
    }

A lot of judgments are made in postSingleEvent, and finally events are sent by postSingleEvent ForEventType:

    private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
        CopyOnWriteArrayList<Subscription> subscriptions;
        synchronized (this) {
            //Remove all subscribers that subscribe to the event based on the event type
            subscriptions = subscriptionsByEventType.get(eventClass);
        }
        if (subscriptions != null && !subscriptions.isEmpty()) {
            //Traverse through all subscribers and distribute the event to them
            for (Subscription subscription : subscriptions) {
                postingState.event = event;
                postingState.subscription = subscription;
                boolean aborted = false;
                try {
                    //Send events
                    postToSubscription(subscription, event, postingState.isMainThread);
                    //If the distribution thread is cancelled, the sending event is interrupted
                    aborted = postingState.canceled;
                } finally {
                    postingState.event = null;
                    postingState.subscription = null;
                    postingState.canceled = false;
                }
                //Interrupt sending event
                if (aborted) {
                    break;
                }
            }
            return true;
        }
        return false;
    }

As you can see, a lot of judgments are made and events are sent to subscribers in postToSubscription:

    private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
        switch (subscription.subscriberMethod.threadMode) {
            case PostThread:
                invokeSubscriber(subscription, event);
                break;
            case MainThread:
                if (isMainThread) {
                    invokeSubscriber(subscription, event);
                } else {
                    mainThreadPoster.enqueue(subscription, event);
                }
                break;
            case BackgroundThread:
                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);
        }
    }

Then, many threads are judged and distributed to the corresponding distribution threads according to the expected threads of the subscriber. Finally, the real method call is triggered in invokeSubscriber.

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

Through reflection calls, it's clear.

Note that when distributing events in the postToSubscription method:

  • If the subscriber expects to be the main thread and is not currently the main thread, the event is inserted into the mainThreadPoster.
  • If the subscriber expects to be a background thread, the event is inserted into the background event queue BackgroundThread.
  • If the subscriber expects an asynchronous thread, the event is inserted into the asyncPoster asynchronous event queue.
  • Others are distributed directly.

In addition, add a few threads about how to achieve:

    private final HandlerPoster mainThreadPoster;
    private final BackgroundPoster backgroundPoster;
    private final AsyncPoster asyncPoster;
    private final ExecutorService executorService;
  • mainThreadPoster, a subclass object of Handler, is still the handler-based mechanism, except that Looper defaults to Looper.getMainLooper()
  • Both backgroundPost and asyncPost are implemented Runnable interfaces, which are then run as parameters of the thread pool executor service
  • ExcutorService defaults to provide implementation in EventBusBuilder, and is a cache thread pool by default:
    private final static ExecutorService DEFAULT_EXECUTOR_SERVICE = Executors.newCachedThreadPool();

So far, the event has been introduced.

Cancellation of registration

According to the usual posture of using EventBus, canceling registration is usually

EventBus.getDefault().unregister(this);

Two map s were introduced earlier: subscriptions ByEventType and typesBySubscriber.

  • Subscriptions ByEventType, categorized by broadcast type, has a value of all subscribers subscribing to that type.
  • TypeesBySubscriber, categorized by subscriber, has a value of all broadcast types for that type of subscription.

When broadcasting, all subscribers can be identified according to subscriptions ByEventType, and then the events are sent to them in turn.

What is the role of typesBySubscriber? If you want to cancel the subscription now, you can see its effectiveness. If you want to cancel all the broadcasts subscribed to by the subscriber, you just need to find all the broadcasts subscribed by the subscriber according to the type BySubscriber and remove them from the subscriptions ByEventType.

Look at the unregister code.

    /** Unregisters the given subscriber from all event classes. */
    public synchronized void unregister(Object subscriber) {
      //Find out all the broadcasts that the subscriber subscribes to
        List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);
        if (subscribedTypes != null) {
            for (Class<?> eventType : subscribedTypes) {
               //Unsubscribe to the broadcast in turn
                unubscribeByEventType(subscriber, eventType);
            }
            //Remove it from the subscriber collection
            typesBySubscriber.remove(subscriber);
        } else {
            Log.w(TAG, "Subscriber to unregister was not registered before: " + subscriber.getClass());
        }
    }

unubscribeByEventType is used in unregister. The code is as follows:

    /** Only updates subscriptionsByEventType, not typesBySubscriber! Caller must update typesBySubscriber. */
    private void unubscribeByEventType(Object subscriber, Class<?> eventType) {
        //Remove all subscribers who subscribed to the broadcast
        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 the current subscriber equals the subscriber to be unregistered, the subscription is cancelled
                if (subscription.subscriber == subscriber) {
                    subscription.active = false;
                    subscriptions.remove(i);
                    i--;
                    size--;
                }
            }
        }
    }

So far, the cancellation of subscriptions has been completed.

Posted by Stripy42 on Mon, 01 Jul 2019 13:21:25 -0700