Analysis of EventBus principle

Keywords: Java Android

Analysis of EventBus principle

1, Overview

As we all know, EventBus is used for data communication within app, which can achieve good decoupling effect. The traditional broadcast or callback mode has the characteristics of high code coupling, messy code and not suitable for maintenance. EventBus uses the subscriber / publisher mode, with concise code and high decoupling. This is the background reason of this framework product

2, Overall architecture

This is the diagram of the official framework. The structure described in this diagram is very clear

Subscriber: This is the subscriber who subscribes to a certain type of message. For example, onMessage(String src) subscribes to the message accepting src. Of course, the @ subscriber annotation should be written in the code

Publisher: This is the publisher. After the published message is processed by EventBus, it will call back to execute the subscriber's method

3, Use steps

1. Registration and de registration

EventBus.getDefault().register(this);
//The registration method is very simple. The code is written in the class that needs to be subscribed, such as Activity
EventBus.getDefault().unregister(this);
//This is de registration. This should be written in the destruction method of the class. This and register appear in pairs

2. Subscription method

    @Subscribe(threadMode = ThreadMode.MAIN)
//This is the subscription mode. This method will be executed in the UI thread of android
//Each subscription method can only have one parameter. When sending a message, the corresponding method will be found according to the parameter type
    public void onEventBusMessage(BMessage msg)
    {
        Log.i("eventbus",Thread.currentThread().getId()+":"+msg.toString());

    }
    @Subscribe(threadMode = ThreadMode.ASYNC)
//This method will be executed in the child thread of android
    public void onEventBusMessageSubThread(String msg)
    {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            Log.i("eventbus", "isCurrentThread:"+Looper.getMainLooper().isCurrentThread());
            Log.i("eventbus", "mainthread.getId:"+Looper.getMainLooper().getThread().getId());
        }
        Log.i("eventbus",Thread.currentThread().getId()+":"+msg);

    }

3. Send message

//A message of type String was sent
EventBus.getDefault().post("subthread");
GameTestActivity.BMessage message=new GameTestActivity.BMessage();
message.code=2;
message.msg="ss";
//A message of type BMessage was sent
EventBus.getDefault().post(message);

4. Execution results

We can see from the log that the execution is accurate

4, Principle analysis

1. Overall execution process

1. Register

Extract the subscription method in the registered target class through reflection and save it in EventBus

2. Send message

According to the parameter type of the sent message, find the corresponding subscriber information in EventBus. If found, execute the corresponding method

The overall principle is relatively simple

2. Detailed explanation process

1. Register

1,

public void register(Object subscriber) {
        Class<?> subscriberClass = subscriber.getClass();
        List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
        synchronized (this) {
            for (SubscriberMethod subscriberMethod : subscriberMethods) {
                subscribe(subscriber, subscriberMethod);
            }
        }
    }
//The above is to find the subscribed method information, and then execute the subscription process for each method information
//SubscriberMethodFinder is a class used to find registered methods
//Let's look at what findSubscriberMethods does next

2,

List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
    //Call the data in the cache first, and return the result directly if there is any
        List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
        if (subscriberMethods != null) {
            return subscriberMethods;
        }
		//Whether to register the method by ignoring the annotation index. This is false by default
        if (ignoreGeneratedIndex) {
            //Find by reflection
            subscriberMethods = findUsingReflection(subscriberClass);
        } else {
            //apt mode
            //This method registers the method by annotation index processor, EventBus annotation processor,
            //This is done during compilation, which is much faster than the traditional reflection method,
            //This also needs to be configured in EventBusBuild and the annotation processor of EventBus
            subscriberMethods = findUsingInfo(subscriberClass);
        }
    //Throw an exception if the registration method is not found
        if (subscriberMethods.isEmpty()) {
            throw new EventBusException("Subscriber " + subscriberClass
                    + " and its super classes have no public methods with the @Subscribe annotation");
        } else {
            //Cache it when you find it
            METHOD_CACHE.put(subscriberClass, subscriberMethods);
            return subscriberMethods;
        }
    }

Let's look at the findUsingInfo method, which is implemented by annotation processor

