A Bus is exactly what it is called in English: a Bus. Traveling through the city on a fixed route, each passenger can choose when to get on and when to get off according to his or her destination. The EventBus is similar, except that the passengers are the messages you want to send. EventBus provides a very flexible way to communicate for Android development. For example, in Android, Google suggests using interface callback for communication between Fragments and activities. It is relatively troublesome to communicate with activities without jumping. That’s where EventBus comes in. You just send a message and the subscriber gets it. Let’s implement an event bus framework ourselves

A prototype

  1. Let’s create a new class and provide three methods that mimic EventBus
public class HBus {

    private static volatile HBus instance;

    public static HBus getInstance(a) {
        if (instance == null) {
            synchronized (HBus.class) {
                if (instance == null) {
                    instance = newHBus(); }}}return instance;
    }

    private HBus(a) {}public void register(Object obj) {
        / / subscribe
    }

    public void unregister(Object obj) {
        // Unsubscribe
    }

    public void post(Object obj) {
        // Publish the message}}Copy the code
  1. We’ll implement the three methods in turn, starting with register and, for convenience, using HashMap to hold class objects
    public void register(Object obj) {
        if (obj == null) {
            throw new RuntimeException("can not register null object");
        }
        String key = obj.getClass().getName();
        if (!subscriptionMap.containsKey(key)) {
            subscriptionMap.put(key, obj);
        }
    }
Copy the code

SubscriptionMap is a normal HashMap initialized in the constructor of HBus. To ensure the uniqueness of different classes, the full name of the class is used as the key.

    private HashMap<String, Object> subscriptionMap;
    private HBus(a) {
        subscriptionMap = new HashMap<>();
    }
Copy the code
  1. Unregister is similar to register
    public void unregister(Object obj) {
        if (obj == null) {
            throw new RuntimeException("can not unregister null object");
        }
        String key = obj.getClass().getName();
        if(subscriptionMap.containsKey(key)) { subscriptionMap.remove(key); }}Copy the code
  1. The most important thing is the implementation of the post method, what does the post actually do? Now that we have all the subscribed classes stored in the HashMap, we need to walk through those classes, find the methods that match the criteria, and execute those methods.
    public void post(Object msg) {
        for (Map.Entry<String, Object> entry : subscriptionMap.entrySet()) {
            // Get the subscription class
            Object obj = entry.getValue();
            // Get all the methods in the subscription class
            Method[] methods = obj.getClass().getDeclaredMethods();
            // Walk through all the methods in the class
            for (Method method : methods) {
                // If the method name begins with onEvent, reflection is executed
                if (method.getName().startsWith("onEvent")) {
                    try {
                        method.invoke(obj, msg);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                    break; }}}}Copy the code
  1. There is a slight problem with the post method above. Every time you post, you have to iterate through the method to find the beginning of onEvent, which is obviously not necessary, so make a little encapsulation. The Subscription class encapsulates the process of finding methods and reflections
public class Subscription {
    private static final String METHOD_PREFIX = "onEvent";
    public Object subscriber;
    private Method method;

    public Subscription(Object obj) {
        subscriber = obj;
        findMethod();
    }

    private void findMethod(a) {
        if (method == null) {
            Method[] allMethod = subscriber.getClass().getDeclaredMethods();
            for (Method method : allMethod) {
                if (method.getName().startsWith(METHOD_PREFIX)) {
                    this.method = method;
                    break; }}}}public void invokeMessage(Object msg) {
        if(method ! =null) {
            try {
                method.invoke(subscriber, msg);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch(InvocationTargetException e) { e.printStackTrace(); }}}}Copy the code

HBus final version

public class HBus {
    private static volatile HBus instance;
    private HashMap<String, Subscription> subscriptionMap;
    public static HBus getInstance(a) {
        if (instance == null) {
            synchronized (HBus.class) {
                if (instance == null) {
                    instance = newHBus(); }}}return instance;
    }

    private HBus(a) {
        subscriptionMap = new HashMap<>();
    }

    public void register(Object obj) {
        if (obj == null) {
            throw new RuntimeException("can not register null object");
        }
        String key = obj.getClass().getName();
        if(! subscriptionMap.containsKey(key)) { subscriptionMap.put(key,newSubscription(obj)); }}public void unregister(Object obj) {
        if (obj == null) {
            throw new RuntimeException("can not unregister null object");
        }
        String key = obj.getClass().getName();
        if(subscriptionMap.containsKey(key)) { subscriptionMap.remove(key); }}public void post(Message msg) {
        for(Map.Entry<String, Subscription> entry : subscriptionMap.entrySet()) { entry.getValue().invokeMessage(msg); }}}Copy the code
  1. test
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        HBus.getInstance().register(this);
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                HBus.getInstance().post("hello"); }}); }public void onEventHello(Object msg) {
        if (msg instanceof String) {
            Log.e("haozhn".""+ msg); }}@Override
    protected void onDestroy(a) {
        super.onDestroy();
        HBus.getInstance().unregister(this); }}Copy the code

