对于Android开发老司机来说肯定不会陌生,它是一个基于观察者模式的事件发布/订阅框架,开发者可以通过极少的代码去实现多个模块之间的通信,而不需要以层层传递接口的形式去单独构建通信桥梁。从而降低因多重回调导致的模块间强耦合,同时避免产生大量内部类。它拥有使用方便,性能高,接入成本低和支持多线程的优点,实乃模块解耦、代码重构必备良药。
作为Markus Junginger大神耗时4年打磨、超过1亿接入量、Github 9000+ star的明星级组件,分析EventBus的文章早已是数不胜数。在EventBus 3中引入了EventBusAnnotationProcessor
(注解分析生成索引)技术,大大提高了EventBus的运行效率。而分析这个加速器的资料在网上很少,因此本文会把重点放在分析这个EventBus 3的新特性上,同时分享一些踩坑经验,并结合源码分析及UML图,以直观的形式和大家一起学习EventBus 3的用法及运行原理。
使用EventBus
1.导入组件
// 打开App的build.gradle,在dependencies中添加最新的EventBus依赖:
compile 'org.greenrobot:eventbus:3.0.0'
如果不需要索引加速的话,就可以直接跳到第二步了。而要应用最新的EventBusAnnotationProcessor
则比较麻烦,因为注解解析依赖于android-apt-plugin。我们一步一步来,首先在项目gradle的dependencies中引入apt编译插件:
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
然后在App的build.gradle中应用apt插件,并设置apt生成的索引的包名和类名:
apply plugin: 'com.neenbedankt.android-apt'
apt {
arguments {
eventBusIndex "org.greenrobot.eventbusperf.MyEventBusIndex"
}
}
接着在App的dependencies中引入EventBusAnnotationProcessor:
apt 'org.greenrobot:eventbus-annotation-processor:3.0.1'
//或者使用源码
apt project(':EventBusAnnotationProcessor')
此时需要我们先编译一次,生成索引类。编译成功之后,就会发现在\ProjectName\app\build\generated\source\apt\PakageName\
下看到通过注解分析生成的索引类,这样我们便可以在初始化EventBus时应用我们生成的索引了。
2.初始化EventBus
EventBus默认有一个单例,可以通过getDefault()
获取,也可以通过EventBus.builder()
构造自定义的EventBus,比如要应用我们生成好的索引时:
EventBus mEventBus = EventBus.builder().addIndex(new MyEventBusIndex()).build();
如果想把自定义的设置应用到EventBus默认的单例中,则可以用installDefaultEventBus()方法:
EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus();
3.定义事件
所有能被实例化为Object的实例都可以作为事件。
在最新版的eventbus 3中如果用到了索引加速,事件类的修饰符必须为public,不然编译时会报错:Subscriber method must be public。
4.监听事件
首先把作为订阅事件的模块通过EventBus注册监听:
mEventBus.register(this);
在3.0之前,注册监听需要区分是否监听黏性(sticky)事件,监听EventBus事件的模块需要实现以onEvent开头的方法。如今改为在方法上添加注解的形式:
@Subscribe(threadMode = ThreadMode.POSTING, priority = 0, sticky = true)
public void handleEvent(DriverEvent event) {
Log.d(TAG, event.info);
}
注解有三个参数,threadMode为回调所在的线程,priority为优先级,sticky为是否接收黏性事件。调度单位从类细化到了方法,对方法的命名也没有了要求,方便混淆代码。但注册了监听的模块必须有一个标注了Subscribe注解方法,不然在register时会抛出异常:
Subscriber class XXX and its super classes have no public methods with the @Subscribe annotation
5.发送事件
调用post或者postSticky即可:
mEventBus.post(new DriverEvent("magnet:?xt=urn:btih……"));
在实际项目的使用中,register和unregister通常与Activity和Fragment的生命周期相关,ThreadMode.MainThread可以很好地解决Android的界面刷新必须在UI线程的问题,不需要再回调后用Handler中转(EventBus中已经自动用Handler做了处理),黏性事件可以很好地解决post与register同时执行时的异步问题(这个在原理中会说到),事件的传递也没有序列化与反序列化的性能消耗,足以满足我们大部分情况下的模块间通信需求。
EventBus原理分析
1.订阅注册(register)
简单来说就是:根据订阅者的类来找回调方法,把订阅者和回调方法封装成关系,并保存到相应的数据结构中,为随后的事件分发做好准备,最后处理黏性事件:
//3.0版本的注册
EventBus.getDefault().register(this);
//2.x版本的注册
EventBus.getDefault().register(this);
EventBus.getDefault().register(this, 100);
EventBus.getDefault().registerSticky(this, 100);
EventBus.getDefault().registerSticky(this);
可以看到2.x版本中有四种注册方法,区分了普通注册和粘性事件注册,并且在注册时可以选择接收事件的优先级,这里我们就不对2.x版本做过多的研究了,如果想研究可以参照此篇文章.由于3.0版本将粘性事件以及订阅事件的优先级换了一种更好的实现方式,所以3.0版本中的注册就变得简单,只有一个register()
方法即可.
public void register(Object subscriber) {
//首先获得订阅者的class对象
Class> subscriberClass = subscriber.getClass();
//通过subscriberMethodFinder来找到订阅者订阅了哪些事件.返回一个SubscriberMethod对象的List,SubscriberMethod
//里包含了这个方法的Method对象,以及将来响应订阅是在哪个线程的ThreadMode,以及订阅的事件类型eventType,以及订阅的优
//先级priority,以及是否接收粘性sticky事件的boolean值.
List subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod subscriberMethod : subscriberMethods) {
//订阅
subscribe(subscriber, subscriberMethod);
}
}
}
可以看到register()``方法很简洁,代码里的注释也很清楚了,我们可以看出通过
subscriberMethodFinder.findSubscriberMethods(subscriberClass)方法就能返回一个
SubscriberMethod的对象,而
SubscriberMethod里包含了所有我们需要的接下来执行
subscribe()的信息.所以我们先去看看
findSubscriberMethods()`是怎么实现的,然后我们再去关注subscribe()。
SubscriberMethodFinder的实现
一句话来描述SubscriberMethodFinder
类就是用来查找和缓存订阅者响应函数的信息的类。所以我们首先要知道怎么能获得订阅者响应函数的相关信息。在3.0版本中,EventBus
提供了一个EventBusAnnotationProcessor
注解处理器来在编译期通过读取@Subscribe()
注解并解析,处理其中所包含的信息,然后生成java类来保存所有订阅者关于订阅的信息,这样就比在运行时使用反射来获得这些订阅者的信息速度要快.我们可以参考EventBus项目里的EventBusPerformance这个例子,编译后我们可以在build文件夹里找到这个类,MyEventBusIndex 类,当然类名是可以自定义的.我们大致看一下生成的MyEventBusIndex
类是什么样的:
/**
* This class is generated by EventBus, do not edit.
*/
public class MyEventBusIndex implements SubscriberInfoIndex {
private static final Map, SubscriberInfo> SUBSCRIBER_INDEX;
static {
SUBSCRIBER_INDEX = new HashMap, SubscriberInfo>();
putIndex(new SimpleSubscriberInfo(org.greenrobot.eventbusperf.testsubject.PerfTestEventBus.SubscriberClassEventBusAsync.class,
true, new SubscriberMethodInfo[]{
new SubscriberMethodInfo("onEventAsync", TestEvent.class, ThreadMode.ASYNC),
}));
putIndex(new SimpleSubscriberInfo(TestRunnerActivity.class, true, new SubscriberMethodInfo[]{
new SubscriberMethodInfo("onEventMainThread", TestFinishedEvent.class, ThreadMode.MAIN),
}));
}
private static void putIndex(SubscriberInfo info) {
SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);
}
@Override
public SubscriberInfo getSubscriberInfo(Class> subscriberClass) {
SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);
if (info != null) {
return info;
} else {
return null;
}
}
}
可以看出是使用一个静态HashMap
即:SUBSCRIBER_INDEX
来保存订阅类的信息,其中包括了订阅类的class对象,是否需要检查父类,以及订阅方法的信息SubscriberMethodInfo
的数组,SubscriberMethodInfo
中又保存了:订阅方法的方法名,订阅的事件类型,触发线程,是否接收sticky事件以及优先级priority
.这其中就保存了register()的所有需要的信息,如果再配置EventBus的时候通过EventBusBuilder配置:eventBus = EventBus.builder().addIndex(new MyEventBusIndex()).build()
;来将编译生成的MyEventBusIndex
配置进去,这样就能在SubscriberMethodFinder
类中直接查找出订阅类的信息,就不需要再利用注解判断了(也就是我们说的涡轮引擎),当然这种方法是作为EventBus的可选配置,SubscriberMethodFinder
同样提供了通过注解来获得订阅类信息的方法,下面我们就来看findSubscriberMethods()
到底是如何实现的:
List findSubscriberMethods(Class> subscriberClass) {
//先从METHOD_CACHE取看是否有缓存,key:保存订阅类的类名,value:保存类中订阅的方法数据,
List subscriberMethods = METHOD_CACHE.get(subscriberClass);
if (subscriberMethods != null) {
return subscriberMethods;
}
//是否忽略注解器生成的MyEventBusIndex类
if (ignoreGeneratedIndex) {
//利用反射来读取订阅类中的订阅方法信息
subscriberMethods = findUsingReflection(subscriberClass);
} else {
//从注解器生成的MyEventBusIndex类中获得订阅类的订阅方法信息
subscriberMethods = findUsingInfo(subscriberClass);
}
if (subscriberMethods.isEmpty()) {
throw new EventBusException("Subscriber " + subscriberClass
+ " and its super classes have no public methods with the @Subscribe annotation");
} else {
//保存进METHOD_CACHE缓存
METHOD_CACHE.put(subscriberClass, subscriberMethods);
return subscriberMethods;
}
}
注释很详细我们就不在多说,由于篇幅原因我们就不在分析findUsingInfo()
方法,其无非就是通过查找我们上面所说的MyEventBusIndex
类中的信息,来转换成List
从而获得订阅类的相关订阅函数的各种信息.有兴趣的可以自己研究看看,下面我们就来看findUsingReflection()
方法是如何实现的:
private List findUsingReflection(Class> subscriberClass) {
//FindState 用来做订阅方法的校验和保存
FindState findState = prepareFindState();
findState.initForSubscriber(subscriberClass);
while (findState.clazz != null) {
//通过反射来获得订阅方法信息
findUsingReflectionInSingleClass(findState);
//查找父类的订阅方法
findState.moveToSuperclass();
}
//获取findState中的SubscriberMethod(也就是订阅方法List)并返回
return getMethodsAndRelease(findState);
}
这里通过FindState
类来做订阅方法的校验和保存,并通过FIND_STATE_POOL
静态数组来保存FindState
对象,可以使FindState
复用,避免重复创建过多的对象.最终是通过findUsingReflectionInSingleClass()
来具体获得相关订阅方法的信息的:
private void findUsingReflectionInSingleClass(FindState findState) {
Method[] methods;
//通过反射得到方法数组
try {
// This is faster than getMethods, especially when subscribers are fat classes like Activities
methods = findState.clazz.getDeclaredMethods();
} catch (Throwable th) {
// Workaround for java.lang.NoClassDefFoundError, see https://github.com/greenrobot/EventBus/issues/149
methods = findState.clazz.getMethods();
findState.skipSuperClasses = true;
}
//遍历Method
for (Method method : methods) {
int modifiers = method.getModifiers();
if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
Class>[] parameterTypes = method.getParameterTypes();
//保证必须只有一个事件参数
if (parameterTypes.length == 1) {
//得到注解
Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
if (subscribeAnnotation != null) {
Class> eventType = parameterTypes[0];
//校验是否添加该方法
if (findState.checkAdd(method, eventType)) {
ThreadMode threadMode = subscribeAnnotation.threadMode();
//实例化SubscriberMethod对象并添加
findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
subscribeAnnotation.priority(), subscribeAnnotation.sticky()));
}
}
} else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
String methodName = method.getDeclaringClass().getName() + "." + method.getName();
throw new EventBusException("@Subscribe method " + methodName +
"must have exactly 1 parameter but has " + parameterTypes.length);
}
} else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
String methodName = method.getDeclaringClass().getName() + "." + method.getName();
throw new EventBusException(methodName +
" is a illegal @Subscribe method: must be public, non-static, and non-abstract");
}
}
}
这里走完,我们订阅类的所有SubscriberMethod
都已经被保存了,最后再通过getMethodsAndRelease()
返回List
至此,所有关于如何获得订阅类的订阅方法信息即:SubscriberMethod
对象就已经完全分析完了,下面我们来看subscribe()是如何实现的.
subscribe()方法的实现
这里我们回到subscribe(subscriber, subscriberMethod);
中去,通过这个方法,我们就完成了注册,下面看一下subscribe()
的实现:
//必须在同步代码块里调用
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
//获取订阅的事件类型
Class> eventType = subscriberMethod.eventType;
//创建Subscription对象
Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
//从subscriptionsByEventType里检查是否已经添加过该Subscription,如果添加过就抛出异常
CopyOnWriteArrayList subscriptions = subscriptionsByEventType.get(eventType);
if (subscriptions == null) {
subscriptions = new CopyOnWriteArrayList<>();
subscriptionsByEventType.put(eventType, subscriptions);
} else {
if (subscriptions.contains(newSubscription)) {
throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
+ eventType);
}
}
//根据优先级priority来添加Subscription对象
int size = subscriptions.size();
for (int i = 0; i <= size; i++) {
if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
subscriptions.add(i, newSubscription);
break;
}
}
//将订阅者对象以及订阅的事件保存到typesBySubscriber里.
List> subscribedEvents = typesBySubscriber.get(subscriber);
if (subscribedEvents == null) {
subscribedEvents = new ArrayList<>();
typesBySubscriber.put(subscriber, subscribedEvents);
}
subscribedEvents.add(eventType);
//如果接收sticky事件,立即分发sticky事件
if (subscriberMethod.sticky) {
//eventInheritance 表示是否分发订阅了响应事件类父类事件的方法
if (eventInheritance) {
// Existing sticky events of all subclasses of eventType have to be considered.
// Note: Iterating over all events may be inefficient with lots of sticky events,
// thus data structure should be changed to allow a more efficient lookup
// (e.g. an additional map storing sub classes of super classes: Class -> List).
Set, Object>> entries = stickyEvents.entrySet();
for (Map.Entry, Object> entry : entries) {
Class> candidateEventType = entry.getKey();
if (eventType.isAssignableFrom(candidateEventType)) {
Object stickyEvent = entry.getValue();
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
} else {
Object stickyEvent = stickyEvents.get(eventType);
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
}
以上就是所有注册过程,现在再来看这张图就会特别清晰EventBus的register()过程了:
2.事件分发(post)
我们知道可以通过EventBus.getDefault().post("str");来发送一个事件,所以我们就从这行代码开始分析,首先看看post()方法是如何实现的:
public void post(Object event) {
//得到当前线程的Posting状态.
PostingThreadState postingState = currentPostingThreadState.get();
//获取当前线程的事件队列
List
首先是通过currentPostingThreadState.get()
方法来得到当前线程PostingThreadState
的对象,为什么是说当前线程我们来看看currentPostingThreadState
的实现:
private final ThreadLocal currentPostingThreadState = new ThreadLocal() {
@Override
protected PostingThreadState initialValue() {
return new PostingThreadState();
}
};
currentPostingThreadState
的实现是一个包含了PostingThreadState
的ThreadLocal
对象,关于ThreadLocal
张涛的这篇文章解释的很好:ThreadLocal 是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,而这段数据是不会与其他线程共享的。其内部原理是通过生成一个它包裹的泛型对象的数组,在不同的线程会有不同的数组索引值,通过这样就可以做到每个线程通过get() 方法获取的时候,取到的只能是自己线程所对应的数据。 所以这里取到的就是每个线程的PostingThreadState
状态.接下来我们来看postSingleEvent()
方法:
private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
Class> eventClass = event.getClass();
boolean subscriptionFound = false;
//是否触发订阅了该事件(eventClass)的父类,以及接口的类的响应方法.
if (eventInheritance) {
//查找eventClass类所有的父类以及接口
List> eventTypes = lookupAllEventTypes(eventClass);
int countTypes = eventTypes.size();
//循环postSingleEventForEventType
for (int h = 0; h < countTypes; h++) {
Class> clazz = eventTypes.get(h);
//只要右边有一个为true,subscriptionFound就为true
subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);
}
} else {
//post单个
subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);
}
//如果没发现
if (!subscriptionFound) {
if (logNoSubscriberMessages) {
Log.d(TAG, "No subscribers registered for event " + eventClass);
}
if (sendNoSubscriberEvent && eventClass != NoSubscriberEvent.class &&
eventClass != SubscriberExceptionEvent.class) {
//发送一个NoSubscriberEvent事件,如果我们需要处理这种状态,接收这个事件就可以了
post(new NoSubscriberEvent(this, event));
}
}
}
跟着上面的代码的注释,我们可以很清楚的发现是在postSingleEventForEventType()
方法里去进行事件的分发,代码如下:
private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class> eventClass) {
CopyOnWriteArrayList subscriptions;
//获取订阅了这个事件的Subscription列表.
synchronized (this) {
subscriptions = subscriptionsByEventType.get(eventClass);
}
if (subscriptions != null && !subscriptions.isEmpty()) {
for (Subscription subscription : subscriptions) {
postingState.event = event;
postingState.subscription = subscription;
//是否被中断
boolean aborted = false;
try {
//分发给订阅者
postToSubscription(subscription, event, postingState.isMainThread);
aborted = postingState.canceled;
} finally {
postingState.event = null;
postingState.subscription = null;
postingState.canceled = false;
}
if (aborted) {
break;
}
}
return true;
}
return false;
}
private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
switch (subscription.subscriberMethod.threadMode) {
case POSTING:
invokeSubscriber(subscription, event);
break;
case MAIN:
if (isMainThread) {
invokeSubscriber(subscription, event);
} else {
mainThreadPoster.enqueue(subscription, event);
}
break;
case BACKGROUND:
if (isMainThread) {
backgroundPoster.enqueue(subscription, event);
} else {
invokeSubscriber(subscription, event);
}
break;
case ASYNC:
asyncPoster.enqueue(subscription, event);
break;
default:
throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
}
}
总结上面的代码就是,首先从subscriptionsByEventType
里获得所有订阅了这个事件的Subscription
列表,然后在通过postToSubscription()
方法来分发事件,在postToSubscription()
通过不同的threadMode
在不同的线程里invoke()
订阅者的方法,ThreadMode
共有四类:
- PostThread
默认的 ThreadMode,表示在执行 Post 操作的线程直接调用订阅者的事件响应方法,不论该线程是否为主线程(UI 线程)。当该线程为主线程时,响应方法中不能有耗时操作,否则有卡主线程的风险。适用场景:对于是否在主线程执行无要求,但若 Post 线程为主线程,不能耗时的操作; - MainThread
在主线程中执行响应方法。如果发布线程就是主线程,则直接调用订阅者的事件响应方法,否则通过主线程的 Handler 发送消息在主线程中处理——调用订阅者的事件响应函数。显然,MainThread
类的方法也不能有耗时操作,以避免卡主线程。适用场景:必须在主线程执行的操作; - BackgroundThread
在后台线程中执行响应方法。如果发布线程不是主线程,则直接调用订阅者的事件响应函数,否则启动唯一的后台线程去处理。由于后台线程是唯一的,当事件超过一个的时候,它们会被放在队列中依次执行,因此该类响应方法虽然没有PostThread
类和MainThread
类方法对性能敏感,但最好不要有重度耗时的操作或太频繁的轻度耗时操作,以造成其他操作等待。适用场景:操作轻微耗时且不会过于频繁,即一般的耗时操作都可以放在这里; - Async
不论发布线程是否为主线程,都使用一个空闲线程来处理。和BackgroundThread
不同的是,Async
类的所有线程是相互独立的,因此不会出现卡线程的问题。适用场景:长耗时操作,例如网络访问。
这里我们只来看看invokeSubscriber(subscription, event);
是如何实现的,关于不同线程的Poster的使用
void invokeSubscriber(Subscription subscription, Object event) {
try {
subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
} catch (InvocationTargetException e) {
handleSubscriberException(subscription, event, e.getCause());
} catch (IllegalAccessException e) {
throw new IllegalStateException("Unexpected exception", e);
}
}
实际上就是通过反射调用了订阅者的订阅函数并把event
对象作为参数传入.至此post()
流程就结束了,整体流程图如下:
3.取消订阅(unregister)
看完了上面的分析,解除注册就相对容易了,解除注册只要调用unregister()
方法即可,实现如下:
public synchronized void unregister(Object subscriber) {
//通过typesBySubscriber来取出这个subscriber订阅者订阅的事件类型,
List> subscribedTypes = typesBySubscriber.get(subscriber);
if (subscribedTypes != null) {
//分别解除每个订阅了的事件类型
for (Class> eventType : subscribedTypes) {
unsubscribeByEventType(subscriber, eventType);
}
//从typesBySubscriber移除subscriber
typesBySubscriber.remove(subscriber);
} else {
Log.w(TAG, "Subscriber to unregister was not registered before: " + subscriber.getClass());
}
}
然后接着看unsubscribeByEventType()
方法的实现:
private void unsubscribeByEventType(Object subscriber, Class> eventType) {
//subscriptionsByEventType里拿出这个事件类型的订阅者列表.
List subscriptions = subscriptionsByEventType.get(eventType);
if (subscriptions != null) {
int size = subscriptions.size();
//取消订阅
for (int i = 0; i < size; i++) {
Subscription subscription = subscriptions.get(i);
if (subscription.subscriber == subscriber) {
subscription.active = false;
subscriptions.remove(i);
i--;
size--;
}
}
}
}
最终分别从typesBySubscriber
和subscriptions
里分别移除订阅者以及相关信息即可.
观察者模式观察者模式是对象的行为模式,又叫发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。EventBus
并不是标准的观察者模式的实现,但是它的整体就是一个发布/订阅框架,也拥有观察者模式的优点,比如:发布者和订阅者的解耦.
4.拓展-注解
注解(Annotations)是一种元数据的格式,为程序提供数据,但又不是程序的一部分。注解(Annotations)在代码上不直接影响代码操作。这怎么理解是数据又不是程序的一部分?我们可以将注解看成一种标注,类似于注释// 或 /* */
,不是程序的一部分但为程序提供注释,注解可以看作是提供数据。下面我们来看一下注解的用途:
- 提供编译信息:注解可以在编译的时候检查错误和提示警告(java语言规范性看来和注解不无关系)
- 编译和部署时处理:软件工具可以通过注解生成代码、xml文件等(注入看来也很黑科技)
- 运行时处理:一些注解可以在运行时使用
注解基础
注解是什么格式呢?
@Entity
这就是注解,看起来很简单,通过设置(@)符号标识,在跟一个注解的名称。下面来看一下我们经常用的Override
注解。
@Override
void mySuperMethod() { ... }
再来看一下带有参数的注解:
@Author(
name = "Benjamin Franklin",
date = "3/27/2003"
)
class MyClass() { ... }
or
@SuppressWarnings(value = "unchecked")
void myMethod() { ... }
如果按照上述value = "unchecked"
是不是参数只能限定一个,如果多个怎么办:
@SuppressWarnings("unchecked")
void myMethod() { ... }
其实还有一种能够传入多个值(Java SE 8 release支持),:
@Author(name = "Jane Doe") @Author(name = "John Smith") class MyClass { ... }
注解可以用在什么地方呢?注解可以用应用在:声明一个类、变量、方法和程序元素。
声明注解类型
如何做一个我们自己的注解?这里来看一个常见类:
public class Generation3List extends Generation2List {
// Author: John Doe
// Date: 3/17/2002
// Current revision: 6
// Last modified: 4/12/2004
// By: Jane Doe
// Reviewers: Alice, Bill, Cindy
// class code goes here
}
这里使用相同的元数据,通过注解来实现,我们必须定义注解类型。语法如下:
@interface ClassPreamble {
String author();
String date();
int currentRevision() default 1;
String lastModified() default "N/A";
String lastModifiedBy() default "N/A";
// Note use of array
String[] reviewers();
}
注解的定义非常类似接口,只是将接口的interface
关键字替换为@interface
。注解也是接口的一种形式。如何使用注解呢?注解的使用在声明定义前使用:
@ClassPreamble (
author = "John Doe",
date = "3/17/2002",
currentRevision = 6,
lastModified = "4/12/2004",
lastModifiedBy = "Jane Doe",
// Note array notation
reviewers = {"Alice", "Bob", "Cindy"}
)
public class Generation3List extends Generation2List {
// class code goes here
}
常用的注解类型
注解实际上被meta-annotations
调用。几种meta-annotations
在 java.lang.annotation.
类中。
- @Retention 定义一种注解的存储方式
- -RetentionPolicy.SOURCE 注解以源码级别存储,忽略编译
- -RetentionPolicy.CLASS 注解在编译时保留,忽略Java Virtual Machine (JVM)
- -RetentionPolicy.RUNTIME 运行时注解
- @Documented标识注解可以使用Javadoc tool工具将其生成文档
- @Target标识注解将要被应用成哪种Java元素,其中包含有:
- -ElementType.ANNOTATION_TYPE
- -ElementType.CONSTRUCTOR
- -ElementType.FIELD
- -ElementType.LOCAL_VARIABLE
- -ElementType.METHOD
- -ElementType.PACKAGE
- -ElementType.PARAMETER
- -ElementType.TYPE 任意一种类形式
- @Inherited标识注解是否可以被子类集成(true或false,默认false),当用户查询注解类型,该类没有声明注解类型,该类的父类查询到该注解类型。该注解仅应用在当前声明类
- @RepeatableJava SE 8引入,标识注解可以用应用多个相同的声明,具体参考:Repeating Annotations.
EventBus对注解的使用
先来看一下注解类声明:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {
ThreadMode threadMode() default ThreadMode.POSTING;
/**
* If true, delivers the most recent sticky event (posted with
* {@link EventBus#postSticky(Object)}) to this subscriber (if event available).
*/
boolean sticky() default false;
/** Subscriber priority to influence the order of event delivery.
* Within the same delivery thread ({@link ThreadMode}), higher priority subscribers will receive events before
* others with a lower priority. The default priority is 0. Note: the priority does *NOT* affect the order of
* delivery among subscribers with different {@link ThreadMode}s! */
int priority() default 0;
}
注解支持文档输出,存在于运行时,对Java方法生效。在使用的时候如下代码:
Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
if (subscribeAnnotation != null) {
Class> eventType = parameterTypes[0];
if (findState.checkAdd(method, eventType)) {
ThreadMode threadMode = subscribeAnnotation.threadMode();
findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
subscribeAnnotation.priority(), subscribeAnnotation.sticky()));
}
}
获取方法声明的注解,通过subscribeAnnotation.threadMode()
拿到对应的ThreadMode
,通过subscribeAnnotation.priority()
拿到声明的优先级,通过subscribeAnnotation.sticky()
拿到是否是粘性广播。在方法前的声明如下,声明非常简单:
@Subscribe(threadMode = ThreadMode.MAIN)
5.涡轮引擎-apt注入
apt注入可以重是一种编译时注解,如何在gradle中进行动态注入呢?android-apt
是一个Gradle插件,协助Android Studio
处理annotation processors
, 它有两个目的:
- 允许配置只在编译时作为注解处理器的依赖,而不添加到最后的APK或library
- 设置源路径,使注解处理器生成的代码能被Android Studio正确的引用
在EventBus上,主要使用注解处理器生成的代码,将所有注解的内容独立到org.greenrobot.eventbusperf.MyEventBusIndex
类中,
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment env) {
Messager messager = processingEnv.getMessager();
try {
// 获取脚本中声明变量eventBusIndex
String index = processingEnv.getOptions().get(OPTION_EVENT_BUS_INDEX);
....
// 收集订阅的声明
collectSubscribers(annotations, env, messager);
// 检查订阅声明
checkForSubscribersToSkip(messager, indexPackage);
if (!methodsByClass.isEmpty()) {
// 将订阅声明写入到索引文件中
createInfoIndexFile(index);
} else {
messager.printMessage(Diagnostic.Kind.WARNING, "No @Subscribe annotations found");
}
writerRoundDone = true;
} catch (RuntimeException e) {
// IntelliJ does not handle exceptions nicely, so log and print a message
e.printStackTrace();
messager.printMessage(Diagnostic.Kind.ERROR, "Unexpected error in EventBusAnnotationProcessor: " + e);
}
return true;
}
关键就是将索引到的所有符合要求的注解类,写入到索引文件中,代码如下所示:
private void createInfoIndexFile(String index) {
BufferedWriter writer = null;
try {
JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(index);
int period = index.lastIndexOf('.');
String myPackage = period > 0 ? index.substring(0, period) : null;
String clazz = index.substring(period + 1);
writer = new BufferedWriter(sourceFile.openWriter());
if (myPackage != null) {
writer.write("package " + myPackage + ";\n\n");
}
writer.write("import org.greenrobot.eventbus.meta.SimpleSubscriberInfo;\n");
writer.write("import org.greenrobot.eventbus.meta.SubscriberMethodInfo;\n");
writer.write("import org.greenrobot.eventbus.meta.SubscriberInfo;\n");
writer.write("import org.greenrobot.eventbus.meta.SubscriberInfoIndex;\n\n");
writer.write("import org.greenrobot.eventbus.ThreadMode;\n\n");
writer.write("import java.util.HashMap;\n");
writer.write("import java.util.Map;\n\n");
writer.write("/** This class is generated by EventBus, do not edit. */\n");
writer.write("public class " + clazz + " implements SubscriberInfoIndex {\n");
writer.write(" private static final Map, SubscriberInfo> SUBSCRIBER_INDEX;\n\n");
writer.write(" static {\n");
writer.write(" SUBSCRIBER_INDEX = new HashMap, SubscriberInfo>();\n\n");
writeIndexLines(writer, myPackage);
writer.write(" }\n\n");
writer.write(" private static void putIndex(SubscriberInfo info) {\n");
writer.write(" SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);\n");
writer.write(" }\n\n");
writer.write(" @Override\n");
writer.write(" public SubscriberInfo getSubscriberInfo(Class> subscriberClass) {\n");
writer.write(" SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);\n");
writer.write(" if (info != null) {\n");
writer.write(" return info;\n");
writer.write(" } else {\n");
writer.write(" return null;\n");
writer.write(" }\n");
writer.write(" }\n");
writer.write("}\n");
} catch (IOException e) {
throw new RuntimeException("Could not write source for " + index, e);
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
//Silent
}
}
}
}
踩坑与经验
1.混淆问题
混淆作为版本发布必备的流程,经常会闹出很多奇奇怪怪的问题,且不方便定位,尤其是EventBus这种依赖反射技术的库。通常情况下都会把相关的类和回调方法都keep住,但这样其实会留下被人反编译后破解的后顾之忧,所以我们的目标是keep最少的代码。
首先,因为EventBus 3弃用了反射的方式去寻找回调方法,改用注解的方式。作者的意思是在混淆时就不用再keep住相应的类和方法。但是我们在运行时,却会报java.lang.NoSuchFieldError: No static field POSTING。
网上给出的解决办法是keep住所有eventbus相关的代码:
-keep class de.greenrobot.** {*;}
其实我们仔细分析,可以看到是因为在SubscriberMethodFinder的findUsingReflection方法中,在调用Method.getAnnotation()时获取ThreadMode这个enum失败了,所以我们只需要keep住这个enum就可以了(如下)。
-keep public enum org.greenrobot.eventbus.ThreadMode { public static *; }
这样就能正常编译通过了,但如果使用了索引加速,是不会有上面这个问题的。因为在找方法时,调用的不是findUsingReflection,而是findUsingInfo。但是使用了索引加速后,编译后却会报新的错误:Could not find subscriber method in XXX Class. Maybe a missing ProGuard rule?
这就很好理解了,因为生成索引GeneratedSubscriberIndex是在代码混淆之前进行的,混淆之后类名和方法名都不一样了(上面这个错误是方法无法找到),得keep住所有被Subscribe注解标注的方法:
-keepclassmembers class * {
@de.greenrobot.event.Subscribe ;
}
所以又倒退回了EventBus2.4时不能混淆onEvent开头的方法一样的处境了。所以这里就得权衡一下利弊:使用了注解不用索引加速,则只需要keep住EventBus相关的代码,现有的代码可以正常的进行混淆。而使用了索引加速的话,则需要keep住相关的方法和类。
2.跨进程问题
目前EventBus只支持跨线程,而不支持跨进程。如果一个app的service起到了另一个进程中,那么注册监听的模块则会收不到另一个进程的EventBus发出的事件。这里可以考虑利用IPC做映射表,并在两个进程中各维护一个EventBus,不过这样就要自己去维护register和unregister的关系,比较繁琐,而且这种情况下通常用广播会更加方便,大家可以思考一下有没有更优的解决方案。
3.事件环路问题
在使用EventBus时,通常我们会把两个模块相互监听,来达到一个相互回调通信的目的。但这样一旦出现死循环,而且如果没有相应的日志信息,很难定位问题。所以在使用EventBus的模块,如果在回调上有环路,而且回调方法复杂到了一定程度的话,就要考虑把接收事件专门封装成一个子模块,同时考虑避免出现事件环路。
老司机教你 “飙” EventBus 3
EventBus-3-0源码分析
注解参考
android-apt 即将退出历史舞台
EventBus Documentation