private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
    //The reuse mechanism will be used in the preparation of FindState. There is a pool
        FindState findState = prepareFindState();
    //Set the initialization of subscriberClass in findState, skipsubsclasses = false, subscriberinfo = null
        findState.initForSubscriber(subscriberClass);
    //The following while is to find the corresponding registration information in its own class and the class of its parent class through a loop
    //findState.moveToSuperclass() at the end of while is equivalent to moving the pointer to the parent class until there is no
    //Of course, this can be masked by skippsuperclasses = true in Builder
        while (findState.clazz != null) {
            findState.subscriberInfo = getSubscriberInfo(findState);
            //subscriberInfo is used in conjunction with annotation processing. It is null by default
            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 {
                //The actual method of extracting registration information, let's enter and see
                findUsingReflectionInSingleClass(findState);
            }
            findState.moveToSuperclass();
        }
        return getMethodsAndRelease(findState);
    }

findUsingReflectionInSingleClass(findState);

This is the actual extraction method. Continue to see

private void findUsingReflectionInSingleClass(FindState findState) {
        Method[] methods;
        try {
            // This is faster than getMethods, especially when subscribers are fat classes like Activities
            //Get its own methods array through reflection
            methods = findState.clazz.getDeclaredMethods();
        } catch (Throwable th) {
            // Workaround for java.lang.NoClassDefFoundError, see https://github.com/greenrobot/EventBus/issues/149
            //If there is an exception, find the method of its own subclass
            methods = findState.clazz.getMethods();
            findState.skipSuperClasses = true;
        }
        for (Method method : methods) {
            int modifiers = method.getModifiers();
            //Meet the following conditions: 1. It is a PUBLIC modifier; 2. It cannot have the following modifiers
            //Modifier.ABSTRACT | Modifier.STATIC | BRIDGE | SYNTHETIC
            //In the Modifier, values are declared in hexadecimal and adjacent to 1 to facilitate bit operation
              
            if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
                //Get the parameters of the method,
                Class<?>[] parameterTypes = method.getParameterTypes();
                //Only one parameter is allowed, so EventBus does not support multi parameter method registration
                if (parameterTypes.length == 1) {
                    //Get the annotation of Subscribe.class, which makes a judgment and filters out the annotation method of @ Subscribe
                    Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class );
                    if (subscribeAnnotation != null) {
                        Class<?> eventType = parameterTypes[0];
                        if (findState.checkAdd(method, eventType)) {
                            //Get the thread mode, which is also written in the annotation
                            ThreadMode threadMode = subscribeAnnotation.threadMode();
                            //Setting: priority sticky event sticky (it can also be received after post registration through caching) 					// Add the new SubscriberMethod to the findState collection
                            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");
            }
        }
    }

There is an intermediate class for FindState to deal with reflection mode and apt mode. It deals with the same logic class, which contains various collection data

So far, we have obtained the List result set

Then traverse the collection

Then let's continue to look at the subscribe method of EventBus

//This method is mainly used to save data for later queries
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
        Class<?> eventType = subscriberMethod.eventType;
    	//new a Subscription, which is the last stored result class
        Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
    	//subscriptionsByEventType is a collection of EventBus storage and registration methods
        CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
        if (subscriptions == null) {
            subscriptions = new CopyOnWriteArrayList<>();
            //Add an empty CopyOnWriteArrayList
            subscriptionsByEventType.put(eventType, subscriptions);
        } else {
            if (subscriptions.contains(newSubscription)) {
                throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
                        + eventType);
            }
        }

        int size = subscriptions.size();
        for (int i = 0; i <= size; i++) {
            if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
                //Add a newSubscription
                subscriptions.add(i, newSubscription);
                break;
            }
        }

        List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
        if (subscribedEvents == null) {
            subscribedEvents = new ArrayList<>();
            typesBySubscriber.put(subscriber, subscribedEvents);
        }
    //The Map collection that stores the eventType
        subscribedEvents.add(eventType);
		//If this is a sticky event, you will go to stickyEvents to find it and then execute the registration method
    	//This can be achieved by sending messages first and then registering
        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();
                        //Finally, it will invoke to the registration method
                        checkPostStickyEventToSubscription(newSubscription, stickyEvent);
                    }
                }
            } else {
                Object stickyEvent = stickyEvents.get(eventType);
                checkPostStickyEventToSubscription(newSubscription, stickyEvent);
            }
        }
    }

