Guava源碼分析——EventBus

EventBus的設(shè)計(jì)理念是基于觀察者模式的届谈,可以參考設(shè)計(jì)模式(1)—觀察者模式先來了解該設(shè)計(jì)模式矫俺。

1、程序示例

EventBus的使用是非常簡單的,首先你要添加Guava的依賴到自己的項(xiàng)目中征讲。這里我們通過一個(gè)最基本的例子來說明EveentBus是如何使用的蜡吧。

public static void main(String...args) {
    // 定義一個(gè)EventBus對象峦失,這里的Joker是該對象的id
    EventBus eventBus = new EventBus("Joker");
    // 向上述EventBus對象中注冊一個(gè)監(jiān)聽對象   
    eventBus.register(new EventListener());
    // 使用EventBus發(fā)布一個(gè)事件燎含,該事件會(huì)給通知到所有注冊的監(jiān)聽者
    eventBus.post(new Event("Hello every listener, joke begins..."));
}

// 事件,監(jiān)聽者監(jiān)聽的事件的包裝對象
public static class Event {
    public String message;
    Event(String message) {
        this.message = message;
    }
}

// 監(jiān)聽者
public static class EventListener {
    // 監(jiān)聽的方法军援,必須使用注解聲明仅淑,且只能有一個(gè)參數(shù),實(shí)際觸發(fā)一個(gè)事件的時(shí)候會(huì)根據(jù)參數(shù)類型觸發(fā)方法
    @Subscribe
    public void listen(Event event) {
        System.out.println("Event listener 1 event.message = " + event.message);
    }
}

首先胸哥,這里我們封裝了一個(gè)事件對象Event涯竟,一個(gè)監(jiān)聽者對象EventListener。然后空厌,我們用EventBus的構(gòu)造方法創(chuàng)建了一個(gè)EventBus實(shí)例庐船,并將上述監(jiān)聽者實(shí)例注冊進(jìn)去。然后嘲更,我們使用上述EventBus實(shí)例發(fā)布一個(gè)事件Event筐钟。然后,以上注冊的監(jiān)聽者中的使用@Subscribe注解聲明并且只有一個(gè)Event類型的參數(shù)的方法將會(huì)在觸發(fā)事件的時(shí)候被觸發(fā)赋朦。

總結(jié):從上面的使用中篓冲,我們可以看出,EventBus與觀察者模式不同的地方在于:當(dāng)注冊了一個(gè)監(jiān)聽者的時(shí)候宠哄,只有當(dāng)某個(gè)方法使用了@Subscribe注解聲明并且參數(shù)與發(fā)布的事件類型匹配壹将,那么這個(gè)方法才會(huì)被觸發(fā)。這就是說毛嫉,同一個(gè)監(jiān)聽者可以監(jiān)聽多種類型的事件诽俯,也可以在多次監(jiān)聽同一個(gè)事件。

2承粤、EventBus源碼分析

2.1 分析之前

好了暴区,通過上面的例子,我們了解了EventBus最基本的使用方法辛臊。下面我們來分析一下在Guava中是如何為我們實(shí)現(xiàn)這個(gè)API的仙粱。不過,首先浪讳,我們還是先試著考慮一下自己設(shè)計(jì)這個(gè)API的時(shí)候如何設(shè)計(jì)缰盏,并且提出幾個(gè)問題,然后帶著問題到源碼中尋找答案淹遵。

假如要我們?nèi)ピO(shè)計(jì)這樣一個(gè)API口猜,最簡單的方式就是在觀察者模式上進(jìn)行拓展:每次調(diào)用EventBus.post()方法的時(shí)候,會(huì)對所有的觀察者對象進(jìn)行遍歷透揣,然后獲取它們?nèi)康姆椒醚祝袛嘣摲椒ㄊ欠袷褂昧?code>@Subscribe并且方法的參數(shù)類型是否與post()方法發(fā)布的事件類型一致,如果一致的話辐真,那么我們就使用反射來觸發(fā)這個(gè)方法须尚。在觀察者模式中,每個(gè)觀察者都要實(shí)現(xiàn)一個(gè)接口侍咱,發(fā)布事件的時(shí)候耐床,我們只要調(diào)用接口的方法就行,但是EventBus把這個(gè)限制設(shè)定得更加寬泛楔脯,也就是監(jiān)聽者無需實(shí)現(xiàn)任何接口撩轰,只要方法使用了注解并且參數(shù)匹配即可。