In the example above, MainActivity is both publisher and subscriber. Post a string “Hello” when the button is clicked and receive it in onEventHello. The results

Use of annotations

One important thing about the implementation of the event bus is the method tag. We publish a message, and we have to know which methods need the message. In the example above, we took a very primitive approach and stipulated that methods must begin with onEvent. This approach is obviously not flexible enough, but is there a more advanced implementation? Of course there is. That’s the way annotations work.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Subscriber {

}
Copy the code

An annotation is just a tag, so no arguments are required. The Subscription modification is required below

public class Subscription {
    public Object subscriber;
    private Method method;

    public Subscription(Object obj) {
        subscriber = obj;
        findMethod();
    }

    private void findMethod(a) {
        if (method == null) {
            Method[] allMethod = subscriber.getClass().getDeclaredMethods();
            for (Method method : allMethod) {
                Annotation annotation = method.getAnnotation(Subscriber.class);
                if(annotation ! =null) {
                    this.method = method;
                    break; }}}}public void invokeMessage(Object msg) {
        if(method ! =null) {
            try {
                method.invoke(subscriber, msg);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch(InvocationTargetException e) { e.printStackTrace(); }}}}Copy the code

It is mainly to modify the findMethod method to determine whether the method is marked by Subscriber annotation. Then let’s modify the previous test code

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        HBus.getInstance().register(this);
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                HBus.getInstance().post("hello"); }}); }@Subscriber
    public void anyMethod(Object msg) {
        if (msg instanceof String) {
            Log.e("haozhn".""+ msg); }}@Override
    protected void onDestroy(a) {
        super.onDestroy();
        HBus.getInstance().unregister(this); }}Copy the code

You can use the Subscriber annotation tag method to name anything you want.

Broadcasting and point-to-point communication

What we’ve implemented so far is an event bus that is similar to broadcasting, in that once a message is published, all subscribers receive it and execute it. In addition, both published and received messages are of Object type, so there is A problem, such as A, B, C three pages. What happens when A and B both subscribe to messages and C publishes A message and only wants A to execute it? We can of course pass an object and define a field in the object as a distinction.

public class HMessage {
    public int what;
    public String msg;
}
Copy the code

After receiving the message, it is strongly converted to HMessage and distinguished by what field. For example, WHEN what==1, A processes the message, and when what==2, B processes the message. If ObjectA is passed and ObjectB is passed in collaborative projects, the resulting code will often become unmaintainable, so we should define a generic Message object similar to Message in Android.

public class HMessage {
    private int key;
    private Object obj;

    private HMessage(a) {}public HMessage(int key) {
        this(key, null);
    }

    public HMessage(int key, Object obj) {
        this.key = key;
        this.obj = obj;
    }

    public int getKey(a) {
        return key;
    }

    public Object getData(a) {
        returnobj; }}Copy the code

Then we need to change the previous post argument to HMessage so that the processing of the message becomes

    @Subscriber
    public void anyMethod(HMessage msg) {
        if(msg == null) return;
        switch (msg.getKey()) {
            case 1:
                Log.e("haozhn"."" + msg.getData());
                break; }}Copy the code

This can be done within each subscription method to determine whether the message needs to be processed, but the message is still sent to all subscribers in a group. Is there any way to implement peer-to-peer communication? Then you need to do some writing on the Subscriber annotation. We can try to add a parameter to it

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Subscriber {
    String tag(a) default Subscription.DEFAULT_TAG;
}
Copy the code

Then add parameters to mark the method

    @Subscriber(tag = "tag")
    public void anyMethod(HMessage msg) {
        if(msg == null) return;
        switch (msg.getKey()) {
            case 1:
                Log.e("haozhn"."" + msg.getData());
                break; }}Copy the code

You also set the tag when you find the method

public class Subscription {
    public Object subscriber;
    private Method method;
    private String tag;

    public Subscription(Object obj) {
        subscriber = obj;
        findMethodAndTag();
    }

    private void findMethodAndTag(a) {
        if (method == null) {
            Method[] allMethod = subscriber.getClass().getDeclaredMethods();
            for (Method method : allMethod) {
                Subscriber annotation = method.getAnnotation(Subscriber.class);
                if(annotation ! =null) {
                    this.method = method;
                    this.tag = annotation.tag();
                    break; }}}}public String getTag(a) {
        return tag;
    }

    public void invokeMessage(HMessage msg) {
        if(method ! =null) {
            try {
                method.invoke(subscriber, msg);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch(InvocationTargetException e) { e.printStackTrace(); }}}}Copy the code

Messages sent by POST are filtered by tag

