Android官方响应式框架Agera详解:一、相关概念和基本使用

Android

前言

在学习 Agera 之前没有接触过响应式编程和 RxJava ,所以当时学起来非常的费劲,也踩了很多坑。写这篇博客的目的就是把自己学习到的成果分享出来,希望能够帮助那些需要的人

后面的文章已经更新啦~
Android官方响应式框架Agera详解:二、Repository的创建和操作符
Android官方响应式框架Agera详解:三、Repository的更新规则及Agera+Retrofit+Okhttp实战

目录

一、Agera 及相关概念简介
二、Agera 使用示例
三、Agera 相关类和基本使用
四、Repository 的分类和创建使用
五、总结
六、相关代码
七、预告

一、Agera 及相关概念简介

Agera

Agera的github地址

Agera is a set of classes and interfaces to help write functional, asynchronous, and reactive applications for Android.Requires Android SDK version 9 or higher

简单翻译一下,Agera 是一组类和接口,帮助安卓开发者实现功能性的、异步的和响应式的应用。要求Android 的 SDK 版本在9以上

Agera (瑞典文的意思是"采取行动")是一个超轻量级的 Android 库,帮助 Android 应用中有生命周期的组件(比如:Activities)或者组件中的对象(比如:Views)预准备数据。 通过加入函数式响应式编程,Agera可以在 什么时机, 什么线程 和 什么数据 层面上更清晰的分离数据处理流程,并且使用一个接近自然语言的单个表达式就能编写一个复杂的异步流

观察者模式

Agera 使用著名的 观察者模式 作为响应式编程的驱动机制。 被观察者(observable)实现 Observable 接口, 并向所有注册的观察者们(observers)广播事件。 观察者(observer)实现 Updatable 接口, 可以注册和注销到 Observable 中,接受通知事件触发更新操作,故此命名为 Updatable

Push event, pull data

Agera 使用 push event, pull data 模型(推送事件,拉取数据)。
push event:被观察者只做事件通知,不携带任何数据;
pull data:观察者根据自己需要从数据仓库(Repository.get())拉取数据

注册/注销 观察者

Updatable 的注册和注销必须配对使用。不能重复注册 Updatable,不能注销没有注册过的 Updatable,也不能重复注销 Updatable,等其他非法操作

