源码剖析:EventBus 从源码分析实现原理

EventBus 是一种用于Android的事件发布-订阅总线,用于代替BroadcastReceiver,可以简化应用程序中各个组件之间的通信复杂度。

今天剖析EventBus的源代码,主要是想了解EventBus的工作原理,学习相关的设计模式、反射、多线程等知识等。

用法
 public static class MessageEvent { /* Additional fields if needed */ }

 @Subscribe(threadMode = ThreadMode.MAIN)  
 public void onMessageEvent(MessageEvent event) {
     /* Do something */
 };

 @Override
 public void onStart() {
     super.onStart();
     EventBus.getDefault().register(this);
 }

 @Override
 public void onStop() {
     super.onStop();
     EventBus.getDefault().unregister(this);
 }
 EventBus.getDefault().post(new MessageEvent());

EventBus的用法非常的简单,只需要在Activity中使用@Subscribe定义了需要接收的事件类型,使用EventBus.getDefault().register(this)进行注册,就可以收到在其他地方发送的消息,同时也可以进行参数的传递。

问题
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {
    ThreadMode threadMode() default ThreadMode.POSTING;
    boolean sticky() default false;
    int priority() default 0;
}

@Subscribe有三个参数,今天的剖析就是分析这三个参数的作用,及实现原理。从官方文档得知threadMode()定义接收事件的线程,一共有POSTING、MAIN、MAIN_ORDERED、BACKGROUND、ASYNC这5种线程类型;priority是接收事件的优先级;然后还有sticky,粘性事件,也就是先发送,然后在订阅的时候接收到之前发送的粘性事件,把其消费掉。

代码剖析

EventBus.getDefault()
    static volatile EventBus defaultInstance;
    public static EventBus getDefault() {
        EventBus instance = defaultInstance;
        if (instance == null) {
            synchronized (EventBus.class) {
                instance = EventBus.defaultInstance;
                if (instance == null) {
                    instance = EventBus.defaultInstance = new EventBus();
                }
            }
        }
        return instance;
    }

volatile:volatile是一个类型修饰符(type specifier).volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

我们就从EventBus.getDefault()开始分析,这里使用了双检锁/双重校验锁(double-checked locking)方式创建单例,并使用volatile修饰符,保证指令不会重排序,确保单例的多线程安全。

EventBus.getDefault().register()
    public void register(Object subscriber) {
        Class<?> subscriberClass = subscriber.getClass();
        List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
        synchronized (this) {
            for (SubscriberMethod subscriberMethod : subscriberMethods) {
                subscribe(subscriber, subscriberMethod);
            }
        }
    }
public class SubscriberMethod {
    final Method method;
    final ThreadMode threadMode;
    final Class<?> eventType;
    final int priority;
    final boolean sticky;
    /** Used for efficient comparison */
    String methodString;
    ...
}

上面的代码是传入对象进行登记,通过findSubscriberMethods方法查找subscriberClass带有订阅(@Subscribe)的方法,然后通过subscribe()进行订阅。SubscriberMethod包含了方法,线程模式,优先级等。那接下来我们看看EventBus的findSubscriberMethods的实现方法。

SubscriberMethodFinder.findSubscriberMethods()
    private static final Map<Class<?>, List<SubscriberMethod>> METHOD_CACHE = new ConcurrentHashMap<>();

    List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
        List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
        if (subscriberMethods != null) {
            return subscriberMethods;
        }

        if (ignoreGeneratedIndex) {
            subscriberMethods = findUsingReflection(subscriberClass);
        } else {
            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.put(subscriberClass, subscriberMethods);
            return subscriberMethods;
        }
    }

ConcurrentHashMap:主要为了解决HashMap线程不安全和Hashtable效率不高的问题。JDK1.7之前的ConcurrentHashMap使用分段锁机制实现,JDK1.8则使用数组+链表+红黑树数据结构和CAS原子操作实现。

