自己動手實現一個EventBus框架

haozhn發表於2018-04-24

匯流排(Bus)正如它的英文名稱一樣:公共汽車。沿著固定的路線穿梭與城市中,每個乘客可以根據自己的目的地選擇在什麼時候上車,什麼時候下車。事件匯流排(EventBus)也是類似,只是那些乘客是你想要傳送的訊息。EventBus對於Android開發來說,提供了一個非常靈活的通訊方式。例如在Android裡,Fragment和Activity通訊中谷歌建議使用介面回撥的方式,Activity和Activity之間只通訊不跳轉也相對比較麻煩。這時候EventBus的作用就提現了出來,只需要發一個訊息,訂閱者就可以收到。下面我們就自己實現一個事件匯流排框架

雛形

  1. 我們新建一個類,並模仿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) {
        // 釋出訊息
    }
}
複製程式碼
  1. 接下來依次來實現這三個方法,首先是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<>();
    }
複製程式碼
  1. 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);
        }
    }
複製程式碼
  1. 最重要的就是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;
                }
            }
        }
    }
複製程式碼
  1. 上面的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);
        }
    }
}
複製程式碼
  1. 測試
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中接收。
執行結果

自己動手實現一個EventBus框架

註解的使用

關於事件匯流排的實現,很重要的一點是方法的標記,我們釋出了一個訊息,必須要知道哪些方法需要這個訊息。在上面的例子中,我們採用了一個很原始的方法,約定方法必須以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

相關文章