The whole registration logic above is finished. To sum up, it is to find the registered method and save it

2. Send message

1,post(object)

public void post(Object event) {
    //currentPostingThreadState saves the current processing state
    //currentPostingThreadState is a ThreadLocal type,
    //This ensures that each thread has its own state and will not conflict
        PostingThreadState postingState = currentPostingThreadState.get();
        List<Object> eventQueue = postingState.eventQueue;
        eventQueue.add(event);
		//No, it can only be sent when it is sent
        if (!postingState.isPosting) {
            //Sets whether the state is currently operating on the main thread
            postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper();
            //Sending message
            postingState.isPosting = true;
            //Throw an exception if it has been cancelled
            if (postingState.canceled) {
                throw new EventBusException("Internal error. Abort state was not reset");
            }
            try {
                //If there are always events in the queue, one will be sent until the next is sent
                while (!eventQueue.isEmpty()) {
                    postSingleEvent(eventQueue.remove(0), postingState);
                }
            } finally {
                postingState.isPosting = false;
                postingState.isMainThread = false;
            }
        }
    }

Then let's take a look at postSingleEvent

private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
        Class<?> eventClass = event.getClass();
    	//Whether this registration method finds the flag of
        boolean subscriptionFound = false;
    	//Whether the corresponding registration method will be found from their parent class. The default is true
        if (eventInheritance) {
            //This method is to find the method information including the method and interface of the parent class and the corresponding type
            List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);
            int countTypes = eventTypes.size();
            for (int h = 0; h < countTypes; h++) {
                Class<?> clazz = eventTypes.get(h);
                // |=Or so on, as long as one finds that there is, the final result is there
                subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);
            }
        } else {
            subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);
        }
        if (!subscriptionFound) {
            if (logNoSubscriberMessages) {
                Log.d(TAG, "No subscribers registered for event " + eventClass);
            }
            if (sendNoSubscriberEvent && eventClass != NoSubscriberEvent.class &&
                    eventClass != SubscriberExceptionEvent.class) {
                //If NoSubscriberEvent is configured and no registration method is found, NoSubscriberEvent event is sent
                post(new NoSubscriberEvent(this, event));
            }
        }
    }

Let's continue with postSingleEventForEventType