從上面的分析中可以看出昧廷,這里面不僅要對所有的監(jiān)聽者進(jìn)行遍歷堪嫂,還要對它們的方法進(jìn)行遍歷,找到了匹配的方法之后又要使用反射來觸發(fā)這個(gè)方法木柬。首先皆串,當(dāng)注冊的監(jiān)聽者數(shù)量比較多的時(shí)候,鏈?zhǔn)秸{(diào)用的效率就不高眉枕;然后我們又要使用反射來觸發(fā)匹配的方法恶复,這樣效率肯定又低了一些。那么在GuavaEventBus中是如何解決這兩個(gè)問題的速挑?

另外還要注意下下文中的觀察者監(jiān)聽者的不同谤牡,監(jiān)聽者用來指我們使用EventBus.register()注冊的對象,觀察者是EventBus中的對象Subscriber梗摇,后者封裝了一個(gè)監(jiān)聽者的所有的信息拓哟,比如監(jiān)聽的方法等等。
一般我們是不會(huì)直接操作Subscriber對象的伶授,它的訪問權(quán)限也只在EventBus的包中可訪問断序。

2.2 著手分析

首先,當(dāng)我們使用new初始化一個(gè)EventBus的時(shí)候糜烹,實(shí)際都會(huì)調(diào)用到下面的這個(gè)方法:

EventBus(String identifier, Executor executor, Dispatcher dispatcher, SubscriberExceptionHandler exceptionHandler) {
    this.subscribers = new SubscriberRegistry(this);
    this.identifier = (String)Preconditions.checkNotNull(identifier);
    this.executor = (Executor)Preconditions.checkNotNull(executor);
    this.dispatcher = (Dispatcher)Preconditions.checkNotNull(dispatcher);
    this.exceptionHandler = (SubscriberExceptionHandler)Preconditions.checkNotNull(exceptionHandler);
}

這里的identifier是一個(gè)字符串類型违诗,類似于EventBus的id;
subscribers是SubscriberRegistry類型的疮蹦,實(shí)際上EventBus在添加诸迟、移除和遍歷觀察者的時(shí)候都會(huì)使用該實(shí)例的方法,所有的觀察者信息也都維護(hù)在該實(shí)例中;
executor是事件分發(fā)過程中使用到的線程池阵苇,可以自己實(shí)現(xiàn)壁公;
dispatcher是Dispatcher類型的子類,用來在發(fā)布事件的時(shí)候分發(fā)消息給監(jiān)聽者绅项,它有幾個(gè)默認(rèn)的實(shí)現(xiàn)紊册,分別針對不同的分發(fā)方式;
exceptionHandler是SubscriberExceptionHandler類型的快耿,它用來處理異常信息囊陡,在默認(rèn)的EventBus實(shí)現(xiàn)中,會(huì)在出現(xiàn)異常的時(shí)候打印出log掀亥,當(dāng)然我們也可以定義自己的異常處理策咯撞反。

所以,從上面的分析中可以看出搪花,如果我們想要了解EventBus是如何注冊和取消注冊以及如何遍歷來觸發(fā)事件的遏片,就應(yīng)該從SubscriberRegistry入手。確實(shí)鳍侣,個(gè)人也認(rèn)為丁稀,這個(gè)類的實(shí)現(xiàn)也是EventBus中最精彩的部分。

2.2.1 SubscriberRegistry

根據(jù)2.1中的分析倚聚,我們需要在EventBus中維護(hù)幾個(gè)映射线衫,以便在發(fā)布事件的時(shí)候找到并通知所有的監(jiān)聽者,首先是事件類型->觀察者列表的映射惑折。
上面我們也說過授账,EventBus中發(fā)布事件是針對各個(gè)方法的,我們將一個(gè)事件對應(yīng)的類型信息和方法信息等都維護(hù)在一個(gè)對象中惨驶,在EventBus中就是觀察者Subscriber白热。
然后,通過事件類型映射到觀察者列表粗卜,當(dāng)發(fā)布事件的時(shí)候屋确,只要根據(jù)事件類型到列表中尋找所有的觀察者并觸發(fā)監(jiān)聽方法即可。
在SubscriberRegistry中通過如下數(shù)據(jù)結(jié)構(gòu)來完成這一映射:

private final ConcurrentMap<Class<?>, CopyOnWriteArraySet<Subscriber>> subscribers = Maps.newConcurrentMap();

從上面的定義形式中我們可以看出续扔,這里使用的是事件的Class類型映射到Subscriber列表的攻臀。這里的Subscriber列表使用的是Java中的CopyOnWriteArraySet集合,
它底層使用了CopyOnWriteArrayList纱昧,并對其進(jìn)行了封裝刨啸,也就是在基本的集合上面增加了去重的操作。這是一種適用于讀多寫少場景的集合识脆,在讀取數(shù)據(jù)的時(shí)候不會(huì)加鎖设联,
寫入數(shù)據(jù)的時(shí)候進(jìn)行加鎖善已,并且會(huì)進(jìn)行一次數(shù)組拷貝。

既然离例,我們已經(jīng)知道了在SubscriberRegistry內(nèi)部會(huì)在注冊的時(shí)候向以上數(shù)據(jù)結(jié)構(gòu)中插入映射换团,那么我們可以具體看下它是如何完成這一操作的。

在分析register()方法之前粘招,我們先看下SubscriberRegistry內(nèi)部經(jīng)常使用的幾個(gè)方法啥寇,它們的原理與我們上面提出的問題息息相關(guān)偎球。
首先是findAllSubscribers()方法洒扎,它用來獲取指定監(jiān)聽者對應(yīng)的全部觀察者集合。下面是它的代碼:

private Multimap<Class<?>, Subscriber> findAllSubscribers(Object listener) {
    // 創(chuàng)建一個(gè)哈希表
    Multimap<Class<?>, Subscriber> methodsInListener = HashMultimap.create();
    // 獲取監(jiān)聽者的類型
    Class<?> clazz = listener.getClass();
    // 獲取上述監(jiān)聽者的全部監(jiān)聽方法
    UnmodifiableIterator var4 = getAnnotatedMethods(clazz).iterator(); // 1
    // 遍歷上述方法衰絮,并且根據(jù)方法和類型參數(shù)創(chuàng)建觀察者并將其插入到映射表中
    while(var4.hasNext()) {
        Method method = (Method)var4.next();
        Class<?>[] parameterTypes = method.getParameterTypes();
        // 事件類型
        Class<?> eventType = parameterTypes[0];
        methodsInListener.put(eventType, Subscriber.create(this.bus, listener, method));
    }
    return methodsInListener;
}

這里注意一下Multimap數(shù)據(jù)結(jié)構(gòu)袍冷,它是Guava中提供的集合結(jié)構(gòu),與普通的哈希表不同的地方在于猫牡,它可以完成一對多操作胡诗。這里用來存儲(chǔ)事件類型到觀察者的一對多映射。
注意下1處的代碼淌友,我們上面也提到過煌恢,當(dāng)新注冊監(jiān)聽者的時(shí)候,用反射獲取全部方法并進(jìn)行判斷的過程非常浪費(fèi)性能震庭,而這里就是這個(gè)問題的答案:

這里getAnnotatedMethods()方法會(huì)嘗試從subscriberMethodsCache中獲取所有的注冊監(jiān)聽的方法(即使用了注解并且只有一個(gè)參數(shù))瑰抵,下面是這個(gè)方法的定義:

private static ImmutableList<Method> getAnnotatedMethods(Class<?> clazz) {
    return (ImmutableList)subscriberMethodsCache.getUnchecked(clazz);
}

這里的subscriberMethodsCache的定義是:

private static final LoadingCache<Class<?>, ImmutableList<Method>> subscriberMethodsCache = CacheBuilder.newBuilder().weakKeys().build(new CacheLoader<Class<?>, ImmutableList<Method>>() {
    public ImmutableList<Method> load(Class<?> concreteClass) throws Exception { // 2
        return SubscriberRegistry.getAnnotatedMethodsNotCached(concreteClass);
    }
});

這里的作用機(jī)制是:當(dāng)使用subscriberMethodsCache.getUnchecked(clazz)獲取指定監(jiān)聽者中的方法的時(shí)候會(huì)先嘗試從緩存中進(jìn)行獲取,如果緩存中不存在就會(huì)執(zhí)行2處的代碼器联,
調(diào)用SubscriberRegistry中的getAnnotatedMethodsNotCached()方法獲取這些監(jiān)聽方法二汛。這里我們省去該方法的定義,具體可以看下源碼中的定于拨拓,其實(shí)就是使用反射并完成一些校驗(yàn)肴颊,并不復(fù)雜。

這樣渣磷,我們就分析完了findAllSubscribers()方法婿着,整理一下:當(dāng)注冊監(jiān)聽者的時(shí)候,首先會(huì)拿到該監(jiān)聽者的類型醋界,然后從緩存中嘗試獲取該監(jiān)聽者對應(yīng)的所有監(jiān)聽方法竟宋,如果沒有的話就遍歷該類的方法進(jìn)行獲取,并添加到緩存中物独;
然后袜硫,會(huì)遍歷上述拿到的方法集合,根據(jù)事件的類型(從方法參數(shù)得知)和監(jiān)聽者等信息創(chuàng)建一個(gè)觀察者挡篓,并將事件類型-觀察者鍵值對插入到一個(gè)一對多映射表中并返回婉陷。

下面帚称,我們看下EventBus中的register()方法的代碼:

void register(Object listener) {
    // 獲取事件類型-觀察者映射表
    Multimap<Class<?>, Subscriber> listenerMethods = this.findAllSubscribers(listener);
    Collection eventMethodsInListener;
    CopyOnWriteArraySet eventSubscribers;
    // 遍歷上述映射表并將新注冊的觀察者映射表添加到全局的subscribers中
    for(Iterator var3 = listenerMethods.asMap().entrySet().iterator(); var3.hasNext(); eventSubscribers.addAll(eventMethodsInListener)) {
        Entry<Class<?>, Collection<Subscriber>> entry = (Entry)var3.next();
        Class<?> eventType = (Class)entry.getKey();
        eventMethodsInListener = (Collection)entry.getValue();
        eventSubscribers = (CopyOnWriteArraySet)this.subscribers.get(eventType);
        // 如果指定事件對應(yīng)的觀察者列表不存在就創(chuàng)建一個(gè)新的
        if (eventSubscribers == null) {
            CopyOnWriteArraySet<Subscriber> newSet = new CopyOnWriteArraySet();
            eventSubscribers = (CopyOnWriteArraySet)MoreObjects.firstNonNull(this.subscribers.putIfAbsent(eventType, newSet), newSet);
        }
    }
}

SubscriberRegistry中的register()方法與unregister()方法類似,我們不進(jìn)行說明秽澳。下面看下當(dāng)調(diào)用EventBus.post()方法的時(shí)候的邏輯闯睹。下面是其代碼:

public void post(Object event) {
    // 調(diào)用SubscriberRegistry的getSubscribers方法獲取該事件對應(yīng)的全部觀察者
    Iterator<Subscriber> eventSubscribers = this.subscribers.getSubscribers(event);
    if (eventSubscribers.hasNext()) {
        // 使用Dispatcher對事件進(jìn)行分發(fā)
        this.dispatcher.dispatch(event, eventSubscribers);
    } else if (!(event instanceof DeadEvent)) {
        this.post(new DeadEvent(this, event));
    }
}

從上面的代碼可以看出,實(shí)際上當(dāng)調(diào)用EventBus.post()方法的時(shí)候回先用SubscriberRegistry的getSubscribers方法獲取該事件對應(yīng)的全部觀察者担神,所以我們需要先看下這個(gè)邏輯楼吃。
以下是該方法的定義:

Iterator<Subscriber> getSubscribers(Object event) {
    // 獲取事件類型的所有父類型和自身構(gòu)成的集合
    ImmutableSet<Class<?>> eventTypes = flattenHierarchy(event.getClass()); // 3
    List<Iterator<Subscriber>> subscriberIterators = Lists.newArrayListWithCapacity(eventTypes.size());
    UnmodifiableIterator var4 = eventTypes.iterator();
    // 遍歷上述事件類型,并從subscribers中獲取所有的觀察者列表
    while(var4.hasNext()) {
        Class<?> eventType = (Class)var4.next();
        CopyOnWriteArraySet<Subscriber> eventSubscribers = (CopyOnWriteArraySet)this.subscribers.get(eventType);
        if (eventSubscribers != null) {
            subscriberIterators.add(eventSubscribers.iterator());
        }
    }
    return Iterators.concat(subscriberIterators.iterator());
}

這里注意以下3處的代碼妄讯,它用來獲取當(dāng)前事件的所有的父類包含自身的類型構(gòu)成的集合孩锡,也就是說,加入我們觸發(fā)了一個(gè)Interger類型的事件亥贸,那么Number和Object等類型的監(jiān)聽方法都能接收到這個(gè)事件并觸發(fā)躬窜。這里的邏輯很簡單,就是根據(jù)事件的類型炕置,找到它及其所有的父類的類型對應(yīng)的觀察者并返回荣挨。

2.2.2 Dispatcher

接下來我們看真正的分發(fā)事件的邏輯是什么樣的。

EventBus.post()方法可以看出朴摊,當(dāng)我們使用Dispatcher進(jìn)行事件分發(fā)的時(shí)候默垄,需要將當(dāng)前的事件和所有的觀察者作為參數(shù)傳入到方法中。然后甚纲,在方法的內(nèi)部進(jìn)行分發(fā)操作口锭。最終某個(gè)監(jiān)聽者的監(jiān)聽方法是使用反射進(jìn)行觸發(fā)的,這部分邏輯在Subscriber內(nèi)部贩疙,而Dispatcher是事件分發(fā)的方式的策略接口讹弯。EventBus中提供了3個(gè)默認(rèn)的Dispatcher實(shí)現(xiàn),分別用于不同場景的事件分發(fā):

  1. ImmediateDispatcher:直接在當(dāng)前線程中遍歷所有的觀察者并進(jìn)行事件分發(fā)这溅;
  2. LegacyAsyncDispatcher:異步方法组民,存在兩個(gè)循環(huán),一先一后悲靴,前者用于不斷往全局的隊(duì)列中塞入封裝的觀察者對象臭胜,后者用于不斷從隊(duì)列中取出觀察者對象進(jìn)行事件分發(fā);實(shí)際上癞尚,EventBus有個(gè)字類AsyncEventBus就是用該分發(fā)器進(jìn)行事件分發(fā)的耸三。
  3. PerThreadQueuedDispatcher:這種分發(fā)器使用了兩個(gè)線程局部變量進(jìn)行控制,當(dāng)dispatch()方法被調(diào)用的時(shí)候浇揩,會(huì)先獲取當(dāng)前線程的觀察者隊(duì)列仪壮,并將傳入的觀察者列表傳入到該隊(duì)列中;然后通過一個(gè)布爾類型的線程局部變量胳徽,判斷當(dāng)前線程是否正在進(jìn)行分發(fā)操作积锅,如果沒有在進(jìn)行分發(fā)操作爽彤,就通過遍歷上述隊(duì)列進(jìn)行事件分發(fā)。

上述三個(gè)分發(fā)器內(nèi)部最終都會(huì)調(diào)用Subscriber的dispatchEvent()方法進(jìn)行事件分發(fā):

final void dispatchEvent(final Object event) {
    // 使用指定的執(zhí)行器執(zhí)行任務(wù)
    this.executor.execute(new Runnable() {
        public void run() {
            try {
                // 使用反射觸發(fā)監(jiān)聽方法
                Subscriber.this.invokeSubscriberMethod(event);
            } catch (InvocationTargetException var2) {
                // 使用EventBus內(nèi)部的SubscriberExceptionHandler處理異常
                Subscriber.this.bus.handleSubscriberException(var2.getCause(), Subscriber.this.context(event));
            }
        }
    });
}

上述方法中的executor是執(zhí)行器缚陷,它是通過EventBus獲取到的适篙;處理異常的SubscriberExceptionHandler類型也是通過EventBus獲取到的。(原來EventBus中的構(gòu)造方法中的字段是在這里用到的s镆)至于反射觸發(fā)方法調(diào)用并沒有太復(fù)雜的邏輯嚷节。

另外還要注意下Subscriber還有一個(gè)字類SynchronizedSubscriber,它與一般的Subscriber的不同就在于它的反射觸發(fā)調(diào)用的方法被sychronized關(guān)鍵字修飾虎锚,也就是它的觸發(fā)方法是加鎖的、線程安全的翁都。

總結(jié):

至此碍论,我們已經(jīng)完成了EventBus的源碼分析。簡單總結(jié)一下:

EventBus中維護(hù)了三個(gè)緩存和四個(gè)映射:

  1. 事件類型到觀察者列表的映射(緩存)坐搔;
  2. 事件類型到監(jiān)聽者方法列表的映射(緩存);
  3. 事件類型到事件類型及其所有父類的類型的列表的映射(緩存)敬矩;
  4. 觀察者到監(jiān)聽者的映射概行,觀察者到監(jiān)聽方法的映射;

觀察者Subscriber內(nèi)部封裝了監(jiān)聽者和監(jiān)聽方法弧岳,可以直接反射觸發(fā)凳忙。而如果是映射到監(jiān)聽者的話,還要判斷監(jiān)聽者的方法的類型來進(jìn)行觸發(fā)禽炬。個(gè)人覺得這個(gè)設(shè)計(jì)是非常棒的涧卵,因?yàn)槲覀儫o需再在EventBus中維護(hù)一個(gè)映射的緩存了,因?yàn)镾ubscriber中已經(jīng)完成了這個(gè)一對一的映射腹尖。