由于EventBus查找订阅方法是通过反射的方式实现,会有较大的性能开销,所以在这个方法中,使用线程安全的Map来缓存类对应的SubscriberMethod,同一个类多次使用时候,可以提高查找效率。这里可以看到,一个需要注册的类必须带有订阅的方法,否则就会抛出异常,这点在开发中必须要注意的。这里可以到非常有意思的的一点,通过ignoreGeneratedIndex来选择通过反射的方式查找或者通过findUsingInfo方式实现,然后这个ignoreGeneratedIndex又是从何而来的?findUsingReflectionfindUsingInfo两种方式又有什么不同呢?

findUsingInfo
    private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
        FindState findState = prepareFindState();
        findState.initForSubscriber(subscriberClass);
        while (findState.clazz != null) {
            findState.subscriberInfo = getSubscriberInfo(findState);
            if (findState.subscriberInfo != null) {
                SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
                for (SubscriberMethod subscriberMethod : array) {
                    if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
                        findState.subscriberMethods.add(subscriberMethod);
                    }
                }
            } else {
                findUsingReflectionInSingleClass(findState);
            }
            findState.moveToSuperclass();
        }
        return getMethodsAndRelease(findState);
    }

findUsingInfo和findUsingReflection的代码很接近,findUsingInfo其实也包含了findUsingReflection的功能,只是仅仅多了通过EventBusBuilder.subscriberInfoIndexes配置实现,具体看到getSubscriberInfo方法。

findUsingReflection
    private List<SubscriberMethod> findUsingReflection(Class<?> subscriberClass) {
        FindState findState = prepareFindState();
        findState.initForSubscriber(subscriberClass);
        while (findState.clazz != null) {
            findUsingReflectionInSingleClass(findState);
            findState.moveToSuperclass();
        }
        return getMethodsAndRelease(findState);
    }

我们先看反射的方式实现,prepareFindState()是从缓存池中读取或者创建一个FindState,getMethodsAndRelease就是把上面获取的FindState写入缓存池中。从代码可以看到,findUsingReflectionInSingleClass是从当前的Class通过反射查找带有注册的方法,然后转移到父类继续查找,直到没有父类为之。那么接下来看看findUsingReflectionInSingleClass的实现方式。

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;
        }
        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();
                            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");
            }
        }
    }

方法代码量比较多,但是逻辑其实也是很简单的,就是获取当前类所有的方法,找到带有@Subscribe注解的方法,获取threadMode进程类型。getMethods只能拿到当前类的方法,逐个逐个分析,所以代码还尝试使用getDeclaredMethods来获取方法数组,由于getDeclaredMethods是会获取到当前类和父类所有的方法,如果执行成功就无法再重复获取父类了,这样的速度也会更加快一些。

EventBus.subscribe(subscriber, subscriberMethod)

到了这里我们重新回到前面的EventBus.register(Object subscriber)方法,前面已经介绍了通过反射查找SubscriberMethod,接下来分析EventBus.subscribe(subscriber, subscriberMethod);

    public void register(Object subscriber) {
        Class<?> subscriberClass = subscriber.getClass();
        List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
        synchronized (this) {
            for (SubscriberMethod subscriberMethod : subscriberMethods) {
                subscribe(subscriber, subscriberMethod);
            }
        }
    }
    private final Map<Class<?>, Object> stickyEvents;
    private final Map<Object, List<Class<?>>> typesBySubscriber;
    private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
// Must be called in synchronized block
    private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
        Class<?> eventType = subscriberMethod.eventType;
        Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
        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);
            }
        }

        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;
            }
        }

        List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
        if (subscribedEvents == null) {
            subscribedEvents = new ArrayList<>();
            typesBySubscriber.put(subscriber, subscribedEvents);
        }
        subscribedEvents.add(eventType);

        if (subscriberMethod.sticky) {
            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);
            }
        }
    }

CopyOnWriteArrayList:传统的List在多线程同时读写的时候会抛出java.util.ConcurrentModificationException,Vector是线程安全的,但是由于使用了synchronized同步锁,导致同一时间内只有一个线程能访问,效率较低。而CopyOnWriteArrayList是使用CopyOnWrite(写时复制)技术解决了这个问题,但是这一般需要很大的内存开销。