public class Test extends Activity {
    //被观察者
    Observable observable = new Observable() {
        @Override
        public void addUpdatable(@NonNull Updatable updatable) {
        }
        @Override
        public void removeUpdatable(@NonNull Updatable updatable) {
        }
    };
    //观察者
    Updatable updatable = new Updatable() {
        @Override
        public void update() {
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    protected void onResume() {
        super.onResume();
        //注册观察者
        observable.addUpdatable(updatable);
    }

    @Override
    protected void onPause() {
        super.onPause();
        //注销观察者
        observable.removeUpdatable(updatable);
    }
}

Activation lifecycle

被观察者(Observable)的有两种状态:

  1. active状态(活动状态):有观察者 (至少注册一个了Updatable)
  2. inactive状态(非活动状态):没有观察者 (没有注册的Updatable)

也就是, 注册 Updatable 让 Observable 从非活动状态激活为活动状态,当 Updatable 全部注销了,Observable 从活动状态变为非活动状态

二、 Agera 使用示例

为了让小伙伴们对 Agera 有一个最初的印象,这里先给出一个 Agera 的使用示例

假设一下,有一个业务场景:

  1. 有一个按钮,当我们点击的时候在子线程进行网络请求,下载一张图片
  2. 切换到另一个子线程去压缩图片
  3. 回到主线程显示出图片
    //网络请求框架
    OkHttpClient client = new OkHttpClient.Builder()
            .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
            .build();

    Repository<Result<Bitmap>> repository = Repositories.repositoryWithInitialValue(Result.<Bitmap>absent())
            .observe()
            .onUpdatesPerLoop()
            .goTo(NETWORK_EXECUTOR)  //切换到子线程进行网络请求
            .attemptGetFrom(() -> {
                runOnUiThread(() -> tvState.setText("正在子线程进行图片下载..."));
                //网络请求部分
                Request request = new Request.Builder()
                        .url(IMAGE_URL)
                        .get()
                        .build();
                try {
                    Response response = client.newCall(request).execute();
                    if (response.isSuccessful()) {
                        //为了更清楚的观察状态,此处休眠一秒
                        Thread.sleep(1000);
                        return Result.present(response.body().byteStream());
                    } else {
                        //网络请求失败时的处理
                        return Result.failure(new Throwable("下载图片失败!" + response.code() + response.message()));
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    //异常时的处理
                    return Result.failure(new Throwable("下载图片异常!" + e.getMessage()));
                }
            })
            .orEnd(Result::failure)
            .goTo(COMPRESS_EXECUTOR) //切换到压缩图片的线程
            .thenTransform(input -> {
                runOnUiThread(() -> tvState.setText("正在子线程进行图片压缩..."));
                //压缩图片的部分
                try {
                    BitmapFactory.Options options = new BitmapFactory.Options();
                    options.inSampleSize = 2;
                    Bitmap bitmap = BitmapFactory.decodeStream(input, new Rect(0, 0, 0, 0), options);
                    Thread.sleep(1500);
                    return Result.present(bitmap);
                } catch (Exception e) {
                    e.printStackTrace();
                    return Result.failure(new Throwable("压缩图片异常!" + e.getMessage()));
                }
            })
            .compile();

    Updatable updatable = new Updatable() {
        @Override
        public void update() {
            repository.get()
                    //上面所有的流程都正确执行
                    .ifSucceededSendTo(value -> {  
                        tvState.setText("加载图片完成");
                        ivImage.setImageBitmap(value);
                    })
                    //上面的流程发生错误
                    .ifFailedSendTo(value -> tvState.setText(value.getMessage()));
        }
    };

效果图如下:


下载图片.gif

如果看不太懂没关系,后面我们会慢慢学到。

虽然你可能没有看太懂,但是通过代码你也可以感受到 Agera 的强大,整体非常简洁,切换线程非常方便,只需一个 goto() 方法。

其实,在网络请求那一部分我们还可以用更简洁的代码去完成,需要使用 Agera + Retrofit + Okhttp 三个框架来共同完成,这个我们后面也会讲到

好了,对 Agera 有了初步的印象后,让我们一起开始学习吧

三、 Agera相关类和基本使用

在 Agera 中,有几个比较重要的类和接口,我们以它们为切入点,一步一步地学习如果使用 Agera

最开始的时候介绍了,Agera 是基于观察者模式的,首先我们看一下观察者、被观察者在 Agera 中是如何表现的

1、 Updatable

翻译为 :观察者
用途:接收事件通知,更新数据
说明:观察者模式中的 Observer,在 Agera 中使用 Updatable

我们看一下它的代码,非常简单,它是一个接口,包含一个 update() 方法。

public interface Updatable {
  void update();
}

2、 Observable

翻译为:被观察者、事件源
用途:作为事件源,通知观察者更新数据,可以注册、注销观察者。
说明:当事件发生的时候,调用 dispatchUpdate() 通知观察者。

Observable 也是一个接口,里面包含两个方法,一个用于添加观察者(也就是 Updatable ),一个用于移除观察者( Updatable )

当有事件发生的时候,被观察者(也就是 Observable ) 会通知所有添加过的观察者( Updatable )

public interface Observable {
  void addUpdatable(@NonNull Updatable updatable);
  void removeUpdatable(@NonNull Updatable updatable);
}

注意,Updatable 的注册和注销必须配对使用

小结

我们先做一个小结

  1. Agera 是基于观察模式的,Updatable 指代的是观察者模式中的观察者,而 Observable 所指代的就是观察者模式中的被观察者

  2. Agera 的更新机制是:使用 Updatable 去观察Observable,Observable 去通知 Updatable 更新

现在我们了解了 Agera 的更新机制,那具体到代码中是如何使用的呢?

  1. 定义一个 Updateable 和一个 Observable
  2. 使用 Observable 的 addUpdatable() 方法去将Updatable 注册到 Observable 中
  3. 当某个事件发生时调用 Updatable 的 update() 方法进行更新操作
  4. 当我们不使用的时候,注销 Updatable

下面我们来看一个具体的例子,按照上面的三个步骤实现。在布局中定义一个 Button 和 TextView,当点击这个 Button 的时候,更新 TextView 中的文字

public class SimpleActivity extends Activity {

    Button btnTest;
    TextView tvHello;

      // 步骤1 定义一个 Updatable 
    Updatable updatable = new Updatable() {
        @Override
        public void update() {
           //当收到更新事件时,更新TextView中的文字
            tvHello.setText("Hello Agera");

            //步骤4 注销 Updatable 
             observable.removeUpdatable(updatable);
        }
    };

    //步骤1 定义一个Observable 
    Observable observable = new Observable() {
        @Override
        public void addUpdatable(@NonNull Updatable updatable) {
            //步骤3 当某个 Updatable 被注册时,通知该 Updatable 更新
            updatable.update();
        }

        @Override
        public void removeUpdatable(@NonNull Updatable updatable) {
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_simple);

        btnTest = findViewById(R.id.btnTest);
        tvHello = findViewById(R.id.tvHello);

        btnTest.setOnClickListener(v -> 
        //步骤2 使用Observable的addUpdatable()方法去将Updatable注册到Observable中
        observable.addUpdatable(updatable));
    }
}

由于代码比较简单,此处就不在给出实现的效果图了

看到这里,有的小伙伴可能会有疑问了,这么简单一个功能,我直接在点击事件那里去更新TextView的提示就好了,何必要费那么大功夫,写一个什么观察者、被观察者去实现?(这不就是 一顿操作猛如虎,一看战绩0-5么。。。orz)

刚开始接触的时候可能会不太理解 Agera (观察者模式)的工作机制,这个例子的目的是让大家更好地理解 Agera 的工作模式

理解了上面的内容,我们继续往下看:

3、 Supplier

翻译为:数据提供者
用途:get()新数据

Supplier 是一个带泛型参数的接口,它有一个 get() 方法,返回一个T类型的值

public interface Supplier<T> {
  @NonNull
  T get();
}

Supplier 该怎么理解呢?简单来说,它只有一个功能,就是提供一个数据。甚至就可以把它理解为一个变量

它的使用是这样的:

  //定义一个数据为String类型的Supplier
  Supplier<String> stringSupplier = new   Supplier<String>() {
        @Override
        public String get() {
            //这里提供具体的值
            return "hello";
        }
    };
 
  //get()方法拿到上面提供的具体值
  //输出 hello
   Log.d("tag", "stringSupplier " + stringSupplier.get()); 


  Supplier<Integer> integerSupplier = new Supplier<Integer>() {
        @Override
        public Integer get() {
            return 666;
        }
    };
    //输出 666
    Log.d("tag", "integerSupplier " + integerSupplier.get());


  Supplier<JSONObject> jsonObjectSupplier = new Supplier<JSONObject>() {
        @Override
        public JSONObject get() {

            try {
                JSONObject jsonObject = new JSONObject("{\n" +
                        "    \"returnCode\": \"0000\",\n" +
                        "    \"returnMessage\": \"success\"\n" +
                        "}");
                return jsonObject;

            } catch (JSONException e) {
                e.printStackTrace();
            }

            return null;
        }
    };
   //输出 {"returnCode":"0000","returnMessage":"success"}
    Log.d("tag", "jsonObjectSupplier " + jsonObjectSupplier.get().toString());

看到这,你应该已经明白了它的作用了,非常简单对吧。你可能还会有疑问,只是用于提供一个值,那我直接提供一个变量值就行了,为啥要巴拉巴拉写那么大一堆?(难道又是传说中的一顿操作猛如虎。。。)

我是这么理解的,Supplier 是一个提供数据的接口,它不仅仅是提供了一个数据,关键是它代表了一种能力和一种规范,什么能力?能够提供一个数据的能力!什么规范?当需要获取数据时数据源必须是 Supplier 的规范!这种能力和规范接下来我们会接触到

在实际的代码中,它也没有像上面那样复杂,我们会使用lambda表达式还有匿名类将其简化,比如

//使用lambda表达式简化
 Supplier<String> stringSupplier = () -> "hello";


//使用lambda表达式和匿名类简化
Repositories.repositoryWithInitialValue(0)
             .observe()
             .onUpdatesPerLoop()
            //注意,getFrom方法的参数必须是一个Supplier 。这就是上面说到的规范
             .getFrom(() -> "hello agera")
             ...
             ...//后面省略了一顿操作


//如果你是用Kotlin语言的话,还可以简化成下面这样
Repositories.repositoryWithInitialValue(0)
            .observe()
            .onUpdatesPerLoop()
            //同样的参数必须是一个Supplier,此处是用lambda表达式简化
            .getFrom { "hello Kotlin" }
            ...
            ...//后面省略了一顿操作
    ...

4、 Receiver

翻译为:数据接收者
用途:accept(T vaule)接收新数据。

public interface Receiver<T> {
  void accept(@NonNull T value);
}

Receiver 是一个数据接收者,它与上面的讲的 Supplier 的作用刚好相反。Receiver 也定义了一种能力和规范。能力是接收一个数据。规范是在 Agera中接收数据时,必须使用 Receiver 接口

在实际代码中是这样的:

   repository.get()
             .ifSucceededSendTo(new Receiver<Bitmap>() {
                        @Override
                        public void accept(@NonNull Bitmap value) {
                          //value值就是我们接收到的数据
                        }
                    })
             .ifFailedSendTo(new Receiver<Throwable>() {
                        @Override
                        public void accept(@NonNull Throwable value) {

                        }
                    });

上面代码中的 ifSucceededSendTo 和 ifFailedSendTo 这两个方法的参数都是一个 Receiver 对象

Supplier 定义了一种能力,提供 一个数据的能力
Receiver 定义了一种能力,接收 一个数据的能力

5、 Repository

翻译为:Repository、数据仓库
用途:接收事件源、提供数据源的结合体

上面我们已经知道了 Updatable (观察者)和 Observable(被观察者),两个主角都已经就位了。那这个 Repository 又是何方神圣?它难道要来抢女一号?

是的,它才是真真正正主角。我们来看一下它的实现:

public interface Repository<T> extends Observable, Supplier<T> {
}

它是一个接口,继承了 Observable 接口和 Supplier 接口。

Observable 接口代表了是一个被观察者,Supplier 代表了有提供数据的能力,Repository 同时继承了这两个接口说明什么?说明了它是一个 拥有提供数据能力的被观察者

Repository 翻译为 仓库 (或者数据仓库)。名字非常形象,既然是仓库,肯定能从它那里拿到货物(也就是我们需要的数据)

我们回想一下上面的那个例子,Observable 作为一个被观察者,它能够在某个事件发生时通知 Updatable。注意,这里仅仅是通知,并没有提供任何数据。这也就是为什么会产生 Repository 的原因

Repository 弥补了不能提供数据的缺陷,它是拥有提供数据能力的被观察者。与 Observable 相比它不仅能在某个事件发生时通知 Updatable,它还能提供一个数据。当 Updatable 收到更新事件的时候,我们就可以从 Repository 中获取数据,用于后续操作

注:在实际的应用中,我们接触最多的就是Repository ,它才是真正的主角

6、 MutableRepository

翻译为:MutableRepository、可改变数据仓库
用途: 接收事件源、提供数据源的结合体、改变仓库中的数据

这里怎么突然冒出来个 MutableRepository? 什么是 MutableRepository ? 它和 Repository 有什么区别?

通过上面的介绍,我们知道,Repository 通过继承 Supplier 接口,实现了提供数据的能力。

注意哈,这里只是实现了 提供数据的能力。是不是感觉缺少点什么能力?是的,它缺少的就是 改变数据的能力!

在实际的开发中,我们仅仅能从 Repository 得到数据是不够的,我们还需要的就是能够需要改变 Repository 中的数据。这也就是 MutableRepository 存在的意义

public interface MutableRepository<T> extends Repository<T>, Receiver<T> {}

MutableRepository 也是一个接口,它继承了 Repository 接口和 Receiver 接口

继承 Receiver 接口给 MutableRepository 提供了一项新的能力,接收一个数据。通过 accept(@NonNull T value) 方法接收数据,这样就是实现了数据仓库中的数据是可改变的

四、 Repository 的分类和创建使用

Repository 的分类

通过上面的介绍我们了解到,Repository 是拥有提供数据能力的被观察者。它是一个接口,根据具体能力的不同,它的实现类被分为两种:

1、简单的 Repository

简单的 Repository 又被分为两种

  1. static repositories :提供相同的数据源而且不生成通知事件,只有get()方法
  1. mutable repositories : 可提供变化的数据源(accept输入->get输出),数据变化时生成通知事件(依赖方法Object.equals(Object))

2、复杂的repositories

复杂的数据仓库(Repository)可以响应其他数据仓库(Repositories)、任意被观察者(Observables)(也可以是该Repository的事件), 并把从其他数据源获取的数据经过同步或者异步内部转换处理后作为数据仓库(Repository)的产出值。 从响应事件中数据仓库(Repository)的数据提供者总保持数据最新的,但由于处理的复杂性,在数据仓库(Repository)不活动时,可以选择不保持数据为最新的。 任何数据消费者都需要通过注册观察者(Updatable)来表示自己需要读取数据的意图。 数据仓库(Repository)进入活动状态,但数据不用立即更新,消费者看到的数据仍然是旧的,直到数据仓库(Repository)分发第一个事件

这里所说的复杂的 repositories 就是在文章一开始给大家看的示例中的 Repository。由于复杂 Repository 相关知识点比较多,我会在后续文章中详细介绍

Repository 的创建和使用

Agera 提供了一个 Repositories 工具类来帮助我们创建 Repository

Repositories 类提供个三个静态方法,分别对应上面介绍的三种 Repository 的创建

//根据传入的值创建一个Repository
public static <T> Repository<T> repository(@NonNull final T object)

//根据传入的值创建一个可修改值的Repository(即 MutableRepository )
public static <T> MutableRepository<T> mutableRepository(@NonNull final T object)

//根据传入值作为初始值,声明一个复杂Repository(即CompiledRepository)的创建的开始
public static <T> REventSource<T, T> repositoryWithInitialValue(@NonNull final T initialValue)

简单Repository 的创建和使用

//创建一个静态的Rpository(即仓库中的值不能改变)
 Repository<Integer> integerRepository = Repositories.repository(666);
  //输出值为 666
 Log.d("tag", "integerRepository " + integerRepository.get());


 //创建可修改值的Repository(即 MutableRepository )
 MutableRepository<Integer> mutableRepository = Repositories.mutableRepository(888);
 //输出为 888
 Log.d("tag", "mutableRepository " + mutableRepository.get());

 //通过accept方法改变仓库中的值
 mutableRepository.accept(999);
 //输出为 999
 Log.d("tag", "mutableRepository " + mutableRepository.get());

到这里,我们学到了如何去创建一个Repository ,并且根据我们的需要去 更新 / 获取 Repository 中的值

还记得上面点击按钮改变文字那个例子吗?现在我们用 Repository 改进一下: 当我们点击按钮的时候,文字上显示出当前的时间,精确到秒

实现思路:

  1. 创建一个 Updatable (观察者) 和一个 MutableRepository (被观察者&& 数据提供者)
  2. 为数据仓库 MutableRepository 添加一个观察者 Updatable
  3. 当点击按钮时,更新仓库 MutableRepository 中的值
  4. 由于 MutableRepository 中的值发送了改变,此时 Updatable 会收到更新事件,在 update() 方法中获取到 MutableRepository 中的最新值,并显示出来
  5. 在不使用时注销 Updatable

代码如下:

public class SimpleActivity extends Activity {
    Button btnTest;
    TextView tvHello;
    DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA);

    //1. 创建一个 Updatable (观察者)
    Updatable updatable = new Updatable() {
        @Override
        public void update() {
            //4. 从数据仓库中获取数据
            String currentTime = mutableRepository.get();
            tvHello.setText(currentTime);
        }
    };

    //1. 创建一个 MutableRepository (被观察者&& 数据提供者)
    MutableRepository<String> mutableRepository = Repositories.mutableRepository(getCurrentTime());

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_simple);

        btnTest = findViewById(R.id.btnTest);
        tvHello = findViewById(R.id.tvHello);

        //3. 当点击按钮时,更新仓库 MutableRepository  中的值
        btnTest.setOnClickListener(v -> mutableRepository.accept(getCurrentTime()));
    }

