github:https://github.com/greenrobot/EventBus
etting started guide:https://greenrobot.org/eventbus/documentation/how-to-get-started/
使用示例:https://github.com/greenrobot-team/greenrobot-examples
什么是EventBus
EventBus是Android/Java的发布-订阅事件总线框架。
事件总线是对发布-订阅模式的一种实现,它是一种集中式事件处理机制,允许不同的组件之间进行通信,又不需要相互依赖,达到解耦的目的。
常见的组件间通信方式
Intent
使用它实现组件跳转,并且能携带参数,但只能携带少量数据,同时在跨组件通信时局限性比较大
缺点:
1.只能携带少量数据
2.跨组件通信时局限性比较大,比如页面逻辑是A->B->C,有一些数据需要从A传给C,这时候B就会做一些无用功,数据会先传到B,再从B传到C。
Handler
使用Handler进行组件通信耦合严重,容易造成内存泄漏
Interface
比如A,B都实现同一个接口,A直接调用B的方法直接进行通信
缺点:
1.需要实现同一个接口,耦合度高
2.不能跨线程通信
Broadcast
主要用于接收系统通知。
缺点:
1.效率不高,安全性不高
AIDL
跨进程通信
缺点:
1.使用成本高
Messenger
Messenger和AIDL都是跨进程通信,Messenger比AIDL方便。
总结
正是因为Google提供的各种通信机制和API有各种各样的缺点,所以就产生了一些第三方的开源的通信框架,比如RxBus,EventBus等。
思考一个问题
如果市场上没有EventBus以及类似的第三方通信框架,如何更加优雅的实现大量组件间的信息通信?
一种思路:
EventBus优点
1.代码简单,快;
2.jar包小,约50K;
3.Activity、Fragment以及线程间通信优秀;
4.稳定,在1亿+应用中得到实践;
google不建议在两个fragment之间直接进行通信,为什么呢?因为google推出fragment的一个核心点是希望可以复用fragment,如果A fragment直接和B fragment进行通信,那么耦合度会太高,那么后续如果想直接复用B fragment就会很麻烦,所以google不建议在两个fragment之间直接进行通信。
EventBus原理
即事件订阅者将内部标注了注解的方法注册到EventBus,当发布者调用了post方法发送了一个事件时,EventBus会调用对应的方法。
EventBus特性
-
simplifies the communication between components
decouples event senders and receivers
performs well with Activities, Fragments, and background threads
avoids complex and error-prone dependencies and life cycle issues -
makes your code simpler
-
is fast
-
is tiny (~60k jar)
-
is proven in practice by apps with 1,000,000,000+ installs
-
has advanced features like delivery threads, subscriber priorities, etc.
delivery threads
即交付线程、线程传送
EventBus可以为您处理线程:事件(events)可以在不同于发布线程(the posting thread)的线程中发布(be posted)。一个常见的用例是处理UI更改。在Android中,UI更改必须在UI(主)线程中完成。另一方面,网络或任何耗时的任务都不能在主线程上运行。EventBus帮助您处理这些任务并与UI线程同步(开发者无需深入研究线程转换、无需使用AsyncTask等)。
在EventBus中,可以使用五种ThreadMode中的一种来定义调用事件处理方法的线程。
- ThreadMode: POSTING
- ThreadMode: MAIN
- ThreadMode: MAIN_ORDERED
- ThreadMode: BACKGROUND
- ThreadMode: ASYNC
ThreadMode: POSTING
将在发布事件的同一线程中调用订阅者,这是默认值。事件交付是同步完成的,一旦发布完成,将调用所有订阅者。这种ThreadMode意味着开销最少,因为它完全避免了线程切换。因此,对于已知不需要主线程就可以在很短的时间内完成的简单任务,推荐使用这种模式。使用此模式的事件处理程序(event handler)应快速返回,以避免阻塞可能是主线程的发布线程。示例:
// Called in the same thread (default)
// ThreadMode is optional here
@Subscribe(threadMode = ThreadMode.POSTING)
public void refreshMessage(EventData eventData) {
Log.i(TAG, "method:refreshMessage#" + eventData.getUserName() + ":\n\n" + eventData.getMessage());
}
ThreadMode: MAIN
订阅者将在Android的主线程中调用(有时称为UI线程)。如果发布线程是主线程,则将直接调用事件处理程序方法(与ThreadMode.POSTING 中描述的同步)。使用此模式的事件处理程序必须快速返回,以避免阻塞主线程。示例:
// Called in Android UI's main thread
@Subscribe(threadMode = ThreadMode.MAIN)
public void refreshMessage(EventData eventData) {
Log.i(TAG, "method:refreshMessage#eventData=" + eventData);
mTvMessage.setText(eventData.getUserName() + ":\n\n" + eventData.getMessage());
}
ThreadMode: MAIN_ORDERED
订阅者将在Android的主线程中被调用。事件总是排队(enqueue)等待稍后交付给订阅者,因此对post的调用将立即返回。这使得事件处理的顺序更加严格和一致(因此命名为MAIN_ORDERED)。例如,如果您使用主线程模式在事件处理程序中发布另一个事件,第二个事件处理程序将在第一个事件处理程序之前完成(因为它是同步调用的——将其与方法调用进行比较)。使用MAIN_ORDERED,第一个事件处理程序将完成,然后在稍后的时间点调用第二个事件处理程序(只要主线程具有容量)。
使用此模式的事件处理程序必须快速返回,以避免阻塞主线程。示例:
// Called in Android UI's main thread
@Subscribe(threadMode = ThreadMode.MAIN_ORDERED)
public void refreshMessage(EventData eventData) {
Log.i(TAG, "method:refreshMessage#eventData=" + eventData);
mTvMessage.setText(eventData.getUserName() + ":\n\n" + eventData.getMessage());
}
ThreadMode: BACKGROUND
订阅者将在后台线程中调用。如果发布线程不是主线程,则将在发布线程中直接调用事件处理程序方法。如果发布线程是主线程,则EventBus使用一个后台线程,该线程将按顺序交付所有事件。使用此模式的事件处理程序应尝试快速返回,以避免阻塞后台线程。示例:
// Called in the background thread
@Subscribe(threadMode = ThreadMode.BACKGROUND)
public void refreshMessage(EventData eventData) {
Log.i(TAG, "method:refreshMessage#eventData.getUserName() + ":\n\n" + eventData.getMessage());
}
ThreadMode: ASYNC
事件处理程序方法在各自单独的线程中调用。这总是独立于发布线程和主线程。发布事件永远不要等待使用此模式的事件处理程序方法。如果事件处理程序方法的执行可能需要一些时间,例如网络访问,则应使用此模式。避免同时触发大量长时间运行的异步处理程序方法来限制并发线程的数量。EventBus使用线程池有效地重用来自已完成的异步事件处理程序通知的线程。示例:
// Called in a separate thread
@Subscribe(threadMode = ThreadMode.ASYNC)
public void refreshMessage(EventData eventData) {
Log.i(TAG, "method:refreshMessage#eventData.getUserName() + ":\n\n" + eventData.getMessage());
}
EventBus手写实现核心原理
分析了EventBus原理后,通过手写实现EventBus核心原理来解构EventBus,更透彻的理解EventBus。
实现步骤:
- 创建线程模式
- 创建注解
- 封装方法类
- 获取被注解的方法,通过反射获取被注解的方法,在发布时调用它
- 线程切换
创建线程模式
EventBus支持订阅者方法在不同的线程中被调用。可以使用指定线程模式来指定订阅者方法运行在哪个线程,EventBus中支持五种线程模式:
ThreadMode.POSTING
ThreadMode.MAIN
ThreadMode.MAIN_ORDERED
ThreadMode.BACKGROUND
ThreadMode.ASYNC
public enum ThreadMode {
POSTING,
MAIN,
BACKGROUND,
MAIN_ORDERED,
ASYNC
}
创建注解
使用它对订阅的方法进行标记,使得我们在运行时获取到订阅方法。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Subscrible {
ThreadMode threadMode() default ThreadMode.POSTING;
}
封装方法类
订阅方法包含多个部分,比如:
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(MessageEvent event) {
Toast.makeText(this, "接收到点击事件", Toast.LENGTH_SHORT).show();
}
包含方法名,方法参数,注解,需要一个类对其进行封装。
/**
* 订阅方法的封装类
*/
public class SubscriberMethod {
final Method method;
final ThreadMode threadMode;
final Class<?> eventType;
public SubscriberMethod(Method method, ThreadMode threadMode, Class<?> eventType) {
this.method = method;
this.threadMode = threadMode;
this.eventType = eventType;
}
}
获取被注解的方法
通过反射获取被注解的方法,在发布时调用它。
当组件进行注册时需要获取其内部使用了注解修饰的订阅方法,并保存起来;当有发布者发布消息时再去调用该方法。
/**
* 供订阅者调用
*
* @param subscriber
*/
public synchronized void register(Object subscriber) {
List<SubscriberMethod> 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);
}
}
/**
* 寻找使用了Subscribe注解修饰的方法
* @param methodList
* @param clazz
*/
private void findAnnotationMethod(List<SubscriberMethod> methodList, Class<?> clazz) {
//获取订阅者自身的所有方法,用getDeclaredMethods(),而getMethod()会将父类的方法也获取到
Method[] m = clazz.getDeclaredMethods();
int size = m.length;
for (int i = 0; i < size; i++) {
Method method = m[i];
//拿到该方法的注解,找到使用Subscribe注解的方法
Subscribe annotation = method.getAnnotation(Subscribe.class);
if (annotation == null)
continue;
/**
* 到这里说明该方法使用了我们定义的Subscribe注解
* 接下来需要判断该注解了的方法是否符合规范:
* 1. 返回值必须是void
* 2. 方法修饰符必须是public,且是非静态抽象的
* 3. 方法参数必须只有一个
*/
//如果方法返回类型不是void 抛出异常
Type genericReturnType = method.getGenericReturnType();
if (!"void".equals(genericReturnType.toString())) {
throw new MyEventBusException("方法返回值必须是void");
}
//如果方法修饰符不是public 抛出异常
int modifiers = method.getModifiers();
if ((modifiers & Modifier.PUBLIC) != 1) {
throw new MyEventBusException("方法修饰符必须是public,且是非静态,非抽象");
}
//如果方法参数不是一个 抛出异常
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length != 1) {
throw new MyEventBusException("方法参数个数必须是一个");
}
//实例化订阅方法对象
SubscriberMethod subscriptionMethod = new SubscriberMethod(method, annotation.threadMode(), parameterTypes[0]);
methodList.add(subscriptionMethod);
}
}
有注册方法就有取消注册方法:
public synchronized void unregister(Object subscriber) {
List<SubscriberMethod> methodList = mAllSubscriberMethods.get(subscriber);
if (methodList == null)
return;
methodList.clear();
mAllSubscriberMethods.remove(subscriber);
}
接下来就是提供一个发布者发布消息的方法:
public void post(Object event){
Set<Object> set = mSubcription.keySet();
Iterator<Object> iterator = set.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
List<SubscriberMethod> methodList = mSubcription.get(next);
if (methodList == null || mSubcription.size() == 0) {
continue;
}
int size = methodList.size();
for (int i = 0; i < size; i++) {
SubscriberMethod method = methodList.get(i);
//method.getEventType()是获取方法参数类型,这里是判断发布的对象类型是否与订阅方法的参数类型一致
if (method.getEventType().isAssignableFrom(event.getClass())) {
invoke(next,method,event);
}
}
}
}
private void invoke(Object next, SubscriberMethod method, Object event) {
Method m = method.getMethod();
try {
m.invoke(next,event);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
主要是遍历Map,获取和发送的事件相匹配的方法,然后调用该方法。
使用测试:
第一个activity注册了MyEventBus,并有一个订阅方法,然后带年纪进入第二个activity,在第二个activity中点击按钮发送一个事件,看看第一个activity中能否收到这个事件。
第一个activity:
/**
* 手写MyEventBus,实现EventBus的核心原理
*/
public class MyEventBusMainActivity extends Activity {
private String TAG = "MyEventBusMainActivity";
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_myeventbus_main);
button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MyEventBusMainActivity.this, MyEventBusSecondActivity.class);
startActivity(intent);
}
});
}
@Override
public void onStart() {
super.onStart();
//register()方法会在参数activity中找到所有标注了@Subscribe注解的方法保存到EventBus中
MyEventBus.getDefault().register(this);
}
@Override
public void onStop() {
super.onStop();
MyEventBus.getDefault().unregister(this);
}
@Subscribe
public void clickEvent(ClickMessageEvent clickMessageEvent) {
Toast.makeText(this, "MyEventBusMainActivity接收到点击事件", Toast.LENGTH_SHORT).show();
}
}
第二个activity:
public class MyEventBusSecondActivity extends Activity {
private String TAG = "MyEventBusSecondActivity";
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_myeventbus_second);
button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
MyEventBus.getDefault().post(new ClickMessageEvent());
}
});
}
}
测试结果:
第一个activity中接收到了点击事件!
线程切换
线程切换功能主要是通过Subscribe注解的ThreadMode参数进行设置的。
修改下post方法:
/**
* 发布事件的方法
* @param event
*/
public void post(final Object event){
Set<Object> set = mAllSubscriberMethods.keySet();
Iterator<Object> iterator = set.iterator();
while (iterator.hasNext()) {
final Object next = iterator.next();
List<SubscriberMethod> methodList = mAllSubscriberMethods.get(next);
if (methodList == null || mAllSubscriberMethods.size() == 0) {
continue;
}
int size = methodList.size();
for (int i = 0; i < size; i++) {
final SubscriberMethod method = methodList.get(i);
//method.getEventType()是获取方法参数类型,这里是判断发布的对象类型是否与订阅方法的参数类型一致
if (method.getEventType().isAssignableFrom(event.getClass())) {
//进行线程切换
switch (method.getThreadMode()) {
case POSTING:
invoke(next,method,event);
break;
case MAIN:
//通过Looper判断当前线程是否是主线程
if (Looper.getMainLooper() == Looper.myLooper()) {
invoke(next,method,event);
} else {
mMainHandler.post(new Runnable() {
@Override
public void run() {
invoke(next,method,event);
}
});
}
break;
case BACKGROUND:
if (Looper.getMainLooper() == Looper.myLooper()) {
executorService.execute(new Runnable() {
@Override
public void run() {
invoke(next,method,event);
}
});
} else {
invoke(next,method,event);
}
break;
case ASYNC:
executorService.execute(new Runnable() {
@Override
public void run() {
invoke(next,method,event);
}
});
break;
}
}
}
}
}
新订阅一个方法,threadMode指定在子线程接收事件,看看是否真的在子线程接收到事件:
@Subscribe(threadMode = ThreadMode.BACKGROUND)
public void threadEvent(ThreadMessageEvent threadMessageEvent) {
Log.i(TAG, "threadEvent(), MyEventBusMainActivity接收到threadMode事件, thread=" + Thread.currentThread().getName());
}
运行结果:
MyEventBusMainActivity: threadEvent(), MyEventBusMainActivity接收到threadMode事件, thread=pool-2-thread-1
参考:
注解反射的高级技巧,让你彻底了解EventBus是如何进行组件通信
Android通过仿写EventBus组件通信框架 掌握运行时注解+反射及事件总线通信核心原理
组件通信框架: EventBus使用详解和手写简易EventBus框架
EventBus解析并实现手写EventBus