每次使用EventBus注冊和取消注冊監(jiān)聽者的時(shí)候柳恐,都會(huì)先從緩存中進(jìn)行獲取,不是每一次都會(huì)用到反射的热幔,這可以提升獲取的效率乐设,也解答了我們一開始提出的效率的問題。當(dāng)使用反射觸發(fā)方法的調(diào)用貌似是不可避免的了绎巨。

最后近尚,EventBus中使用了非常多的數(shù)據(jù)結(jié)構(gòu),比如MultiMap场勤、CopyOnWriteArraySet等戈锻,還有一些緩存和映射的工具庫介汹,這些大部分都來自于Guava。

看了EventBus的實(shí)現(xiàn)舶沛,由衷地感覺Google的工程師真牛嘹承!而Guava中還有許多更加豐富的內(nèi)容值得我們?nèi)ネ诰颍?/p>

了解線程局部遍歷可以參考下我的另一篇博文:ThreadLocal的使用及其源碼實(shí)現(xiàn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市如庭,隨后出現(xiàn)的幾起案子叹卷,更是在濱河造成了極大的恐慌,老刑警劉巖坪它,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件骤竹,死亡現(xiàn)場離奇詭異,居然都是意外死亡往毡,警方通過查閱死者的電腦和手機(jī)蒙揣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來开瞭,“玉大人懒震,你說我怎么就攤上這事∴拖辏” “怎么了个扰?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長葱色。 經(jīng)常有香客問我递宅,道長,這世上最難降的妖魔是什么苍狰? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任办龄,我火速辦了婚禮,結(jié)果婚禮上淋昭,老公的妹妹穿的比我還像新娘俐填。我一直安慰自己,他們只是感情好响牛,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布玷禽。 她就那樣靜靜地躺著,像睡著了一般呀打。 火紅的嫁衣襯著肌膚如雪矢赁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天贬丛,我揣著相機(jī)與錄音撩银,去河邊找鬼。 笑死豺憔,一個(gè)胖子當(dāng)著我的面吹牛额获,可吹牛的內(nèi)容都是我干的够庙。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼抄邀,長吁一口氣:“原來是場噩夢啊……” “哼耘眨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起境肾,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤剔难,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后奥喻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體偶宫,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年环鲤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了纯趋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡冷离,死狀恐怖吵冒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情酒朵,我是刑警寧澤桦锄,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站蔫耽,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏留夜。R本人自食惡果不足惜匙铡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望碍粥。 院中可真熱鬧鳖眼,春花似錦、人聲如沸嚼摩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽枕面。三九已至愿卒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間潮秘,已是汗流浹背琼开。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留枕荞,地道東北人柜候。 一個(gè)月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓搞动,卻偏偏與公主長得像,于是被迫代替她去往敵國和親渣刷。 傳聞我的和親對象是個(gè)殘疾皇子鹦肿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容

  • EventBus基本使用 EventBus基于觀察者模式的Android事件分發(fā)總線。 從這個(gè)圖可以看出辅柴,Even...
    顧氏名清明閱讀 617評論 0 1
  • 對于Android開發(fā)老司機(jī)來說肯定不會(huì)陌生箩溃,它是一個(gè)基于觀察者模式的事件發(fā)布/訂閱框架,開發(fā)者可以通過極少的代碼...
    飛揚(yáng)小米閱讀 1,473評論 0 50
  • EventBus源碼分析(一) EventBus官方介紹為一個(gè)為Android系統(tǒng)優(yōu)化的事件訂閱總線碌识,它不僅可以很...
    蕉下孤客閱讀 3,979評論 4 42
  • 我從去年開始使用 RxJava 碾篡,到現(xiàn)在一年多了。今年加入了 Flipboard 后筏餐,看到 Flipboard 的...
    Jason_andy閱讀 5,460評論 7 62
  • EventBus 是基于觀察者模式的發(fā)布/訂閱事件總線开泽,它讓組件間的通信變得更加簡單。類似廣播系統(tǒng)魁瞪,不過 Even...
    MarchCd閱讀 493評論 1 1