本分析基于的代码可以在这里clone到,强烈建议边看代码边浏览本文,本文章主要分析eventbus的核心思想,因为版本差异,有一些细节可能不太一样,但我建议各位不要陷入细节无法自拔,站在较高角度去吃透它的思想才是我们的目的,因此,一些基本概念已经有很多文章已经讲过,下文不再赘述
分析之前的思考
- eventbus的用法大家都很清楚,请看下面一种最简单的情况(一定要对照着上述地址的代码看,一些用法和当前版本的EventBus有名称上的差异),我们很清楚,在PostActivity.java中当调用了
Bus.getDefault().post("Hello EventBus");
这句代码后,RegisterActivity类中的onThreadEvent会被调用,而且是在子线程中.
// 在RegisterActivity.java类中的逻辑
// onCreate函数中
Bus.getDefault().register(this);
// onDestroy函数中
Bus.getDefault().unRegister(this);
//一个接收函数
@BusReceiver(mode = EventMode.Thread)
public void onThreadEvent(final String event) {
Log.v(TAG, "onThreadEvent=" + Thread.currentThread().getName());
appendLog("onThreadEvent event=" + event
+ " thread=" + Thread.currentThread().getName());
}
// 在PostActivity.java类中的逻辑
// onCreate函数中
Bus.getDefault().post("Hello EventBus");
- 我们不妨思考一下,如果是我们自己实现,我们需要考虑哪些问题,我在下面列出了一部分我们需要考虑的核心问题
- 我们需要保存注册类的哪些信息,该如何保存?
- 我们需要保存参数的哪些信息,该如何保存?
- 我们该如何确定哪些函数会被调用?
- 我们该如何控制他们在特定的线程中运行?
- 我们需不需要考虑缓存的功能?如何提升框架的性能?
- 我们接下来就一个一个解决上述的问题,再强调一遍,此时你一定要有一份完整的源代码,我的分析只能作为一个引子,讲解核心的思想,之后还需要你深入代码中把它融会贯通
4个重要的数据结构
- 标识注解结构非常基础,直接看代码,Bus.EventMode是个枚举类型,有三种值,Sender,Main,Thread,分别标识着不同的线程
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BusReceiver {
// 默认回调的方法在主线程中执行
Bus.EventMode mode() default Bus.EventMode.Main;
}
- 先解决一个重要的问题,我们在存储什么?该如何存储,我们存储的是有特殊注解的方法(Method对象),这个方法中的参数,注册的对象以及相应的注解,所以我们拼装出来了第一个数据结构
MethodInfo
public class MethodInfo {
// 方法对象
public final Method method;
// 注册对象的Class对象
public final Class<?> targetType;
// 参数的Class对象
public final Class<?> eventType;
// 没有什么关键的作用
public final String name;
// 注解的mode值
public final Bus.EventMode mode;
public MethodInfo(final Method method, final Class<?> targetClass, final Bus.EventMode mode) {
this.method = method;
this.targetType = targetClass;
this.eventType = method.getParameterTypes()[0];
this.mode = mode;
this.name = targetType.getName() + "." + method.getName()
+ "(" + eventType.getName() + ")";
}
// else codes
}
- 再封装一层,做到最大化解耦,让MehodInfo只负责存储相应数据结构的职责,让Subscriber去做具体的执行功能,请注意invoke函数
class Subscriber {
public final MethodInfo method;
public final Object target;
// 我认为下面4个成员变量是多余的,因为可以从MethodInfo中拿到
public final Class<?> targetType;
public final Class<?> eventType;
public final Bus.EventMode mode;
public final String name;
public Subscriber(final MethodInfo method, final Object target) {
this.method = method;
this.target = target;
this.eventType = method.eventType;
this.targetType = method.targetType;
this.mode = method.mode;
this.name = method.name;
}
// 使用反射调用注册类的相应函数,传递进来的event是post的内容
public Object invoke(Object event)
throws InvocationTargetException, IllegalAccessException {
return this.method.method.invoke(this.target, event);
}
// else codes
}
- 请注意,Subscriber只是负责了调用函数,但是没有去负责该在哪个线程中去执行,所以说,需要再封装一层,EventEmitter,它是一个Runnable对象,所以我们就可以使用Handler,Executor,控制它到底在哪个线程中执行!
public class EventEmitter implements Runnable {
private static final String TAG = Bus.TAG;
public final Bus bus;
public final Object event;
public final Subscriber subscriber;
public final Bus.EventMode mode;
public final boolean debug;
public EventEmitter(final Bus bus, final Object event,
final Subscriber subscriber, final boolean debug) {
this.bus = bus;
this.event = event;
this.subscriber = subscriber;
this.mode = subscriber.mode;
this.debug = debug;
}
@Override
public void run() {
try {
if (debug) {
Log.v(TAG, "sending event:[" + event
+ "] to subscriber:[" + subscriber
+ "] at thread:" + Thread.currentThread().getName());
}
subscriber.invoke(event);
} catch (Exception e) {
if (debug) {
Log.e(TAG, "sending event:[" + event + "] to subscriber:["
+ subscriber + "] failed, reason: " + e, e);
}
}
}
@Override
public String toString() {
return "{" +
"event:[" + event +
"] to subscriber:[" + subscriber +
"]}";
}
看完四个数据结构后,感叹它巧妙的设计,利用层层包装解耦,符合单一职责的原则,使结构清晰易于维护
注册时的行为
Bus.getDefault().register(this)
- 注册时我们主要做了两件事情
- 第一件事,以注册类的完整类名为key,它其中的所有满足条件的方法为Value,存储到一个集合中
final static Map<String, Set<MethodInfo>> sMethodCache = new ConcurrentHashMap<String, Set<MethodInfo>>();
第二件事以参数类型为key,符合的方法集合为Value存储为一个ConcurrentHashMap(以方法参数为标准分割函数)
private final Map<Class<?>, Set<Subscriber>> mSubscriberMap;
我们来看一下这个过程中的重要的方法
//获得注册对象中所有符合条件的函数,包括父类中的
private Set<MethodInfo> getMethods(Class<?> targetClass) {
// 完整的类名作为缓存的key
String cacheKey = targetClass.getName();
Set<MethodInfo> methods;
// 先看缓存是否有,如果没有去类中找寻符合要求的方法,然后在存储到缓存中
synchronized (Cache.sMethodCache) {
methods = Cache.sMethodCache.get(cacheKey);
}
if (methods == null) {
/**
* 找对象以及对象父类的符合要求的方法
*/
methods = mMethodFinder.find(this, targetClass);
synchronized (Cache.sMethodCache) {
Cache.sMethodCache.put(cacheKey, methods);
}
}
return methods;
}
- 跟入,看
mMethodFinder.find(this, targetClass);
的具体实现,代码很简单,请去MethodHelp.java类中查看,做一个简单的总结,此类的作用就是筛选出符合要求的Method拼装为MethodInfo集合,注意挑选的规则有以下几条(在isValidMethod函数中)
- 首先它必须拥有BusReceiver注解,这个是大前提,之后需满足以下四个小条件
!Modifier.isPublic(method.getModifiers())
函数必须是public的Modifier.isStatic(method.getModifiers())
函数不是静态的method.getParameterTypes().length != 1
函数只能有一个参数!Modifier.isVolatile(method.getModifiers())
这个条件很重要,是修复getDeclaredMethods存在的一个bug,请参考这里
public static Set<MethodInfo> findSubscriberMethodsByAnnotation(
final Class<?> targetClass) {
final MethodConverter converter = new MethodConverter() {
@Override
public MethodInfo convert(final Method method) {
// check annotation
final BusReceiver annotation = method.getAnnotation(BusReceiver.class);
if (annotation == null) {
return null;
}
if (!isValidMethod(method)) {
return null;
}
return new MethodInfo(method, targetClass, annotation.mode());
}
};
return findSubscriberMethods(targetClass, converter);
}
/**
* 根据传入的相应对象的class对象,去检索"那些要被回调的方法",存储起来,并且父类方法都要存储,其实就是加了一个while循环,
* 内部再加入一个for循环
*/
public static Set<MethodInfo> findSubscriberMethods(
final Class<?> targetClass, MethodConverter converter) {
Class<?> clazz = targetClass;
final Set<MethodInfo> methods = new HashSet<MethodInfo>();
while (!shouldSkipClass(clazz)) {
final Method[] clsMethods = clazz.getDeclaredMethods();
for (final Method method : clsMethods) {
final MethodInfo methodInfo = converter.convert(method);
if (methodInfo != null) {
methods.add(methodInfo);
}
}
// search more methods in super class
clazz = clazz.getSuperclass();
}
return methods;
}
post时的行为
- 存储以参数类型全称为key(如java.lang.String),此参数类型的父类型或者接口类型为value(父类型和接口类型也加入是因为考虑到了兼容性,这样不需要严格的匹配参数就可以调起函数)
以参数类型为key的原因是因为eventbus确定调用哪个方法就是由参数类型而不是函数名确定的,所以说只要你post时的参数与你声明的回调函数的参数兼容,就可以调用!
final static Map<String, Set<Class<?>>> sEventTypeCache = new ConcurrentHashMap<String, Set<Class<?>>>();
- 获得了参数类型的所有兼容的类型后,一次传递需要传递的值和相应的兼容的类型
for (Class<?> eventType : eventTypes) {
postEventByType(event, eventType);
}
- 跟入postEventByType函数
/**
* 发送某个事件给某个特定类型的订阅者
*
* @param event 事件对象
* @param eventType 匹配的事件类型
* @param <E> 事件类型
*/
private synchronized <E> void postEventByType(final E event, final Class<?> eventType) {
final Set<Subscriber> subscribers = mSubscriberMap.get(eventType);
if (subscribers == null || subscribers.isEmpty()) {
return;
}
for (Subscriber subscriber : subscribers) {
sendEvent(new EventEmitter(this, event, subscriber, mDebug));
}
}
- 前面说过EventEmitter的类型和作用,继续跟入sendEvent方法,此方法的作用是根据注解的模式选择相应的类去执行emitter的run方法,此方法逻辑比较简单,请去代码中查看Schedulers类,它会用工厂模式分别创建三个子类,SenderScheduler(直接调用emitter的run方法,以同步方式执行)负责在当前线程中执行,HandlerScheduler(依赖Handler)负责在主线程中执行,ExecutorScheduler(依赖Executor)负责在异步线程中执行,到最后就都会执行到Emitter的run方法中
public void sendEvent(EventEmitter emitter) {
if (mDebug) {
Log.v(TAG, "send event:" + emitter);
}
if (EventMode.Sender.equals(emitter.mode)) {
mSenderScheduler.post(emitter);
} else if (EventMode.Main.equals(emitter.mode)) {
if (Helper.isMainThread()) {
mSenderScheduler.post(emitter);
} else {
mMainScheduler.post(emitter);
}
} else if (EventMode.Thread.equals(emitter.mode)) {
mThreadScheduler.post(emitter);
}
}
- 看一下emitter的run方法,非常简单,到最后都会执行过来,只是过来的途径不同(上面分析的三个类负责),由下面可以看到是最终其实执行的是Subscriber的invoke方法,我们上面分析过,这个方法是用反射去调相应的method,到此就分析完毕了,最终就会成功的调用注册类中相应的方法了
@Override
public void run() {
try {
subscriber.invoke(event);
} catch (Exception e) {}
}
解注册时的行为
- 不知道大家有没有想过这个问题,post时,调用哪个方法是基于参数去判断的,那么,当我们解注册的时候,如何去确定解注册的那个类中的所有方法,我们需要把这些方法从mSubscriberMap中移除,要保证精确性而不能移除错误,因此,又多出来一个数据结构去做这件事情,它的key就是我们的类对象,它的value就是我们类对象中符合方法的所有的参数对象(想想为什么把参数对象作为value,而不是把方法对象作为value,因为我们有mSubscriberMap,可以根据参数对象取出方法对象,再通过方法对象Subscriber的target属性去和我们当前要解注册的对象对比,如果一致,那么把此方法对象从mSubscriberMap中删除)
private final Map<Object, Set<Class<?>>> mEventMap;
public <T> void unregister(final T target) {
if (mDebug) {
Log.v(TAG, "unregister() target:" + target);
mStopWatch.start("unregister()");
}
//mEventMap的添加元素逻辑在register方法中
final Set<Class<?>> eventTypes = mEventMap.remove(target);
if (eventTypes == null || eventTypes.isEmpty()) {
Log.v(TAG, "unregister() no subscriber for target:" + target);
return;
}
for (Class<?> eventType : eventTypes) {
Set<Subscriber> subscribers = mSubscriberMap.get(eventType);
if (subscribers == null || subscribers.isEmpty()) {
continue;
}
synchronized (mSubscriberMap) {
Iterator<Subscriber> it = subscribers.iterator();
while (it.hasNext()) {
final Subscriber subscriber = it.next();
if (subscriber.target == target) {
it.remove();
}
}
}
}
}
}
缓存处理
- events的性能瓶颈在两方面有体现.
- 第一,就是我们去查找某个类中符合特定要求的方法并存储,因为我们不止要查找当前类,还需要查找父类,所有这是一笔不小的开销
- 第二,我们要去把所有兼容的参数类型查找并存储,这也是一笔不小的开销
- 根据以上的描述,我们就用了两个数据结构把他们在第一次search时缓存了起来,一个是完整的类名和对应的所有符合要求的方法,另一个是完整的参数名和所有兼容的类型
static class Cache {
// key=注册类的完整类名 target.getClass().getName()
// value=注册类包含的合法的@BusReceiver方法对象集合
// targetTypeName->method set
final static Map<String, Set<MethodInfo>> sMethodCache =
new ConcurrentHashMap<String, Set<MethodInfo>>();
// key=事件类型的完整类名
// value=事件类型的所有父类和接口
// eventTypeName-> event type set
final static Map<String, Set<Class<?>>> sEventTypeCache =
new ConcurrentHashMap<String, Set<Class<?>>>();
}
- 它俩使用和填充位置分别在register()和unRegister()函数中,请在AndroidStudio中自己使用command + F和command + G自行查看.
可以优化的地方
- 当我们去搜寻一个类中符合要求的方法时,其实没有必要搜寻系统级别的类的方法,比如Object类中的方法,那么我们就可以做一些小小的优化,请看下面代码,代码很简单,不做赘述
public static boolean shouldSkipClass(final Class<?> clazz) {
if (clazz == null || Object.class.equals(clazz)) {
return true;
}
final String clsName = clazz.getName();
return clsName.startsWith("java.")
|| clsName.startsWith("javax.")
|| clsName.startsWith("android.")
|| clsName.startsWith("com.android.");
}