自己动手实现一个EventBus框架

1,735 阅读8分钟

总线(Bus)正如它的英文名称一样:公共汽车。沿着固定的路线穿梭与城市中,每个乘客可以根据自己的目的地选择在什么时候上车,什么时候下车。事件总线(EventBus)也是类似,只是那些乘客是你想要发送的消息。EventBus对于Android开发来说,提供了一个非常灵活的通信方式。例如在Android里,Fragment和Activity通信中谷歌建议使用接口回调的方式,Activity和Activity之间只通信不跳转也相对比较麻烦。这时候EventBus的作用就提现了出来,只需要发一个消息,订阅者就可以收到。下面我们就自己实现一个事件总线框架

雏形

  1. 我们新建一个类,并模仿EventBus提供三个方法
public class HBus {

    private static volatile HBus instance;

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

    private HBus() {
    }

    public void register(Object obj) {
        // 订阅
    }

    public void unregister(Object obj) {
        // 取消订阅
    }

    public void post(Object obj) {
        // 发布消息
    }
}
  1. 接下来依次来实现这三个方法,首先是register,为了方便起见,我们先使用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, obj);
        }
    }

subscriptionMap是一个普通的HashMap,在HBus的构造方法中初始化,为了保证不同类的唯一性,采用类名全称作为key。

    private HashMap<String, Object> subscriptionMap;
    private HBus() {
        subscriptionMap = new HashMap<>();
    }
  1. unregister与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);
        }
    }
  1. 最重要的就是post方法的实现了,post的时候究竟做了什么呢?我们现在已经把订阅的类都保存在HashMap里面,接下来我们需要遍历这些类,找出这些类中符合条件的方法,然后执行这些方法。
    public void post(Object msg) {
        for (Map.Entry<String, Object> entry : subscriptionMap.entrySet()) {
            // 获取订阅类
            Object obj = entry.getValue();
            // 获取订阅类中的所有方法
            Method[] methods = obj.getClass().getDeclaredMethods();
            // 遍历类中的全部方法
            for (Method method : methods) {
                // 如果方法名以onEvent开头,那就反射执行
                if (method.getName().startsWith("onEvent")) {
                    try {
                        method.invoke(obj, msg);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                    break;
                }
            }
        }
    }
  1. 上面的post方法有一点小问题,每次post的时候都要重新遍历找到onEvent开头的方法,这显然是不必要的,所以做一点小封装。
    Subscription类封装了查找方法和反射的过程
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() {
        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();
            }
        }
    }
}

HBus最终版

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

    private HBus() {
        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, new Subscription(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);
        }
    }
}
  1. 测试
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() {
        super.onDestroy();
        HBus.getInstance().unregister(this);
    }
}

上面的例子中,MainActivity既是发布者,也是订阅者。在点击按钮的时候发布一个字符串"hello",然后在onEventHello中接收。
运行结果

注解的使用

关于事件总线的实现,很重要的一点是方法的标记,我们发布了一个消息,必须要知道哪些方法需要这个消息。在上面的例子中,我们采用了一个很原始的方法,约定方法必须以onEvent开头。这种方式显然是不够灵活的,那有没有更高级的实现方式呢?当然有,那就是注解的方式。

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

}

注解只是一个标记,所以不需要参数。下面需要对Subscription修改

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

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

    private void findMethod() {
        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();
            }
        }
    }
}

主要是对findMethod方法进行修改,判断方法是否被Subscriber注解标记。
然后我们把之前的测试代码修改一下

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() {
        super.onDestroy();
        HBus.getInstance().unregister(this);
    }
}

通过Subscriber注解标记的方法就可以任意取名了。

广播与点对点通信

目前为止我们实现的实现的事件总线其实和广播类似,发布一个消息后所有的订阅者都能收到,并且都会执行。而且发布的消息和接受的消息都是Object类型,这样就会有一个问题,比如有A,B,C三个页面。A,B都订阅了消息,C发布了一个消息只想让A执行,这时候怎么办?我们当然可以传一个对象过去,在对象里定义一个字段作为区分。

public class HMessage {
    public int what;
    public String msg;
}

然后在接收到消息后强转成HMessage,根据what字段来区分,比如what==1的时候A来处理这个消息,what==2的时候B来处理这个消息。
如果在多人合作开发的项目里,有人传ObjectA,有人传ObjectB,这样最后的代码通常会变得不可维护,所以我们应该定义一个类似Android系统中Message一样的通用消息对象。

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

    private HMessage() {
    }

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

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

    public int getKey() {
        return key;
    }

    public Object getData() {
        return obj;
    }
}

然后我们需要把之前post的参数改成HMessage,这样消息的处理就会变成

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

这样可以在每个订阅的方法内部去判断是否需要处理这个消息,但是消息依然会群发给所以订阅者,有没有什么方法可以实现点对点的通信呢?那就需要在Subscriber注解上做点文章了。我们可以尝试给它加个参数

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

然后在标记方法的时候加上参数

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

在找到method的时候也要设置一下tag

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

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

    private void findMethodAndTag() {
        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() {
        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();
            }
        }
    }
}

在post发送消息的时候通过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);
                }
            }
        }
    }

为了更易于使用,我们可以给tag加一个默认值。

public @interface Subscriber {
    String tag() default Subscription.DEFAULT_TAG;
}

当tag等于默认值的时候说明没有设置tag,我们就自己给该方法设置一个tag,我选用类全称作为tag,这样可以避免重复

    private void initMethodAndTag() {
        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;
            }
        }
    }

然后重载一个post方法,传一个Class数组

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

这样修改以后我们使用起来也非常方便

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"), new Class[]{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() {
        super.onDestroy();
        HBus.getInstance().unregister(this);
    }
}

如果真要实现类似的广播的效果呢,那当然是可以的,只是非常不建议这样,因为广播的模式虽然灵活,但多了之后就会非常混乱,难以维护。

线程切换

现在这个框架已经可以满足基本的使用需求了,但还有一个明显的缺陷,那就是如果在子线程中发送一个消息,通知Activity去更改一个UI,可以成功吗?当然不能,因为现在的处理方式是在当前线程处理结果,如果在子线程发送消息,就会在子线程尝试更改UI。所以接下来要做的就是区分订阅者所在线程。我们不妨再给Subscriber加个参数。

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

ThreadMode是个枚举类,表示线程的类型

public enum  ThreadMode {
    /**
     * 主线程
     */
    MAIN,
    /**
     * 相同的线程
     */
    SAMETHREAD
}

如果是SAMETHREAD就保持和原来的处理方式就可以了。如果是MAIN呢?那就按照Android的方法,通过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() {
        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() {
        return tag;
    }

    public void invokeMessage(HMessage msg) {
        if (method != null) {
            try {
                if (threadMode == ThreadMode.MAIN) {
                    // 主线程
                    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();
                }
            }
        }
    }
}

这样一个简单版的事件总线框架就完成了,最后附上github地址HBus