    @Override
    protected void onResume() {
        super.onResume();
        //2. 为数据仓库 MutableRepository 添加一个观察者 Updatable
        mutableRepository.addUpdatable(updatable);
    }

    @Override
    protected void onPause() {
        super.onPause();
        //5. 移除观察者
        mutableRepository.removeUpdatable(updatable);
    }

    private String getCurrentTime() {
        return format.format(new Date());
    }
}

运行看一下效果,当我们点击按钮时,会显示当前的时间:


显示时间.gif

看到这里不知道小伙伴有没有疑问,就是为什么当MutableRepository 的值改变的时候(也就是我们点击按钮),Updatable 会接收到更新事件?

其实是这样的,MutableRepository 只是一个接口。当我们用Repositories.mutableRepository(getCurrentTime())的时候,真正创建的是一个 SimpleRepository 对象。 这个对象继承了 BaseObservable 类,并且实现了 MutableRepository接口。BaseObservable 类中帮我们做了处理,当仓库中的值改变的时候(也就是调用了 mutableRepository.accept() ) 方法后,如果接收的新值与原有的旧值不相同的话,就去通知注册过的 Updatable 更新

这一部分内容如果你还不了解的话,可以先不用深究,知道如何使用就好。后续文章会解析相关源码

注意: BaseObservable 中判断新旧值是否相等使用的是 boolean equals(Object obj) 方法

