EventBus框架想必做过Android开发的或多或少接触过,使用过;它是一款Android/Java的发布-订阅事件总线框架,简化了Android组件间消息通信的过程,将我们从复杂的组件内通信解脱出来,其Github地址EventBus;既然它这么好用,那不对它进行一番解析,或者说仿写一款同样的框架,实在说不过去;然后在加深对它的理解的同时,站在作者的角度去解决问题,也能提高我们平时的开发质量;本篇文章重点不是对它进行源码解析(源码分析放在后续文章),而是仿写一款事件总线框架
有同学可能觉得对事件总线的理解有点拗口,其实事件总线是对发布-订阅模式的一种实现,它是一种集中式事件处理机制,允许不同的组件之间进行通信,又不需要相互依赖,达到解耦的目的
我们平时常用的组件通信方式有:
正是因为Google提供的api有各种各样的缺点,所以就产生了一些通信框架,比如RxBus,EventBus等,这里我们对EventBus进行分析
我从以下几个步骤来一步步实现仿写:
使用过EventBus的都知道,它包含三个角色:
同时EventBus还支持线程切换,默认有四种线程模型:
使用方法很简单:
引入依赖
implementation 'org.greenrobot:eventbus:3.1.1'
封装事件对象
public class MessageObject {
public final String message;
public void setMessage(String message){
this.message = message
}
public String getMessage(){
return message;
}
}
发布事件
任何对象在任何线程都可以发布事件
EventBus.getDefault().post(new MessageObject ());
注册事件
只要你对别人发布的事件感兴趣,就可以注册该事件,在订阅方法里接收事件
@Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
@Override
public void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(MessageObject event) {
/* Do something */
};
使用一张图来说明
有很多的订阅者与发布者,它们都由代理中心去管理,维护它们的关系;仔细一想,其实这跟观察者模式是有点像的(观察者模式可参考Android面试题–设计模式之观察者模式的通俗易懂实现 与发布/订阅框架有区别解析);只不过这里将发布者与订阅者之间解耦了,将所有的订阅者维护在一个Map里
注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的上面,用来对这些元素进行标记说明,它本身不会在运行时起什么作用,需要我们编写注解处理器处理这些注解(编译时注解),或者在程序运行时通过反射得到这些注解做出相应的处理(运行时注解)
每个注解都必须使用注解接口@interface进行声明,这实际上会创建一个Java接口,也会编译成一个class文件,注解接口内部的元素声明实际上是方法声明,方法没有参数,没有throws语句,也不能使用泛型
注解又分为标准注解、编译时注解和运行时注解:
标准注解:Java API中默认定义的注解我们称为标准注解,它们定义在java.lang、java.lang.annotation、javax.annotation中;按照使用场景不同又可以分为三类:
编译相关注解:编译相关的注解是供编译期使用的,如@Override:编译器会检查被注解的方法是否真的重写了父类的方法,没有的话编译器会提示错误;@Deprecated:用来修饰任何不再鼓励使用或被弃用的方法
资源相关注解:这个一般用在JavaEE领域,在Android开发中没有用到,比如@Resource:用于Web容器的资源注入,表示单个资源
元注解:这个一般用来定义和实现注解的注解,也就是用来修饰注解的,总共有如下5种:
编译时注解:要定义编译时注解只需在定义注解时使用@Retention(RetentionPolicy.SOURCE)或者@Retention(RetentionPolicy.CLASS)修饰即可,编译时注解能够自动处理Java源文件并生成更多源码、配置文件、脚本等; 实现手段有APT(注解处理器)和JavaPoet(自动生成代码),在编译期完成操作,像@Nullable@NonNull这类的注解就是编译时注解;一些开源框架如BufferKnife,阿里路由 ARout、Dagger、Retrofit等都有使用编译时注解
运行时注解:只需在定义注解时使用@Retention(RetentionPolicy.RUNTIME)修饰即可;运行时注解一般和反射配合使用,相比编译时注解,性能较低,但是灵活,实现方便;像@Subscribe@Autowired等都是通过反射API进行操作,otto、EventBus等框架会使用运行时注解
EventBus就定义了运行时注解@Subscribe,当有事件发布时,只要某个订阅者内部有方法添加了该注解,且事件类型与订阅的方法参数类型一致,那这个方法就会被调用,接受事件
实现步骤:
项目结构如图
线程模式的意思在上面已经介绍过了,为了业务需要,有时候需要进行线程切换,它最后是作为注解的参数使用
/**
* Author: Mangoer
* Time: 2019/5/15 20:41
* Version:
* Desc: TODO(线程模型)
*/
public enum ThreadMode {
POSTING,
MAIN,
BACKGROUND,
ASYNC
}
注解是跟类处于同一层次的东西,使用它对订阅的方法进行标记,方便我们在运行时获取到它,如下
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Subscribe {
//定义线程模型,指定默认值
ThreadMode threadMode() default ThreadMode.POSTING;
}
以 @interface 格式声明注解,为了使这个注解有效,还需要在上面至少添加两个注解@Target 和 @Retention,它们的含义在上面已经介绍过,显然我们需要使用ElementType.METHOD和RetentionPolicy.RUNTIME;至于@Inherited注解是允许该注解被子类继承
要知道我们的订阅方法携带的信息比较多,比如:
@Subscribe(threadMode = ThreadMode.MAIN)
public void showMsg(String msg){
}
首先它有注解修饰,还有方法参数,还有方法本身,为了方便处理,我们对其进行封装
/**
* Author: Mangoer
* Time: 2019/5/15 20:23
* Version:
* Desc: TODO(订阅者对象)
*/
public class SubscriptionMethod {
//订阅方法
private Method method;
//订阅方法的参数类型
private Class> type;
//注解里的参数
private ThreadMode threadMode;
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public Class> getType() {
return type;
}
public void setType(Class> type) {
this.type = type;
}
public ThreadMode getThreadMode() {
return threadMode;
}
public void setThreadMode(ThreadMode threadMode) {
this.threadMode = threadMode;
}
}
这一步其实是当组件进行注册时需要进行处理的,在这里将其内部使用注解修饰的订阅方法保存起来;当有发布者发布消息时再去调用该方法
这里定义一个代理类处理所有订阅者以及发布者的消息通信
/**
* Author: Mangoer
* Time: 2019/5/15 20:20
* Version:
* Desc: TODO(代理类)
*/
public class MangoBus {
/**
* 订阅方法
* key 订阅类
* value 类中的订阅方法集合
*/
private Map
接着添加注册方法,获取订阅方法
/**
* 供订阅者调用 进行注册
* 取出订阅者的所有订阅方法,将其保存到mSubcription
* @param subscriber
*/
public void register(Object subscriber){
List methodList = mSubcription.get(subscriber);
if (methodList == null) {
Class> clazz = subscriber.getClass();
methodList = new ArrayList<>();
/**
* 某些情况下我们继承了父类,但父类并没有注册,只提供订阅方法,让子类去注册
* 那么就需要将父类的订阅方法也保存起来
*/
while (clazz != null) {
String className = clazz.getName();
if (className.startsWith("java.") || className.startsWith("javax.")
|| className.startsWith("android.")) {
break;
}
findAnnotationMethod(methodList,clazz);
clazz = clazz.getSuperclass();
}
mSubcription.put(subscriber,methodList);
}
}
private void findAnnotationMethod(List methodList, Class> clazz){
//获取订阅者自身的所有方法,而getMethod会将父类的方法也拿到
Method[] m = clazz.getDeclaredMethods();
int size = m.length;
for (int i=0; i[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length != 1) {
throw new MangoBusException("方法参数个数必须是一个");
}
//这里就需要实例化订阅方法对象了
SubscriptionMethod subscriptionMethod = new SubscriptionMethod();
subscriptionMethod.setMethod(method);
subscriptionMethod.setType(parameterTypes[0]);
subscriptionMethod.setThreadMode(annotation.threadMode());
methodList.add(subscriptionMethod);
}
}
这里需要用到一个自定义的异常类
public class MangoBusException extends RuntimeException {
public MangoBusException(String message) {
super(message);
}
}
获取有效的订阅方法步骤如下:
接下来就是发布者发布消息了,那就得提供一个发布的方法
/**
* 发布事件
* 根据参数类型找出对应的方法并调用
* @param event
*/
public void post(Object event){
Set
这里主要就是从Map中遍历出符合要求的方法,然后执行它,到这里基本上就完成的差不多了,现在来使用下看看:现在有两个activity,第一个activity注册,从第一个activity跳转到第二个activity,第二个activity发布事件,然后结束自己回到第一个activity,看看第一个activity能不能收到刚才发布的事件
第一个activity
public class MainActivity extends AppCompatActivity {
TextView intent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MangoBus.getInstance().register(this);
intent = findViewById(R.id.intent);
intent.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(MainActivity.this,OtherActivity.class));
}
});
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void subscribeEvent(Event event){
intent.setText(event.getMsg());
}
}
第二个activity
public class OtherActivity extends AppCompatActivity {
Button push;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_other);
push = findViewById(R.id.push);
push.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
MangoBus.getInstance().post(new Event("OtherActivity"));
finish();
}
});
}
}
可以看到第一个activity的textview内的文字变化了,说明这套流程已经通了
线程切换的功能这里要通过注解的参数【线程模型】来进行动态设置
修改下post方法即可
public void post(final Object event){
Set set = mSubcription.keySet();
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
final Object next = iterator.next();
List methodList = mSubcription.get(next);
if (methodList == null || mSubcription.size() == 0) {
continue;
}
int size = methodList.size();
for (int i = 0; i < size; i++) {
final SubscriptionMethod method = methodList.get(i);
//method.getType()是获取方法参数类型,这里是判断发布的对象类型是否与订阅方法的参数类型一致
if (method.getType().isAssignableFrom(event.getClass())) {
//进行线程切换
switch (method.getThreadMode()) {
case POSTING:
invoke(next,method,event);
break;
case MAIN:
//通过Looper判断当前线程是否是主线程
//也可以通过线程名判断 "main".equals(Thread.currentThread().getName())
if (Looper.getMainLooper() == Looper.myLooper()) {
invoke(next,method,event);
} else {
mHandler.post(new Runnable() {
@Override
public void run() {
invoke(next,method,event);
}
});
}
break;
case BACKGROUND:
if (Looper.getMainLooper() == Looper.myLooper()) {
THREAD_POOL_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
invoke(next,method,event);
}
});
} else {
invoke(next,method,event);
}
break;
case ASYNC:
THREAD_POOL_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
invoke(next,method,event);
}
});
break;
}
}
}
}
}
接下来验证下,在订阅方法中修改下,即在子线程接收事件,然后获取下线程名,看看是不是在子线程
@Subscribe(threadMode = ThreadMode.BACKGROUND)
public void subscribeEvent(Event event){
intent.setText(event.getMsg()+"-"+Thread.currentThread().getName());
}
可以看到线程名不是 【main】,说明这里的线程切换是正常的
最后需要提供一个方法供订阅者取消订阅,防止出现内存泄漏
/**
* 取消订阅
* @param target
*/
public void unRegister(Object target){
List methodList = mSubcription.get(target);
if (methodList == null) return;
methodList.clear();
mSubcription.remove(target);
}
到这里我们的仿写EventBus框架就结束了,使用到的核心技术就是运行时注解和反射
代码可从Github下载