從零開始“擼”出自己得EventBus(二)

轉載文章請標明出處http://www.reibang.com/p/233a40a2e0b5
如果你沒有看過我之前的博客建議你先去看看之前的博客 從零開始“擼”出自己的EventBus(一)
或者你已經(jīng)看過我這篇博客從零開始“擼”出自己的EventBus(三)

前一篇博客我們實現(xiàn)了一個簡單MiniBus伤塌。MiniBus雖然已經(jīng)實現(xiàn),但是其仍存在很多缺陷轧铁。

  1. 不能切換線程每聪,在哪個線程發(fā)送事件,觀察者注冊的方法就會在哪個線程執(zhí)行
  2. 采用前一篇博客所提到的第二種方案實現(xiàn)齿风,每次有post事件產(chǎn)生會遍歷所有注冊方法药薯,效率很低
  3. 講觀察者直接存進到Map的key值當中,如果使用的時候忘記在其對應的生命周期取消注冊聂宾,很容易引起內存泄漏

當然果善,它距離我們心中的EventBus還存在很多其他問題,比如不支持注解方式定義觀察者方法巾陕,不支持粘性事件等。但是我們不能一口吃成一個胖子是吧纪他,所有這次我們些先來解決以上三個問題鄙煤,剩下的問題我們在接下來的博客中解決。

夢想起航

這次我們就不再新建工程了茶袒,就在上次新建的工程中新建一個包梯刚,包名就叫middle_bus吧。在這里我再把上次提到的第三種方案拿出來復習一下薪寓。

在第二種方案上做一絲絲改動亡资,就是不以subscriber為Map的key值,而是以event的類型作為Map的key值向叉,這樣的話就不用每次post請求可以直接通過key值取出對應的方法锥腻,都遍歷一遍那么多的方法啦~

為了方便了我再對這種方案進行一丟丟的修改,就是key值直接存儲為event類型對應的Class母谎,因為最后比對的時候還是要利用反射的瘦黑。
嗯,key值類型確定了,value的類型呢幸斥?像以前一樣存為Method匹摇?這樣的話觀察者存到哪兒呢。甲葬。廊勃。。所以我們得自己封裝一個類演顾,這個類里面既要有Method數(shù)據(jù)供搀,也要有觀察者。輕輕松松钠至,我們的第一段代碼就出來了葛虐。

public class BusMethod {
    private Method mMethod;
    //采用弱引用的方法去存觀察者,可以有效的防止內存泄漏
    private WeakReference<Object> mObserverRefer;

    public BusMethod() {
    }

    public BusMethod(Method method, WeakReference<Object> objectWeakReference) {
        this.mMethod = method;
        this.mObserverRefer = objectWeakReference;
    }

    public Method getMethod() {
        return mMethod;
    }

    public void setMethod(Method method) {
        this.mMethod = method;
    }

    public WeakReference<Object> getObserverRefer() {
        return mObserverRefer;
    }

    public void setObserverRefer(WeakReference<Object> observerRefer) {
        this.mObserverRefer = observerRefer;
    }


    //提供一個方便的獲取觀察者的方法棉钧,若觀察者已被回收屿脐,則返回為null
    public Object getObserver() {
        Object observer = null;
        if (null != mObserverRefer) {
            observer = mObserverRefer.get();
        }
        return observer;
    }
}

為了防止內存泄漏,我們采用弱引用的方式去存儲觀察者宪卿。
接下來我們把EventBus的整體框架寫好的诵,這次我們就不叫MiniBus了,我們叫MiddleBus佑钾。

public class MiddleBus {
    private static class BusHolder {
        private static MiddleBus instance = new MiddleBus();
        //key為event對象西疤,value為對應的觀察者方法集合
        private static Map<Class<?>, List<BusMethod>> data = new HashMap<>();
    }

    public static MiddleBus getInstance() {
        return BusHolder.instance;
    }

    //因為注冊的觀察者可能沒有有效的方法,這時我們拋出異常提醒
    public void register(@NonNull Object observer) throws IllegalArgumentException {
    
    }

    public void post(@NonNull Object event) {
        
    }

    public void unregister(@NonNull Object observer) {
        
    }
}

沖浪~

接下來就是具體方法的實現(xiàn)休溶。
相比較與第二種思路代赁,第三種思路的注冊方法和取消注冊方法都要復雜很多。首先我們來實現(xiàn)注冊方法兽掰。