isAssignableFrom:isAssignableFrom()方法是从类继承的角度去判断,instanceof关键字是从实例继承的角度去判断。isAssignableFrom()方法是判断是否为某个类的父类,instanceof关键字是判断是否某个类的子类。

EventBus.subscribe() 有2个关键的东西,Object subscriber注册的对象和SubscriberMethod订阅的方法,包含的method(方法)threadMode(订阅线程)eventType(事件类)priority(优先级)sticky。一个subscriber当中是可以有多个SubscriberMethod。

  1. subscriptionsByEventType:是以事件类作为key,来保存Subscription(subscriber, subscriberMethod),并且用priority排序,所以注册和取消注册都是对subscriptionsByEventType进行增删。
  2. typesBySubscriber:以subscriber注册对象作为key,保存事件类列表,其实他的作用当前只是用于判断subscriber是否已经注册,并没有其他的作用,改成Set类型也未尝不可。
  3. stickyEvents:是以事件类作为key,保存事件对象,stickyEvents是以key-value形式,意味着粘性事件一个事件类只能保存一个事件对象,后面发送的事件对象对覆盖前面发的。

stickyEvents接收时机:粘性属性的的实现,就是在注册的时候就会收到sticky粘性事件,意味着没有注册完成就收到了,所以我们在使用sticky属性时候,UI的初始化必须放在EventBus.register()之前,避免在收到事件后还没有初始化相关的信息。

eventInheritance:比如 A extends B implements C 发布者post(A()),那么找订阅者的时候不仅要找订阅了事件A的订阅者,还要找订阅了B和C的订阅者。这个参数是EventBusBuilder.eventInheritance配置的,默认是false,意味着只注册A的事件。

到了这里,注册就已经讲解完毕,由于注册还有checkPostStickyEventToSubscription方法,这个方法就是粘性事件的发送,如果当前存在需要发送的粘性事件,那么就会去调用postToSubscription方法发送事件,后面会说到postToSubscription方法。

从代码可见,粘性事件就算发送了,也不会从stickyEvents移除,意味着必须手动删除这个粘性事件才能够停止下次的分发。查看EventBus类的代码,就有removeStickyEventremoveAllStickyEvents实现移除工作,所以使用使用必须注意。

    private void checkPostStickyEventToSubscription(Subscription newSubscription, Object stickyEvent) {
        if (stickyEvent != null) {
            // If the subscriber is trying to abort the event, it will fail (event is not tracked in posting state)
            // --> Strange corner case, which we don't take care of here.
            postToSubscription(newSubscription, stickyEvent, isMainThread());
        }
    }
发送事件 EventBus.post(event) 、EventBus.postSticky(event)

ThreadLocal:为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

public class ThreadLocalTest {
    static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "default");

    public static void main(String[] args) {
        Runnable runnable = () -> {
            String name = Thread.currentThread().getName();
            System.out.println(name + ":" + threadLocal.get());
            threadLocal.set(name);
            try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
            System.out.println(name + ":" + threadLocal.get());
        };
        new Thread(runnable,"线程A").start();
        new Thread(runnable,"线程B").start();
        new Thread(runnable,"线程C").start();
    }
}
运行结果:
线程A:default
线程B:default
线程C:default
线程B:线程B
线程A:线程A
线程C:线程C

EventBus代码

    public void postSticky(Object event) {
        synchronized (stickyEvents) {
            stickyEvents.put(event.getClass(), event);
        }
        // Should be posted after it is putted, in case the subscriber wants to remove immediately
        post(event);
    }
private final ThreadLocal<PostingThreadState> currentPostingThreadState = null;
    public void post(Object event) {
        PostingThreadState postingState = currentPostingThreadState.get();
        List<Object> eventQueue = postingState.eventQueue;
        eventQueue.add(event);

        if (!postingState.isPosting) {
            postingState.isMainThread = isMainThread();
            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;
            }
        }
    }

