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