public void register(@NonNull Object observer) throws IllegalArgumentException {
        List<Method> methods = findUsefulMethods(observer);
        //如果一個有效方法都沒有則拋出異常
        if (null == methods || methods.size() == 0) {
            throw new IllegalArgumentException("No useful methods.");
        }

        for (Method method : methods) {
            //因為通過findUsefulMethods方法拿到的方法都只有一個參數(shù)
            //所以以下語句沒有問題
            Class<?> parameterType = method.getParameterTypes()[0];
            List<BusMethod> busMethods = BusHolder.data.get(parameterType);
            if (null == busMethods) {
                busMethods = new ArrayList<>();
            }
            BusMethod busMethod = new BusMethod(method, new WeakReference<Object>(observer));
            busMethods.add(busMethod);
            BusHolder.data.put(parameterType, busMethods);
        }
    }
    
    /**
     * 查找觀察者對象中以onEvent開頭的方法
     */
    private List<Method> findUsefulMethods(@NonNull Object observer) {
        List<Method> usefulMethods = new ArrayList<>();
        Class<?> observerClass = observer.getClass();
        Method[] methods = observerClass.getDeclaredMethods();
        for (Method method : methods) {
            if (checkIsUsefulMethod(method)) {
                usefulMethods.add(method);
            }
        }
        return usefulMethods;
    }
    
    /**
     * 遍歷觀察者所有的方法芭碍,拿到有效的方法
     * 在此先采用EventBus3.0以前版本的方法,
     * 也就是以onEvent開頭的方法孽尽,返回值為void
     * 并且只有一個參數(shù)窖壕,即視為有效方法
     */
    private boolean checkIsUsefulMethod(@NonNull Method method) {
        String methodName = method.getName();
        Class<?> returnType = method.getReturnType();
        Class<?>[] paramsClass = method.getParameterTypes();
        if (methodName.startsWith("onEvent")
                && returnType.equals(Void.TYPE)
                && paramsClass.length == 1) {
            return true;
        }
        return false;
    }

其中findUsefulMethods方法和checkIsUsefulMethod幾乎沒有改動。
我們再來看看unregister方法如何實現(xiàn)杉女。

public void unregister(@NonNull Object observer) {
        Set<Class<?>> keys = BusHolder.data.keySet();
        //拿到所有的key然后遍歷
        Iterator<Class<?>> iterator = keys.iterator();
        while (iterator.hasNext()) {
            Class<?> key = iterator.next();
            List<BusMethod> busMethods = BusHolder.data.get(key);
            if (null == busMethods || busMethods.size() == 0) {
                continue;
            }

            //拿到key中所有的方法然后遍歷
            Iterator<BusMethod> methodIterator = busMethods.iterator();
            while (methodIterator.hasNext()) {
                BusMethod busMethod = methodIterator.next();
                Object savedObserver = busMethod.getObserver();
                if (null == savedObserver || savedObserver.equals(observer)) {
                    methodIterator.remove();
                }
            }
        }
    }

可以看出瞻讽,第三種方式的取消注冊的效率要比第二種方法要低很多,凡事有舍必有得熏挎,本來當初分析的時候覺得這種方式要好很多速勇,但是實際上還是存在缺陷。
接下來我們看看post方法如何實現(xiàn)婆瓜。因為我們要解決不能切換線程的問題快集,所有我們先來定義個線程模式的枚舉。

public enum EventThread {
    DEFAULT, //默認模式廉白,即在哪個線程調用被調用方法就在哪個線程執(zhí)行
    MAIN, //主線程模式个初,不管調用方法在哪個線程,被調用方法均在UI線程執(zhí)行
    BACKGROUND, //后臺模式猴蹂,若在UI線程調用則新開一個線程處理院溺,若在非UI線程調用則在原線程處理
    ASYNC //異步模式,不管調用方法在哪個線程磅轻,被調用方法均新開一個線程執(zhí)行
}

一步一步的來珍逸,我們先來定義默認模式調用的方法和異步模式調用的方法。

//異步模式調用此方法
private void executeMethodAsync(final Object event, final BusMethod busMethod) {
    new Thread() {
        @Override
        public void run() {
            executeMethod(event, busMethod);
        }
    }.start();
}

//默認模式執(zhí)行此方法
private void executeMethod(Object event, BusMethod busMethod) {
    Object observer = busMethod.getObserver();
    //確定觀察者尚未被回收
    if (null != observer) {
        Method method = busMethod.getMethod();
        try {
            if (!method.isAccessible()) method.setAccessible(true);
            method.invoke(observer, event);
        } catch (Exception e) {
           e.printStackTrace();
        }
    }
}

那如果要在UI線程怎么處理呢聋溜?我首先想到的是調用runOnUiThread()方法,但是在Fragment和Activity里面可以調用到此方法谆膳,那在Service里怎么調用呢?似乎走到了死路撮躁。漱病。。把曼。其實杨帽,我們不該忘記還有Handler這種東東,在Handler初始化的時候傳入一個UI線程的Looper嗤军,接受到消息后自然就會在主線程中執(zhí)行啦~我們來重寫這個Handler