    public void post(HMessage msg,String[] tags) {
        if(tags == null || tags.length == 0) {
            throw new IllegalArgumentException("tags can not be null or length of tags can not be 0");
        }
        for (Map.Entry<String, Subscription> entry : subscriptionMap.entrySet()) {
            Subscription sub = entry.getValue();
            for (String tag : tags) {
                if(tag.equals(sub.getTag())) { sub.invokeMessage(msg); }}}}Copy the code

To make it easier to use, we can add a default value to tag.

public @interface Subscriber {
    String tag(a) default Subscription.DEFAULT_TAG;
}
Copy the code

When the value of tag is equal to the default value, no tag is set. We will set a tag for this method. I choose the full name of the class as the tag, so as to avoid duplication

    private void initMethodAndTag(a) {
        Method[] methods = subscriber.getClass().getDeclaredMethods();
        for (Method m : methods) {
            Subscriber annotation = m.getAnnotation(Subscriber.class);
            if(annotation ! =null) {
                method = m;
                tag = DEFAULT_TAG.equals(annotation.tag()) ? subscriber.getClass().getName() : annotation.tag();
                break; }}}Copy the code

Then override a POST method and pass in a Class array

    public void post(HMessage msg, Class[] classes) {
        if (classes == null || classes.length == 0) {
            throw new IllegalArgumentException("classes can not be null or length of classes can not be 0");
        }
        String[] tags = new String[classes.length];
        for (int i = 0; i < classes.length; i++) {
            tags[i] = classes[i].getName();
        }
        post(msg, tags);
    }
Copy the code

This modification is also very convenient for us to use

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        HBus.getInstance().register(this);
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                HBus.getInstance().post(new HMessage(1."hello"), newClass[]{MainActivity.class}); }}); }@Subscriber
    public void anyMethod(HMessage msg) {
        if (msg == null) return;
        switch (msg.getKey()) {
            case 1:
                Log.e("haozhn"."" + msg.getData());
                break; }}@Override
    protected void onDestroy(a) {
        super.onDestroy();
        HBus.getInstance().unregister(this); }}Copy the code

If you really want to achieve a similar effect of broadcast, of course, it is possible, but it is not recommended, because the broadcast mode is flexible, but too much will be very confusing and difficult to maintain.

thread

The framework now meets basic usage requirements, but there is an obvious flaw. Is it successful to send a message in a child thread telling an Activity to change a UI? Of course not, because the way it works now is to process the result in the current thread, and if you send a message in a child thread, you try to change the UI in the child thread. So the next thing to do is to separate the threads on which the subscribers are. We might as well add another parameter to Subscriber.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Subscriber {
    String tag(a) default Subscription.DEFAULT_TAG;
    ThreadMode thread(a) default ThreadMode.SAMETHREAD;
}
Copy the code

ThreadMode is an enumerated class that represents the type of thread

public enum  ThreadMode {
    /** ** main thread */
    MAIN,
    /**
     * 相同的线程
     */
    SAMETHREAD
}
Copy the code

If it was SAMETHREAD, we would have kept it the same. What about MAIN? Do what Android does, and pass the message to the main thread via Handler.

public class Subscription {
    static final String DEFAULT_TAG = "hbus_default_tag_value";
    public Object subscriber;
    private Method method;
    private String tag;
    private ThreadMode threadMode;
    private MsgHandler mHandler;

    public Subscription(Object obj) {
        subscriber = obj;
        mHandler = new MsgHandler(Looper.getMainLooper());
        findMethodAndTag();
    }

    private void findMethodAndTag(a) {
        if (method == null) {
            Method[] allMethod = subscriber.getClass().getDeclaredMethods();
            for (Method method : allMethod) {
                Subscriber annotation = method.getAnnotation(Subscriber.class);
                if(annotation ! =null) {
                    this.method = method;
                    this.tag = DEFAULT_TAG.equals(annotation.tag()) ? subscriber.getClass().getName() : annotation.tag();
                    this.threadMode = annotation.thread();
                    break; }}}}public String getTag(a) {
        return tag;
    }

    public void invokeMessage(HMessage msg) {
        if(method ! =null) {
            try {
                if (threadMode == ThreadMode.MAIN) {
                    / / main thread
                    Message message = Message.obtain();
                    message.obj = msg;
                    mHandler.sendMessage(message);
                } else{ method.invoke(subscriber, msg); }}catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch(InvocationTargetException e) { e.printStackTrace(); }}}private class MsgHandler extends Handler {
        public MsgHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.obj instanceof HMessage) {
                try {
                    HMessage hm = (HMessage) msg.obj;
                    method.invoke(subscriber, hm);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
Copy the code

Such a simple version of the event bus framework is complete, finally attached to the Github address HBus