注意:Updatable 的注册和注销必须配对使用。在有声明周期的地方,比如 Activity,就可以在 onResume、onPause 等回调方法去添加/注销 Updatable

这里在给大家留一个小的思考题,上面的例子,我们是点击按钮后更新当前时间并显示出来,现在要求不用点击按钮,并且时间一直在更新(类似电子钟表)。该如何实现呢?你可以尝试自己实现一下。如果有疑问可以留言与我交流

五、 总结

这里我们做一下总结:

  1. Agera 是基于 观察者模式 的。在 Agera 中,Updatable 指代的是观察者模式中的观察者, Observable 所指代的就是观察者模式中的被观察者
  2. Supplier 接口定义了 提供 一个数据的能力
  3. Receiver 定义了 接收 一个数据的能力
  4. Repository 是一个 拥有提供数据能力的被观察者
  5. MutableRepository 是一个 拥有 提供/更新 数据能力的被观察者
  6. 简单的 Repository 通过 Repositories 类中的静态方法去创建(创建出来的对象实际上是 SimpleRepository 对象)
  7. 当 MutableRepository 中的值 改变 的时候(值的改变是通过equals方法判断的),会通知所有注册过的 Updatable 更新(这个操作是在 BaseObservable 中处理的)

下面我用一张表格对本文中所讲到的类/接口再做一个总结:

类/接口 描述 作用
Updatable 观察者 更新事件
Observable 被观察者 添加/删除Updatable
Supplier 数据提供者 提供一个数据
Receiver 数据接收者 接收一个数据
Repository 拥有提供数据能力的被观察者 添加/删除Updatable、提供一个数据
MutableRepository 拥有 提供/更新 数据能力的被观察者 添加/删除Updatable、提供/更新一个数据

六、相关代码

https://github.com/smashinggit/Study

注:此工程包含多个module,本文所用代码均在AgeraDemo下

七、预告

由于篇幅有限,这篇文章只是给小伙伴们介绍了 Agera 的基本概念和基本使用。在后面的文章中,将会详细介绍 Repository 的具体使用,敬请期待~

后续文章已经更新啦~
Android官方响应式框架Agera详解:二、Repository的创建和操作符

注:由于本人水平有限,所以难免会有理解偏差或者使用不正确的问题。如果小伙伴们有更好的理解或者发现有什么问题,欢迎留言批评指正~

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