public class MainHandler extends Handler {
    private BusMethod mBusMethod;
    private Object mEvent;

    public MainHandler(Object event, BusMethod busMethod) {
        super(Looper.getMainLooper());
        this.mBusMethod = busMethod;
        this.mEvent = event;
    }

    @Override
    public void handleMessage(Message msg) {
        if (null == mBusMethod || null == mEvent) {
            return;
        }

        Object observer = mBusMethod.getObserver();
        //確定觀察者尚未被回收
        if (null != observer) {
            Method method = mBusMethod.getMethod();
            try {
                if (!method.isAccessible()) method.setAccessible(true);
                method.invoke(observer, mEvent);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

準備工作完成注盈,我們來攻克最后的post方法。

public void post(@NonNull Object event) {
    Class<?> eventClass = event.getClass();
    List<BusMethod> busMethods = BusHolder.data.get(eventClass);
    if (null != busMethods && busMethods.size() > 0) {
        Iterator<BusMethod> iterator = busMethods.iterator();
        while (iterator.hasNext()) {
            BusMethod busMethod = iterator.next();
            EventThread eventThead = findMethodThread(busMethod.getMethod());
            switch (eventThead) {
                //主線程模式則交給Handler去處理
                case MAIN:
                    new MainHandler(event, busMethod).sendEmptyMessage(0);
                    break;
                //默認模式交給我們剛才定義好的方法去處理
                case DEFAULT:
                    executeMethod(event, busMethod);
                    break;
                //判斷當前線程再進行相應處理
                case BACKGROUND:
                    if (Looper.getMainLooper() == Looper.myLooper()) {
                        executeMethodAsync(event, busMethod);
                    } else {
                        executeMethod(event, busMethod);
                    }
                    break;
                //交給我們事先定義好的方法取出來
                case ASYNC:
                    executeMethodAsync(event, busMethod);
                    break;
                default:
                    executeMethod(event, busMethod);
                    break;
            }
        }
    }
}

//查找方法應該在哪個線程執(zhí)行
private EventThread findMethodThread(@NonNull Method method) {
    String methodName = method.getName();
    if (null == methodName || methodName.length() < 7) {
        return EventThread.DEFAULT;
    }

    String subName = methodName.substring(7, methodName.length());
    //如果在onEvent后接的Main則視為主線程模式
    if (subName.startsWith("Main")) {
        return EventThread.MAIN;
    }
    //如果在onEvent后接的Background則視為后臺線程模式 
    else if (subName.startsWith("Background")) {
        return EventThread.BACKGROUND;
    }
    //如果在onEvent后接的Async則視為異步模式 
    else if (subName.startsWith("Async")) {
        return EventThread.ASYNC;
    }
    //其他一律視為默認模式 
    else {
        return EventThread.DEFAULT;
    }
}

其實我們也可以在register的時候就查找方法的線程叙赚,然后存到我們事先定義的類里面老客,就不用每次執(zhí)行post的時候都去查找一遍其對應的方法啦~

著陸

這里我就不寫測試代碼了,小伙伴們可以根據(jù)思路自己去測試一下bug~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末纠俭,一起剝皮案震驚了整個濱河市沿量,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌冤荆,老刑警劉巖朴则,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異钓简,居然都是意外死亡乌妒,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門外邓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來撤蚊,“玉大人,你說我怎么就攤上這事损话≌煨ィ” “怎么了槽唾?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長光涂。 經(jīng)常有香客問我庞萍,道長,這世上最難降的妖魔是什么忘闻? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任钝计,我火速辦了婚禮,結果婚禮上齐佳,老公的妹妹穿的比我還像新娘私恬。我一直安慰自己,他們只是感情好炼吴,可當我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布本鸣。 她就那樣靜靜地躺著,像睡著了一般硅蹦。 火紅的嫁衣襯著肌膚如雪永高。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天提针,我揣著相機與錄音命爬,去河邊找鬼。 笑死辐脖,一個胖子當著我的面吹牛饲宛,可吹牛的內容都是我干的。 我是一名探鬼主播嗜价,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼艇抠,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了久锥?” 一聲冷哼從身側響起家淤,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎瑟由,沒想到半個月后絮重,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡歹苦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年青伤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片殴瘦。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡狠角,死狀恐怖,靈堂內的尸體忽然破棺而出蚪腋,到底是詐尸還是另有隱情丰歌,我是刑警寧澤姨蟋,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站立帖,受9級特大地震影響芬探,放射性物質發(fā)生泄漏。R本人自食惡果不足惜厘惦,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望哩簿。 院中可真熱鬧宵蕉,春花似錦、人聲如沸节榜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宗苍。三九已至稼稿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間讳窟,已是汗流浹背让歼。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留丽啡,地道東北人谋右。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像补箍,于是被迫代替她去往敵國和親改执。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內容