private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
    	
        CopyOnWriteArrayList<Subscription> subscriptions;
        synchronized (this) {
            //Find the corresponding result value in the map according to the corresponding parameter type. This result is saved from the previous registration
            //This is an array. The CopyOnWriteArrayList class is characterized by high read performance during concurrency and does not worry about synchronization
            //Because reading does not cause concurrency problems
            subscriptions = subscriptionsByEventType.get(eventClass);
        }
    	//Here is the information to facilitate all registration, and then execute
        if (subscriptions != null && !subscriptions.isEmpty()) {
            for (Subscription subscription : subscriptions) {
                postingState.event = event;
                postingState.subscription = subscription;
                boolean aborted = false;
                try {
                    //Jump to the execution method and continue the analysis below
                    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;
    }
//This method is executed differently according to the threadModel thread pattern
private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
        switch (subscription.subscriberMethod.threadMode) {
            case POSTING:
                invokeSubscriber(subscription, event);
                break;
            case MAIN:
                //This isMainThread represents whether the message is sent in the main thread. If so, it can be executed directly,
                if (isMainThread) {
                    invokeSubscriber(subscription, event);
                } else {
                    //If not, execute mainThreadPoster
                    mainThreadPoster.enqueue(subscription, event);
                }
                break;
            case BACKGROUND:
                if (isMainThread) {
                    backgroundPoster.enqueue(subscription, event);
                } else {
                    invokeSubscriber(subscription, event);
                }
                break;
            case ASYNC:
                asyncPoster.enqueue(subscription, event);
                break;
            default:
                throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
        }
    }

There are two important classes

1,HandlerPoster

Switch to the HandlerPoster of the main thread, which is actually a Handler. During initialization, the parameter in EventBus is Looper.getMainLooper(), which can be used to send the operations of sub threads to the main thread for execution
There is also a queue in this class, which is completed in the order of first in first processing

As follows:

final class HandlerPoster extends Handler {

    private final PendingPostQueue queue;
    private final int maxMillisInsideHandleMessage;
    private final EventBus eventBus;
    private boolean handlerActive;

    HandlerPoster(EventBus eventBus, Looper looper, int maxMillisInsideHandleMessage) {
        super(looper);
        this.eventBus = eventBus;
        this.maxMillisInsideHandleMessage = maxMillisInsideHandleMessage;
        queue = new PendingPostQueue();
    }

    void enqueue(Subscription subscription, Object event) {
        PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
        synchronized (this) {
            queue.enqueue(pendingPost);
            if (!handlerActive) {
                handlerActive = true;
                //Send an empty message, start handleMessage, and then get the message data from the queue
                if (!sendMessage(obtainMessage())) {
                    throw new EventBusException("Could not send handler message");
                }
            }
        }
    }

    @Override
    public void handleMessage(Message msg) {
        boolean rescheduled = false;
        try {
            long started = SystemClock.uptimeMillis();
            //Keep getting messages
            while (true) {
                PendingPost pendingPost = queue.poll();
                if (pendingPost == null) {
                    synchronized (this) {
                        //Two checks were done here
                        // Check again, this time in synchronized
                        pendingPost = queue.poll();
                        if (pendingPost == null) {
                            handlerActive = false;
                            //Return when there is no message
                            return;
                        }
                    }
                }
                //Finally, the registered method will be executed, and now it is a ui thread
                eventBus.invokeSubscriber(pendingPost);
                long timeInMethod = SystemClock.uptimeMillis() - started;
                if (timeInMethod >= maxMillisInsideHandleMessage) {
                    if (!sendMessage(obtainMessage())) {
                        throw new EventBusException("Could not send handler message");
                    }
                    rescheduled = true;
                    return;
                }
            }
        } finally {
            handlerActive = rescheduled;
        }
    }
}
2,AsyncPoster

There is asynchronous queue execution processing. Finally, the thread pool getExecutorService().execute(this) is called to execute the thread

Finally, the invokeSubscriber method is executed in the run method

class AsyncPoster implements Runnable {
	
    private final PendingPostQueue queue;
    private final EventBus eventBus;

    AsyncPoster(EventBus eventBus) {
        this.eventBus = eventBus;
        queue = new PendingPostQueue();
    }

    public void enqueue(Subscription subscription, Object event) {
        PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
        queue.enqueue(pendingPost);
        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);
    }

}

Let's take a look at eventBus.invokeSubscriber, which is the last place to execute the reflection registration method

 void invokeSubscriber(PendingPost pendingPost) {
        Object event = pendingPost.event;
        Subscription subscription = pendingPost.subscription;
        PendingPost.releasePendingPost(pendingPost);
        if (subscription.active) {
            invokeSubscriber(subscription, event);
        }
    }
void invokeSubscriber(Subscription subscription, Object event) {
        try {
            //Finally, the registration method is executed to achieve the purpose of callback
            subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
        } catch (InvocationTargetException e) {
            //Handle various exceptions and throw them
            handleSubscriberException(subscription, event, e.getCause());
        } catch (IllegalAccessException e) {
            throw new IllegalStateException("Unexpected exception", e);
        }
    }

5, Summary

The overall logic of EventBus is not responsible, and the logic is very clear

1. Register, extract registration method storage

2. Send a message, find the corresponding registration method, and then invoke to execute

While studying other people's excellent frameworks, I learned excellent design and technology and broadened my thinking. When android technology development encounters a bottleneck, learning more good frameworks and source code is a very good means and strategy

The level is limited. If there is any error, please advise

Posted by fonster_mox on Tue, 26 Oct 2021 01:26:57 -0700