EventBus 是 Android 开发者们都很熟悉的一个库,它可以代替
Intent、Handler 或者 Broadcast 在各个活动、碎片、服务或者线程间传递消息,使用方便,性能开销小。
下面让我们模仿源码,写一个属于自己的小 EventBus。在理解 EventBus 工作原理基础上,也附带复习了一番 Java 的反射、注解知识,一举两得。
注意本文所指的 EventBus 是针对 3.x 版本而非 2.x。
如果你还不了解 EventBus,推荐看这篇文——
实现思路
我们用一个 Java Map 去记录所有的订阅关系。这个 Map 的键,对应一个事件;值,则对应所有订阅了此事件的方法(因此应该是一个集合)。
因此首先要定义一个订阅类,用作此集合的类型。
订阅类 Subscription
打开「安卓死丢丢」,新建一个项目 MyEventBus。
订阅类与方法挂钩,又应该能指向所有的类,因此包含一个 Method 和一个 Object 字段。Object 对应的就是「订阅者」,即能接收事件的类。
新建一个类 Subscription。
public class Subscription {
public Method method;
public Object subscriber;
public Subscription(Method method, Object subscriber) {
this.method = method;
this.subscriber = subscriber;
}
}
完成订阅类后,就可以写 EventBus 的主类了。
主类 EventBus
主类 通过getDefault()
方法获取单例,此后可以调用如下方法——
-
register(Object subscriber)
注册,让订阅者(subscriber)可以接收事件 -
unRegister(Object subscriber)
反注册 -
post(Object event)
发送事件
当然还要包含前面提到的 Map。
新建一个类 EventBus,然后一个个实现上述的方法。
public class EventBus {
private volatile static EventBus instance;
private Map<Class<?>, List<Subscription>> map;
private EventBus() {//原版这个构造器不是私有
map = new HashMap<>();
}
public static EventBus getDefault() {
if (instance == null) {
synchronized (EventBus.class) {
if (instance == null) {
instance = new EventBus();
}
}
}
return instance;
}
public void register(Object subscriber) {}
public void unRegister(Object subscriber) {}
public void post(Object event) {}
}
register 方法
EventBus 3.x 采用注解区分是否订阅方法,因此先新建一个注解接口 Subscribe,生命周期记得设为 Runtime,否则后面会反射不到。
@Retention(RetentionPolicy.RUNTIME)
public @interface Subscribe {}
然后去写 EventBus 类下的register()
方法。拿到订阅者的 class,通过反射获取所有已声明的方法,遍历之。
当发现此方法有@Subscribe
注解,便拿到它的 class,拿到它的参数类型(这里只考虑一个参数)。
根据参数类型从 Map 里取出订阅方法集合,如为空则新建,然后 new 出订阅类,放入集合中。
public void register(Object subscriber) {
Class<?> clazz = subscriber.getClass();
//这里其实可能有NoClassDefFoundError,原版在捕获块里用的是getMethods()
Method[] methods = clazz.getDeclaredMethods();
for (Method m : methods) {
if (m.isAnnotationPresent(Subscribe.class)) {
Subscribe s = m.getAnnotation(Subscribe.class);
//原版在这区分了不同参数列表的情况
Class<?> c = m.getParameterTypes()[0];
List<Subscription> list = map.get(c);
if (list == null) {
list = new ArrayList<>();
map.put(c, list);
}
list.add(new Subscription(m, subscriber));
}
}
}
unRegister 方法
这个简单了,就是把上一个方法反过来执行——反射获取方法组,遍历,遇到有@Subscribe
注解的方法如法炮制,拿到集合,干掉对应的订阅类即可。
public void unRegister(Object subscriber) {
Class<?> clazz = subscriber.getClass();
Method[] methods = clazz.getDeclaredMethods();
for (Method m : methods) {
if (m.isAnnotationPresent(Subscribe.class)) {
Class<?> c = m.getParameterTypes()[0];
List<Subscription> list = map.get(c);
if (list != null) {
for (Subscription s : list) {
if (s.subscriber == subscriber) {
list.remove(s);
}
}
}
}
}
}
post 方法
这个更简单,根据事件,从 Map 里取出订阅方法集合,遍历并调用就 ok。
public void post(Object event) {
Class<?> clazz = event.getClass();
List<Subscription> list = map.get(clazz);
if (list == null) {
return;//这里最好抛异常或打印日志,提醒调用者「没有任何类订阅该事件」
}
for (Subscription s : list) {
try {
s.method.invoke(s.subscriber, event);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
好,属于你的 EventBus 已初步搞定!马上测试一下吧。
新建一个事件类 TestEvent,携带一个字符串字段。
public class TestEvent {
private String text;
public TestEvent(String text) {
this.text = text;
}
public String getText() {
return text;
}
}
新建一个活动 SecondActivity 作为发送者,里面放个按钮。
点击后 post 一个 TestEvent ,传入一句话后关闭活动。
布局文件非常简单就不上传了,有兴趣的可以下载源码。
public class SecondActivity extends AppCompatActivity
implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
findViewById(R.id.ac_second_btn1).setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.ac_second_btn1:
EventBus.getDefault()//发送
.post(new TestEvent("Hello EventBus!"));
finish();
break;
}
}
}
新建一个活动 MainActivity 作为订阅者,里面一个按钮一个文本。
在活动的生命周期方法onCreate()
里注册,onDestory()
里反注册,再写一个订阅方法(名字可以随便取了,不像 2.x 版必须取那几个固定的名字),加上@Subscribe
注解。把接收到的话展示在文本上再换个颜色。
public class MainActivity extends AppCompatActivity
implements View.OnClickListener {
private TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
EventBus.getDefault().register(this);//注册
tv = (TextView) findViewById(R.id.ac_main_tv);
findViewById(R.id.ac_main_btn).setOnClickListener(this);
}
@Subscribe//加注解
public void testFoo(TestEvent event) {//订阅方法(接收)
tv.setText(event.getText());
tv.setTextColor(getResources().getColor(R.color.colorAccent));
}
@Override
protected void onDestroy() {
super.onDestroy();
EventBus.getDefault().unRegister(this);//反注册
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.ac_main_btn:
startActivity(new Intent(MainActivity.this,
SecondActivity.class));
break;
}
}
}
运行之,给力!
这也太简单了吧?答案当然是 no。一个健壮的框架,需要考虑太多东西,比如代码的可拓展性和可读性,性能优化,可测试性,兼容性,极端情况等等。
心灵鸡汤不常说「二八定理」么,这在编程界其实非常适用!一个好程序 80% 的功能,是由它 20% 代码去实现的;剩下 80% 的代码负责的,除去那 20% 的功能,还有各种查漏补缺 and 重构优化。
下面我们来简单的扩展一下这个 EventBus 吧,让它能区分主线程(MainThread)和发送线程(PostThread)。
原版 EventBus 默认的 ThreadMode 是「发送线程」,我们也如法炮制,先新建一个常量管理类 ThreadMode。
public class ThreadMode {//原版这里用的是枚举类,我个人更喜欢写成静态常量
public static final int POST_THREAD = 0;//发送线程
public static final int MAIN_THREAD = 1;//主线程
}
然后修改注解接口 Subscribe,添加一个字段threadMode()
,默认为发送线程。
@Retention(RetentionPolicy.RUNTIME)
public @interface Subscribe {
int threadMode() default ThreadMode.POST_THREAD;
}
新建一个类 MainThreadHandler 继承 android.os.Handler,用于把消息从发送线程传递给主线程。
public class MainThreadHandler extends Handler {
private Object event;
private Subscription subscription;
public MainThreadHandler(Looper looper) {//注意构造器里带上looper
super(looper);
}
public void post(Subscription subscription, Object event) {
this.subscription = subscription;
this.event = event;
sendMessage(Message.obtain());
}
@Override
public void handleMessage(Message msg) {
try {
subscription.method.invoke(subscription.subscriber, event);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
修改订阅类 Subscription 的构造器,让它能分辨线程模式。
public class Subscription {
public int mode;
public Method method;
public Object subscriber;
public Subscription(Method method, Object subscriber, int mode) {
this.method = method;
this.subscriber = subscriber;
this.mode = mode;
}
}
修改主类 EventBus,增加一个成员变量 handler,在构造器里初始化。
然后修改register()
和post()
两个方法,unRegister()
不用动。
private MainThreadHandler handler;
private EventBus() {
map = new HashMap<>();
handler = new MainThreadHandler(Looper.getMainLooper());
}
public void register(Object subscriber) {
Class<?> clazz = subscriber.getClass();
Method[] methods = clazz.getDeclaredMethods();
for (Method m : methods) {
if (m.isAnnotationPresent(Subscribe.class)) {
Subscribe s = m.getAnnotation(Subscribe.class);
Class<?> c = m.getParameterTypes()[0];
List<Subscription> list = map.get(c);
if (list == null) {
list = new ArrayList<>();
map.put(c, list);
}
switch (s.threadMode()) {
case ThreadMode.POST_THREAD:
list.add(new Subscription(m, subscriber,
ThreadMode.POST_THREAD));
break;
case ThreadMode.MAIN_THREAD:
list.add(new Subscription(m, subscriber,
ThreadMode.MAIN_THREAD));;
break;
default:
break;
}
}
}
}
public void post(Object event) {
Class<?> clazz = event.getClass();
List<Subscription> list = map.get(clazz);
if (list == null) {
return;
}
for (Subscription s : list) {
switch (s.mode) {
case ThreadMode.POST_THREAD:
try {
s.method.invoke(s.subscriber, event);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
break;
case ThreadMode.MAIN_THREAD:
handler.post(s, event);
break;
default:
break;
}
}
}
好了,测试一下!把 SecondActivity 的onClick()
方法改一改。
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.ac_second_btn1:
new Thread(new Runnable() {//另起一个线程发送
@Override
public void run() {
EventBus.getDefault()
.post(new TestEvent("Hello EventBus!"));
}
}).start();
finish();
break;
}
}
然后给 MainActivity 的订阅方法的注解添加字段。
@Subscribe(threadMode = MAIN_THREAD)//在主线程里处理
public void testFoo(TestEvent event) {//接收
tv.setText(event.getText());
tv.setTextColor(getResources().getColor(R.color.colorAccent));
}
效果和上面的动图是一样的。如法炮制,你能给它加入更多的功能。
留一个问题给读者思考:如果上一段不写(threadMode = MAIN_THREAD)
,执行结果是文本上只有一个 Hello 而且没有变颜色,这是什么原因呢?
本文结束!欢迎拍砖指教。
文章代码下载