對于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<SubscriberMethod> 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<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;
static {
SUBSCRIBER_INDEX = new HashMap<Class<?>, 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<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
//先從METHOD_CACHE取看是否有緩存,key:保存訂閱類的類名,value:保存類中訂閱的方法數據,
List<SubscriberMethod> 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<SubscriberMethod>
從而獲得訂閱類的相關訂閱函數的各種信息.有興趣的可以自己研究看看,下面我們就來看findUsingReflection()
方法是如何實現的:
private List<SubscriberMethod> 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>
至此,所有關于如何獲得訂閱類的訂閱方法信息即: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<Subscription> 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<Class<?>> 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<Class>).
Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet();
for (Map.Entry<Class<?>, 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<Object> eventQueue = postingState.eventQueue;
eventQueue.add(event);
if (!postingState.isPosting) {
postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper();
postingState.isPosting = true;
if (postingState.canceled) {
throw new EventBusException("Internal error. Abort state was not reset");
}
try {
//一直發送
while (!eventQueue.isEmpty()) {
//發送單個事件
postSingleEvent(eventQueue.remove(0), postingState);
}
} finally {
postingState.isPosting = false;
postingState.isMainThread = false;
}
}
}
首先是通過currentPostingThreadState.get()
方法來得到當前線程PostingThreadState
的對象,為什么是說當前線程我們來看看currentPostingThreadState
的實現:
private final ThreadLocal<PostingThreadState> currentPostingThreadState = new ThreadLocal<PostingThreadState>() {
@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<Class<?>> 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<Subscription> 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<Class<?>> 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<Subscription> 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<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;\n\n");
writer.write(" static {\n");
writer.write(" SUBSCRIBER_INDEX = new HashMap<Class<?>, 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 <methods>;
}
所以又倒退回了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