匯流排(Bus)正如它的英文名稱一樣:公共汽車。沿著固定的路線穿梭與城市中,每個乘客可以根據自己的目的地選擇在什麼時候上車,什麼時候下車。事件匯流排(EventBus)也是類似,只是那些乘客是你想要傳送的訊息。EventBus對於Android開發來說,提供了一個非常靈活的通訊方式。例如在Android裡,Fragment和Activity通訊中谷歌建議使用介面回撥的方式,Activity和Activity之間只通訊不跳轉也相對比較麻煩。這時候EventBus的作用就提現了出來,只需要發一個訊息,訂閱者就可以收到。下面我們就自己實現一個事件匯流排框架
雛形
- 我們新建一個類,並模仿EventBus提供三個方法
public class HBus {
private static volatile HBus instance;
public static HBus getInstance() {
if (instance == null) {
synchronized (HBus.class) {
if (instance == null) {
instance = new HBus();
}
}
}
return instance;
}
private HBus() {
}
public void register(Object obj) {
// 訂閱
}
public void unregister(Object obj) {
// 取消訂閱
}
public void post(Object obj) {
// 釋出訊息
}
}
複製程式碼
- 接下來依次來實現這三個方法,首先是register,為了方便起見,我們先使用HashMap來儲存類物件
public void register(Object obj) {
if (obj == null) {
throw new RuntimeException("can not register null object");
}
String key = obj.getClass().getName();
if (!subscriptionMap.containsKey(key)) {
subscriptionMap.put(key, obj);
}
}
複製程式碼
subscriptionMap是一個普通的HashMap,在HBus的構造方法中初始化,為了保證不同類的唯一性,採用類名全稱作為key。
private HashMap<String, Object> subscriptionMap;
private HBus() {
subscriptionMap = new HashMap<>();
}
複製程式碼
- unregister與register類似
public void unregister(Object obj) {
if (obj == null) {
throw new RuntimeException("can not unregister null object");
}
String key = obj.getClass().getName();
if (subscriptionMap.containsKey(key)) {
subscriptionMap.remove(key);
}
}
複製程式碼
- 最重要的就是post方法的實現了,post的時候究竟做了什麼呢?我們現在已經把訂閱的類都儲存在HashMap裡面,接下來我們需要遍歷這些類,找出這些類中符合條件的方法,然後執行這些方法。
public void post(Object msg) {
for (Map.Entry<String, Object> entry : subscriptionMap.entrySet()) {
// 獲取訂閱類
Object obj = entry.getValue();
// 獲取訂閱類中的所有方法
Method[] methods = obj.getClass().getDeclaredMethods();
// 遍歷類中的全部方法
for (Method method : methods) {
// 如果方法名以onEvent開頭,那就反射執行
if (method.getName().startsWith("onEvent")) {
try {
method.invoke(obj, msg);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
break;
}
}
}
}
複製程式碼
- 上面的post方法有一點小問題,每次post的時候都要重新遍歷找到onEvent開頭的方法,這顯然是不必要的,所以做一點小封裝。
Subscription類封裝了查詢方法和反射的過程
public class Subscription {
private static final String METHOD_PREFIX = "onEvent";
public Object subscriber;
private Method method;
public Subscription(Object obj) {
subscriber = obj;
findMethod();
}
private void findMethod() {
if (method == null) {
Method[] allMethod = subscriber.getClass().getDeclaredMethods();
for (Method method : allMethod) {
if (method.getName().startsWith(METHOD_PREFIX)) {
this.method = method;
break;
}
}
}
}
public void invokeMessage(Object msg) {
if (method != null) {
try {
method.invoke(subscriber, msg);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
複製程式碼
HBus最終版
public class HBus {
private static volatile HBus instance;
private HashMap<String, Subscription> subscriptionMap;
public static HBus getInstance() {
if (instance == null) {
synchronized (HBus.class) {
if (instance == null) {
instance = new HBus();
}
}
}
return instance;
}
private HBus() {
subscriptionMap = new HashMap<>();
}
public void register(Object obj) {
if (obj == null) {
throw new RuntimeException("can not register null object");
}
String key = obj.getClass().getName();
if (!subscriptionMap.containsKey(key)) {
subscriptionMap.put(key, new Subscription(obj));
}
}
public void unregister(Object obj) {
if (obj == null) {
throw new RuntimeException("can not unregister null object");
}
String key = obj.getClass().getName();
if (subscriptionMap.containsKey(key)) {
subscriptionMap.remove(key);
}
}
public void post(Message msg) {
for (Map.Entry<String, Subscription> entry : subscriptionMap.entrySet()) {
entry.getValue().invokeMessage(msg);
}
}
}
複製程式碼
- 測試
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
HBus.getInstance().register(this);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
HBus.getInstance().post("hello");
}
});
}
public void onEventHello(Object msg) {
if (msg instanceof String) {
Log.e("haozhn", " " + msg);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
HBus.getInstance().unregister(this);
}
}
複製程式碼
上面的例子中,MainActivity既是釋出者,也是訂閱者。在點選按鈕的時候釋出一個字串"hello",然後在onEventHello中接收。
執行結果
註解的使用
關於事件匯流排的實現,很重要的一點是方法的標記,我們釋出了一個訊息,必須要知道哪些方法需要這個訊息。在上面的例子中,我們採用了一個很原始的方法,約定方法必須以onEvent開頭。這種方式顯然是不夠靈活的,那有沒有更高階的實現方式呢?當然有,那就是註解的方式。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Subscriber {
}
複製程式碼
註解只是一個標記,所以不需要引數。下面需要對Subscription修改
public class Subscription {
public Object subscriber;
private Method method;
public Subscription(Object obj) {
subscriber = obj;
findMethod();
}
private void findMethod() {
if (method == null) {
Method[] allMethod = subscriber.getClass().getDeclaredMethods();
for (Method method : allMethod) {
Annotation annotation = method.getAnnotation(Subscriber.class);
if (annotation != null) {
this.method = method;
break;
}
}
}
}
public void invokeMessage(Object msg) {
if (method != null) {
try {
method.invoke(subscriber, msg);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
複製程式碼
主要是對findMethod方法進行修改,判斷方法是否被Subscriber註解標記。
然後我們把之前的測試程式碼修改一下
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
HBus.getInstance().register(this);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
HBus.getInstance().post("hello");
}
});
}
@Subscriber
public void anyMethod(Object msg) {
if (msg instanceof String) {
Log.e("haozhn", " " + msg);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
HBus.getInstance().unregister(this);
}
}
複製程式碼
通過Subscriber註解標記的方法就可以任意取名了。
廣播與點對點通訊
目前為止我們實現的實現的事件匯流排其實和廣播類似,釋出一個訊息後所有的訂閱者都能收到,並且都會執行。而且釋出的訊息和接受的訊息都是Object型別,這樣就會有一個問題,比如有A,B,C三個頁面。A,B都訂閱了訊息,C釋出了一個訊息只想讓A執行,這時候怎麼辦?我們當然可以傳一個物件過去,在物件裡定義一個欄位作為區分。
public class HMessage {
public int what;
public String msg;
}
複製程式碼
然後在接收到訊息後強轉成HMessage,根據what欄位來區分,比如what==1的時候A來處理這個訊息,what==2的時候B來處理這個訊息。
如果在多人合作開發的專案裡,有人傳ObjectA,有人傳ObjectB,這樣最後的程式碼通常會變得不可維護,所以我們應該定義一個類似Android系統中Message一樣的通用訊息物件。
public class HMessage {
private int key;
private Object obj;
private HMessage() {
}
public HMessage(int key) {
this(key, null);
}
public HMessage(int key, Object obj) {
this.key = key;
this.obj = obj;
}
public int getKey() {
return key;
}
public Object getData() {
return obj;
}
}
複製程式碼
然後我們需要把之前post的引數改成HMessage,這樣訊息的處理就會變成
@Subscriber
public void anyMethod(HMessage msg) {
if(msg == null) return;
switch (msg.getKey()) {
case 1:
Log.e("haozhn", " " + msg.getData());
break;
}
}
複製程式碼
這樣可以在每個訂閱的方法內部去判斷是否需要處理這個訊息,但是訊息依然會群發給所以訂閱者,有沒有什麼方法可以實現點對點的通訊呢?那就需要在Subscriber註解上做點文章了。我們可以嘗試給它加個引數
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Subscriber {
String tag() default Subscription.DEFAULT_TAG;
}
複製程式碼
然後在標記方法的時候加上引數
@Subscriber(tag = "tag")
public void anyMethod(HMessage msg) {
if(msg == null) return;
switch (msg.getKey()) {
case 1:
Log.e("haozhn", " " + msg.getData());
break;
}
}
複製程式碼
在找到method的時候也要設定一下tag
public class Subscription {
public Object subscriber;
private Method method;
private String tag;
public Subscription(Object obj) {
subscriber = obj;
findMethodAndTag();
}
private void findMethodAndTag() {
if (method == null) {
Method[] allMethod = subscriber.getClass().getDeclaredMethods();
for (Method method : allMethod) {
Subscriber annotation = method.getAnnotation(Subscriber.class);
if (annotation != null) {
this.method = method;
this.tag = annotation.tag();
break;
}
}
}
}
public String getTag() {
return tag;
}
public void invokeMessage(HMessage msg) {
if (method != null) {
try {
method.invoke(subscriber, msg);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
複製程式碼
在post傳送訊息的時候通過tag進行過濾
public void post(HMessage msg,String[] tags) {
if(tags == null || tags.length == 0) {
throw new IllegalArgumentException("tags can not be null or length of tags can not be 0");
}
for (Map.Entry<String, Subscription> entry : subscriptionMap.entrySet()) {
Subscription sub = entry.getValue();
for (String tag : tags) {
if (tag.equals(sub.getTag())) {
sub.invokeMessage(msg);
}
}
}
}
複製程式碼
為了更易於使用,我們可以給tag加一個預設值。
public @interface Subscriber {
String tag() default Subscription.DEFAULT_TAG;
}
複製程式碼
當tag等於預設值的時候說明沒有設定tag,我們就自己給該方法設定一個tag,我選用類全稱作為tag,這樣可以避免重複
private void initMethodAndTag() {
Method[] methods = subscriber.getClass().getDeclaredMethods();
for (Method m : methods) {
Subscriber annotation = m.getAnnotation(Subscriber.class);
if (annotation != null) {
method = m;
tag = DEFAULT_TAG.equals(annotation.tag()) ? subscriber.getClass().getName() : annotation.tag();
break;
}
}
}
複製程式碼
然後過載一個post方法,傳一個Class陣列
public void post(HMessage msg, Class[] classes) {
if (classes == null || classes.length == 0) {
throw new IllegalArgumentException("classes can not be null or length of classes can not be 0");
}
String[] tags = new String[classes.length];
for (int i = 0; i < classes.length; i++) {
tags[i] = classes[i].getName();
}
post(msg, tags);
}
複製程式碼
這樣修改以後我們使用起來也非常方便
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
HBus.getInstance().register(this);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
HBus.getInstance().post(new HMessage(1, "hello"), new Class[]{MainActivity.class});
}
});
}
@Subscriber
public void anyMethod(HMessage msg) {
if (msg == null) return;
switch (msg.getKey()) {
case 1:
Log.e("haozhn", " " + msg.getData());
break;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
HBus.getInstance().unregister(this);
}
}
複製程式碼
如果真要實現類似的廣播的效果呢,那當然是可以的,只是非常不建議這樣,因為廣播的模式雖然靈活,但多了之後就會非常混亂,難以維護。
執行緒切換
現在這個框架已經可以滿足基本的使用需求了,但還有一個明顯的缺陷,那就是如果在子執行緒中傳送一個訊息,通知Activity去更改一個UI,可以成功嗎?當然不能,因為現在的處理方式是在當前執行緒處理結果,如果在子執行緒傳送訊息,就會在子執行緒嘗試更改UI。所以接下來要做的就是區分訂閱者所線上程。我們不妨再給Subscriber加個引數。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Subscriber {
String tag() default Subscription.DEFAULT_TAG;
ThreadMode thread() default ThreadMode.SAMETHREAD;
}
複製程式碼
ThreadMode是個列舉類,表示執行緒的型別
public enum ThreadMode {
/**
* 主執行緒
*/
MAIN,
/**
* 相同的執行緒
*/
SAMETHREAD
}
複製程式碼
如果是SAMETHREAD就保持和原來的處理方式就可以了。如果是MAIN呢?那就按照Android的方法,通過Handler把訊息傳給主執行緒。
public class Subscription {
static final String DEFAULT_TAG = "hbus_default_tag_value";
public Object subscriber;
private Method method;
private String tag;
private ThreadMode threadMode;
private MsgHandler mHandler;
public Subscription(Object obj) {
subscriber = obj;
mHandler = new MsgHandler(Looper.getMainLooper());
findMethodAndTag();
}
private void findMethodAndTag() {
if (method == null) {
Method[] allMethod = subscriber.getClass().getDeclaredMethods();
for (Method method : allMethod) {
Subscriber annotation = method.getAnnotation(Subscriber.class);
if (annotation != null) {
this.method = method;
this.tag = DEFAULT_TAG.equals(annotation.tag()) ? subscriber.getClass().getName() : annotation.tag();
this.threadMode = annotation.thread();
break;
}
}
}
}
public String getTag() {
return tag;
}
public void invokeMessage(HMessage msg) {
if (method != null) {
try {
if (threadMode == ThreadMode.MAIN) {
// 主執行緒
Message message = Message.obtain();
message.obj = msg;
mHandler.sendMessage(message);
} else {
method.invoke(subscriber, msg);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
private class MsgHandler extends Handler {
public MsgHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.obj instanceof HMessage) {
try {
HMessage hm = (HMessage) msg.obj;
method.invoke(subscriber, hm);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
}
複製程式碼
這樣一個簡單版的事件匯流排框架就完成了,最後附上github地址HBus