前面已经把注册事件讲完了,现在就谈谈一个事件在怎么发送的,postSticky只是在post的基础上增加了一个存储事件到stickyEvents的功能,前面我们已经讲了其作用。这里使用了了ThreadLocal来保存当前线程的事件发送状态,postingState.isPosting表示是否发送中,如果是已经在发送就不重复进入发送进程,postingState.eventQueue是用于存储发送的事件,接下来就分析postSingleEvent()

细心的人可能会问为何这个方法中没有使用同步锁相关的线程管理,难道就不怕会出现多线程冲突问题吗?其实currentPostingThreadState其实就已经解决这个问题,这个属性是基于当前线程存储信息的,不会出现多线程冲突问题,这个设计很精妙。

postSingleEvent()
    private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
        Class<?> eventClass = event.getClass();
        boolean subscriptionFound = false;
        if (eventInheritance) {
            List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);
            int countTypes = eventTypes.size();
            for (int h = 0; h < countTypes; h++) {
                Class<?> clazz = eventTypes.get(h);
                subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);
            }
        } else {
            subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);
        }
        if (!subscriptionFound) {
            if (logNoSubscriberMessages) {
                logger.log(Level.FINE, "No subscribers registered for event " + eventClass);
            }
            if (sendNoSubscriberEvent && eventClass != NoSubscriberEvent.class &&
                    eventClass != SubscriberExceptionEvent.class) {
                post(new NoSubscriberEvent(this, event));
            }
        }
    }

eventInheritance前面已经讲过了,这里就不展开了。如果没有找到注册的事件方法,就会打印到日志里面,同时如果设置了sendNoSubscriberEvent,还会给sendNoSubscriberEvent发送一个NoSubscriberEvent事件。接下来就到了postSingleEventForEventType。

    private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
        CopyOnWriteArrayList<Subscription> subscriptions;
        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;
    }

这个方法的参数是事件和线程信息还有注册的事件类,这个方法的返回值是如果找不到注册的方法就返回false,方便前面一个类进行其他的操作。这里会以注册的事件类从subscriptionsByEventType找到对应的注册方法列表,然后遍历一个一个执行发送,所以在这里priority的作用就生效了,因为subscriptionsByEventType中的value列表就是基于priority排序的,前面已经讲过,所以这个方法也没有什么内容类,接下来就到了postToSubscription,前面的checkPostStickyEventToSubscription也是调用postToSubscription。

postToSubscription
    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 MAIN_ORDERED:
                if (mainThreadPoster != null) {
                    mainThreadPoster.enqueue(subscription, event);
                } else {
                    // temporary: technically not correct as poster not decoupled from subscriber
                    invokeSubscriber(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);
        }
    }

这个方法的逻辑也很简单,其实就是根据@Subscribe的threadMode参数进行不同的线程切换执行,从这里我们可以很好地知道threadMode的意义。

  1. POSTING:在和post相同的线程执行。
  2. MAIN:在UI线程执行,如果当前是UI进程马上执行。
  3. MAIN_ORDERED:和MAIN不同的是,MAIN_ORDERED是放到队列中,不会马上执行,等到前面的UI线程任务执行完毕才去执行。
  4. BACKGROUND:如果不是UI主进程,就马上执行,否则就放到后台队列。
  5. ASYNC:和BACKGROUND类似都是在非UI线程执行,但是不同的是BACKGROUND是多个事件共用一个线程先后执行,而ASYNC是为每一个事件分发都新开一个线程执行。
invokeSubscriber
    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);
        }
    }

这个方法很简单,就是通过反射执行方法并入事件参数,到这里事件的发送也基本讲完了。

EventBus.unregister()
    public synchronized void unregister(Object subscriber) {
        List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);
        if (subscribedTypes != null) {
            for (Class<?> eventType : subscribedTypes) {
                unsubscribeByEventType(subscriber, eventType);
            }
            typesBySubscriber.remove(subscriber);
        } else {
            logger.log(Level.WARNING, "Subscriber to unregister was not registered before: " + subscriber.getClass());
        }
    }

取消注册的代码很简单,就是从Map和List中查找删除注册的对象信息。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352