从 android-paging 开始学习 Paging

谷歌关于 pagign 多数据源示例: https://github.com/googlecodelabs/android-paging
此示例为 kotlin 语言,使用 room + paging 进行翻页请求

当然,为了学习方便,我呕心沥血的把 kotlin 转成 java ,如果对对大家有帮助,请在底部赞赏支持一下

地址:https://github.com/liaozhoubei/Android_Sample

从 android paging 开始学习

android paging 流程:

1.监听 editText ,当发送改变时,使用 LiveData 通知 SearchRepositoriesViewModel 中的 queryLiveData 发生改变
2.queryLiveData 被 repoResult 所监听,repoResult 中的回调方法

    // 数据源
    DataSource.Factory<Integer, Repo> dataSourceFactory = cache.reposByName(query);
    RepoBoundaryCallback boundaryCallback = new RepoBoundaryCallback(query, service, cache);
    // 网络请求
    MutableLiveData<String> networkErrors = boundaryCallback.getNetworkErrors();

    LiveData data = new LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE).setBoundaryCallback(boundaryCallback).build();

3.先进行查询数据库的操作,但是查询的结果被包裹在 DataSource.Factory 这个数据源工厂中

4.然后创建 RepoBoundaryCallback 对象,调用了 GithubRepository 的方法进行网络请求。
5.RepoBoundaryCallback是 PagedList.BoundaryCallback 的子类,这是个接口类。它有两个方法:

    // PagedList的数据源的初始加载返回零项时调用
    onZeroItemsLoaded() 
    // 当数据源在列表末尾用尽数据时,onItemAtEndLoaded(Object)会调用,
    // 并且您可以启动异步网络加载,将结果直接写入数据库。
    // 由于正在观察数据库,因此绑定到该UI的UI LiveData<PagedList>将
    // 自动更新以考虑新项目。
    onItemAtEndLoaded(@NonNull Repo itemAtEnd)

这两个方法相当重要,就是他们构成了 paging下拉刷新的功能
6.RepoBoundaryCallback 接口被设置在 LivePagedListBuilder 里面,从此当数据源在末尾时,就调用 onItemAtEndLoaded() 方法,当没有数据时调用 onZeroItemsLoaded() 方法。
7.在 onItemAtEndLoaded() 中会调用请求网络的方法,请求完网络,就直接插入数据库中

实际上整个流程已经跑完了,然后运行项目发现不断的刷新数据,不断的进行网络请求,最后将请求到的数据插入数据库中。

然而令人迷惑的是网络请求完之后,就只见到数据库数据增多了,但是中间是怎么通过LiveData发送新的数据呢?

奥秘在于 :

    LiveData data = new LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE).setBoundaryCallback(boundaryCallback).build();

这行代码中,在这里获取了数据库的数据源,从此 Paging 库每次刷新了数据之后,就在内部直接更新了数据,发送到监听的 Observer 之中。

这种操作虽然好,但是在开发的时候令人非常的迷惑,因为数据都在内部更新了,完全不清楚它的走向

paging 中内置的三种 DataSource 的区别

DataSource.png

paging 中内置有三种 DataSource ,他们是:

PositionalDataSource<T>
ItemKeyedDataSource<Key, Value>
PageKeyedDataSource<Key, Value>

这三个都是抽象类,使用这几个类作为模板能够实现一个简单的dataSource。
其中 PositionalDataSource 的父类为 DataSource<Key, Value> ,而 ItemKeyedDataSource 与 PageKeyedDataSource 的父类为 ContiguousDataSource<Key, Value> ,祖父类才为 DataSource<Key, Value>。

  • PositionalDataSource 类用于加载在任意位置请求大小的页面,并提供一个固定的项目计数。这个比较容易理解,以数据库为例,我们从头第一条数据开始取,每次需要取固定10条数据,不断往后面去,也就是第一次取出 0-9 条,第二次取 10-19 条,依次类推。

PositionalDataSource 就是起着这个作用,给一个取值范围,会每次都按照取值范围顺序取出固定条数的数据

  • ItemKeyedDataSource 用于列表中加载了N条数据,加载下一页数据时,会以列表中最后一条数据的某个字段为Key查询下一页数。它的功能与 PositionalDataSource 类似,都是设置一个固定的条数来取出数据。不同的地方在于 PositionalDataSource 是在请求的时候将当前的请求位置以及请求多少条数据当参数发送,而 ItemKeyedDataSource 则要设置一个 key 来当参数,同时需要后端做支撑。

同样以每次取10条数据为例。

ItemKeyedDataSource 初始请求时,同 PositionalDataSource 一样,获取 0 - 9 条数据。但是下拉更新的时候就不一样了,它需要获取一个 key ,这个 key 是数据中的某条唯一字段,然后将 key 以及 请求的条数 当参数进行请求,获取新的数据。

从后端的角度来说就是获取到一条数据中的唯一字段,然后找到这条数据,查询这条数据中的 位置,最后获取数据当前位置 +1 到后面 10 条数据进行返回

  • PageKeyedDataSource 页面中加载了N条数据,每一页数据都会提供下一页数据的关键字Key作为下次查询的依据,基本与 ItemKeyedDataSource 雷同

paging 源码解析

LivePagedListBuilder 初始化

LivePagedListBuilder(new DataSourceFactory(), config)
            .setBoundaryCallback(null)
            .setFetchExecutor(null)
            .build();

最终会生成一个 ComputableLiveData 对象,即可以检测到生命周期的对象,并且实现 compute 抽象方法,用此抽象方法创建 DataSource 和 PageList。如下图:

LivePageDListBuilder.png

图片来源:https://blog.csdn.net/Alexwll/article/details/83246201

compute() 方法会在 LiveData 的 onActive() 方法中进行调用,实际是ObserverWrapper的activeStateChanged()方法中调用 onActive()。

我们在仔细看 compute() 方法

 protected PagedList<Value> compute() {
     ...

     do {
         ...
         mDataSource = dataSourceFactory.create();
         mDataSource.addInvalidatedCallback(mCallback);

         mList = new PagedList.Builder<>(mDataSource, config)
                 .setNotifyExecutor(notifyExecutor)
                 .setFetchExecutor(fetchExecutor)
                 .setBoundaryCallback(boundaryCallback)
                 .setInitialKey(initializeKey)
                 .build();
     } while (mList.isDetached());
     return mList;
 }

首先是获取 DataSource 对象,在 LivePagedListBuilder.build 的时候传入了 DataSourceFactory ,在这里回调DataSourceFactory.create(), 创建具体的 DataSource。

其次创建了 PagedList 对象,并且将 DataSource 传入 。PagedList是 paging 中要具操控中的对象。

至此,Paging 完成了第一步,数据源与数据列表的创建与绑定

PagedList 源码

PagedList 继承结构图:


AbstractList.png

上面在创建 PagedList 的时候使用了 Builder,实质上是调用了 PagedList.create() 方法,代码如下:

static <K, T> PagedList<T> create(@NonNull DataSource<K, T> dataSource,
        @NonNull Executor notifyExecutor,
        @NonNull Executor fetchExecutor,
        @Nullable BoundaryCallback<T> boundaryCallback,
        @NonNull Config config,
        @Nullable K key) {
    if (dataSource.isContiguous() || !config.enablePlaceholders) {
        int lastLoad = ContiguousPagedList.LAST_LOAD_UNSPECIFIED;
        if (!dataSource.isContiguous()) {
            //noinspection unchecked
            dataSource = (DataSource<K, T>) ((PositionalDataSource<T>) dataSource)
                    .wrapAsContiguousWithoutPlaceholders();
            if (key != null) {
                lastLoad = (Integer) key;
            }
        }
        ContiguousDataSource<K, T> contigDataSource = (ContiguousDataSource<K, T>) dataSource;
        return new ContiguousPagedList<>(contigDataSource,
                notifyExecutor,
                fetchExecutor,
                boundaryCallback,
                config,
                key,
                lastLoad);
    } else {
        return new TiledPagedList<>((PositionalDataSource<T>) dataSource,
                notifyExecutor,
                fetchExecutor,
                boundaryCallback,
                config,
                (key != null) ? (Integer) key : 0);
    }
}

上面使用 dataSource.isContiguous() 以及 config.enablePlaceholders 判断是创建 ContiguousPagedList 还是 TiledPagedList,它们的意义如下:

dataSource.isContiguous() : 如果数据源保证生成一组连续的项,则返回true,而不会生成空白。

config.enablePlaceholders : 定义PagedList是否可以显示空占位符(如果DataSource提供它们)。

进入 ContiguousPagedList 构造方法 :

ContiguousPagedList(
        @NonNull ContiguousDataSource<K, V> dataSource,
        @NonNull Executor mainThreadExecutor,
        @NonNull Executor backgroundThreadExecutor,
        @Nullable BoundaryCallback<V> boundaryCallback,
        @NonNull Config config,
        final @Nullable K key,
        int lastLoad) {

    super(new PagedStorage<V>(), mainThreadExecutor, backgroundThreadExecutor,
            boundaryCallback, config);
    mDataSource = dataSource;
    mLastLoad = lastLoad;
    // 当数据源无效时
    if (mDataSource.isInvalid()) {
        detach();
    } else {
        mDataSource.dispatchLoadInitial(key,
                mConfig.initialLoadSizeHint,
                mConfig.pageSize,
                mConfig.enablePlaceholders,
                mMainThreadExecutor,
                mReceiver);
    }
    mShouldTrim = mDataSource.supportsPageDropping()
            && mConfig.maxSize != Config.MAX_SIZE_UNBOUNDED;
}

首先看到这行代码:

super(new PagedStorage<V>(), mainThreadExecutor, backgroundThreadExecutor,
            boundaryCallback, config);

这里创建了 PagedStorage 对象,这个对象是保持页面数据的实际对象,实则在里面用 ArrayList 来实现各种增删改查的操作,这个后面再说。

然后调用了 mDataSource.dispatchLoadInitial() 方法,从名字上就可以看出它是处理加载初始化的方法,跟进此方法,发现它是 ContiguousDataSource.dispatchLoadInitial() ,由 ItemKeyedDataSource 以及 PageKeyedDataSource 进行实现。最后调用 loadInitial() 这个由我们具体实现的获取初始化数据的方法,获取数据后,通过接口回调的方式通知主线程,最后通过 LiveData 的 observe()接口返回到需要数据的 Adapter 中。

以上就完成了整个 Paging 的初始化调用。

初始化示例代码如下:

    PagedList.Config config = new PagedList.Config.Builder()
            .setPageSize(10)                         //配置分页加载的数量
            .setEnablePlaceholders(false)     //配置是否启动PlaceHolders
            .setInitialLoadSizeHint(10)              //初始化加载的数量
            .build();

    LiveData<PagedList<DataBean>> liveData = new LivePagedListBuilder(
        new MyDataSourceFactory(), config).build();

    liveData.observe(this,new Observer<PagedList<DataBean>>() {
        @Override
        public void onChanged(@Nullable PagedList<DataBean> dataBeans) {
            // 每次数据更改后都从此接口获取数据
            mAdapter.submitList(dataBeans);
        }
    });

数据上拉与下拉刷新

我们为什么要用 Paging 这个库,为的就是更便捷的上拉刷新,所以我们必须得清楚它的上拉刷新机制。

我们跟踪一下 PositionalDataSource 上拉时的表现,在上拉的时候,它会调用 loadRange() 方法,所以我们一步步更上去看它的调用链。

最后发现它的调用者为 PagedListAdapter 中的 getItem(int position) 方法,详情如下:

public T getItem(int index) {
    ...
    mPagedList.loadAround(index);
    return mPagedList.get(index);
}

这里的 index 实际上是界面上显示的最后一条数据的位置,同时这最后一条数据也是这条数据在已保存的列表中的位置。Paging 会将获取到的数据全部保持到一个 ArrayList 之中。

loadRange() 又调用了 loadAround() 方法,然后 loadAround() 又调用了抽象方法 loadAroundInternal(index),如下

public void loadAround(int index) {
    if (index < 0 || index >= size()) {
        throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size());
    }

    mLastLoad = index + getPositionOffset();
    // 具体加载数据方法
    loadAroundInternal(index);

    mLowestIndexAccessed = Math.min(mLowestIndexAccessed, index);
    mHighestIndexAccessed = Math.max(mHighestIndexAccessed, index);

    tryDispatchBoundaryCallbacks(true);
}

loadAroundInternal(index) 方法是在 TiledPagedList 与 ContiguousPagedList 中会执行不同逻辑。

TiledPagedList 如下:

@Override
protected void loadAroundInternal(int index) {
    mStorage.allocatePlaceholders(index, mConfig.prefetchDistance, mConfig.pageSize, this);
}

mStorage.allocatePlaceholders 又是什么呢?它是一个 PagedStorage ,用于存储分片数据的类,内部是由一系列 List 的增删改查操作实现的, allocatePlaceholders() 方法如下:

public void allocatePlaceholders(int index, int prefetchDistance,
                                 int pageSize, Callback callback) {
    if (pageSize != mPageSize) {
        if (pageSize < mPageSize) {
            throw new IllegalArgumentException("Page size cannot be reduced");
        }
        if (mPages.size() != 1 || mTrailingNullCount != 0) {
            // not in single, last page allocated case - can't change page size
            throw new IllegalArgumentException(
                    "Page size can change only if last page is only one present");
        }
        mPageSize = pageSize;
    }

    final int maxPageCount = (size() + mPageSize - 1) / mPageSize;
    int minimumPage = Math.max((index - prefetchDistance) / mPageSize, 0);
    int maximumPage = Math.min((index + prefetchDistance) / mPageSize, maxPageCount - 1);

    allocatePageRange(minimumPage, maximumPage);
    int leadingNullPages = mLeadingNullCount / mPageSize;
    for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) {
        int localPageIndex = pageIndex - leadingNullPages;
        if (mPages.get(localPageIndex) == null) {
            //noinspection unchecked
            mPages.set(localPageIndex, PLACEHOLDER_LIST);
            callback.onPagePlaceholderInserted(pageIndex);
        }
    }
}

这片代码的逻辑也很简单,只有在 mPages.get(localPageIndex) 也就是下一页分片不存在的时候调用 callback.onPagePlaceholderInserted(pageIndex) ,此方法会在线程中调用 PositionalDataSource 的 loadRange() 方法。另外 onPagePlaceholderInserted() 方法只在 TiledPagedList 中实现, ContiguousPagedList中执行会抛出异常

ContiguousPagedList 如下:

@MainThread
@Override
protected void loadAroundInternal(int index) {
    int prependItems = getPrependItemsRequested(mConfig.prefetchDistance, index,
            mStorage.getLeadingNullCount());
    int appendItems = getAppendItemsRequested(mConfig.prefetchDistance, index,
            mStorage.getLeadingNullCount() + mStorage.getStorageCount());

    mPrependItemsRequested = Math.max(prependItems, mPrependItemsRequested);
    if (mPrependItemsRequested > 0) {
        schedulePrepend();
    }

    mAppendItemsRequested = Math.max(appendItems, mAppendItemsRequested);
    if (mAppendItemsRequested > 0) {
        scheduleAppend();
    }
}

其中有两个重要的变量

mPrependItemsRequested :向前需要多少条数据,当其 >0 时,会调用 DataSource(ItemKeyedDataSource与PageKeyedDataSource) 的 loadBefore() 方法(PositionalDataSource 则调用 dispatchLoadRange() 方法)
mAppendItemsRequested : 向后需要多少条数据,当其 >0 时,会调用 DataSource 的 loadAfter() 方法

mPrependItemsRequested 的值为获取自己与 prependItems 之间的最大值 ,mAppendItemsRequested 则是获取自己与 appendItems 的最大值。prependItems 与 appendItems 则是通过以下两个方法计算出来的。

static int getPrependItemsRequested(int prefetchDistance, int index, int leadingNulls) {
    return prefetchDistance - (index - leadingNulls);
}

static int getAppendItemsRequested(
        int prefetchDistance, int index, int itemsBeforeTrailingNulls) {
    return index + prefetchDistance + 1 - itemsBeforeTrailingNulls;
}

方法中第一个参数 prefetchDistance ,一般是设置 PagedList.Config 时的 PageSize , 其含义为:预取距离,用于定义加载前的距离。如果此值设置为50,则分页列表将尝试提前加载50个项目已经访问过的数据。

第二个参数 index 则是当前 item 所处的位置,上面有提及。

第三个参数 leadingNulls 和 itemsBeforeTrailingNulls 表示列表中为 null 的数据的个数,可不理会。

现在我们就知道上拉和下拉的逻辑了。假如我们设置每页的数据需要 10 条,当我们把页面往上拖动,需要加载更多数据,这时当前数据存量为10条,此时页面底部的 index 为 10,拖拽时,appendItems = 10(index)+ 10(mConfig.prefetchDistance) - 10(mStorage.getStorageCount() 当前数据存量),最后得到结果为 10,这时就需要请求网络加载更多。

加载完毕后在 onPageAppended() 方法更新 mAppendItemsRequested 的值,如下:

@MainThread
@Override
public void onPageAppended(int endPosition, int changedCount, int addedCount) {
    // consider whether to post more work, now that a page is fully appended
    mAppendItemsRequested = mAppendItemsRequested - changedCount - addedCount;
    mAppendWorkerState = READY_TO_FETCH;
    if (mAppendItemsRequested > 0) {
        // not done appending, keep going
        scheduleAppend();
    }

    // finally dispatch callbacks, after append may have already been scheduled
    notifyChanged(endPosition, changedCount);
    notifyInserted(endPosition + changedCount, addedCount);
}

如此便完成了一个闭环。

同理,下拉刷新时也是同样进行更新数据,当界面中最顶上的 index 小于预设的 PageSize 的时候,就会触发 loadBefore() ,最后在 PagedStroage 将加载出来的数据加到 ArrayList 的最顶上。

PagedList.Config 配置信息

PagedList.Config 的配置参数会影响到 Paging 的 DataSource 的使用,以下稍微解释一下这些参数:

setEnablePlaceholders(boolean) : 此选项会影响到具体初始化那种 PageList,而不同的 PageList 会影响到是否要调用 BoundaryCallback 这个监听数据是否到达边界的回调。

当设置为 true 的时候,会将数据源初始化为 PositionalDataSource ,为fasle 时初始化为:ContiguousDataSource 。也就是说实例化 DataSource 的时候并不一定取决于我们设置了哪种 DataSource ,而是取决于 PagedList.Config 的参数。 PagedList 初始化 DataSource 代码如下(PagedList.create()):

        PagedList<T> create(@NonNull DataSource<K, T> dataSource,
            @NonNull Executor notifyExecutor,
            @NonNull Executor fetchExecutor,
            @Nullable BoundaryCallback<T> boundaryCallback,
            @NonNull Config config,
            @Nullable K key) {
        if (dataSource.isContiguous() || !config.enablePlaceholders) {
            if (!dataSource.isContiguous()) {
                //noinspection unchecked
                dataSource = (DataSource<K, T>) ((PositionalDataSource<T>) dataSource)
                        .wrapAsContiguousWithoutPlaceholders();
                if (key != null) {
                    lastLoad = (Integer) key;
                }
            }
            ContiguousDataSource<K, T> contigDataSource = (ContiguousDataSource<K, T>) dataSource;
            return new ContiguousPagedList<>(contigDataSource,
                    notifyExecutor,
                    fetchExecutor,
                    boundaryCallback,
                    config,
                    key,
                    lastLoad);
        } else {
            return new TiledPagedList<>((PositionalDataSource<T>) dataSource,
                    notifyExecutor,
                    fetchExecutor,
                    boundaryCallback,
                    config,
                    (key != null) ? (Integer) key : 0);
        }

从中可看出仅有 dataSource.isContiguous() 为 ture 以及 config.enablePlaceholders 为 false 时才会初始化 ContiguousPagedList ,其余的时候都初始化 TiledPagedList。

其次,config.enablePlaceholders 的默认值为 ture, 也就是说默认情况下初始化的都是 TiledPagedList 。

在实际测试中,config.enablePlaceholders 为 false, BoundaryCallback 为 null 时, 并且 DataSource 为 PositionDataSource 的时候,此时界面中可正常加载数据。

但当 config.enablePlaceholders 为 true 时,却发现界面只加载了第一份数据,不在继续往下加载。

究其原因:
PositionalDataSource 在上拉的时候,是调用子类的 landRange 方法,

当 config.enablePlaceholders 为 true 时,会初始化PositionalDataSource,同时 PageList 初始化为 TiledPagedList。在 TiledPagedList 的 loadAroundInternal 上拉刷新中,此时会调用 PagedStorag.allocatePlaceholders 方法,在这个方法中判断是否要调用 PositionalDataSource.landRange,然而 mPages.get(localPageIndex) 不会为 null, 导致landRange不被调用。如下(位于 PagedStorag.allocatePlaceholders() ):

    public void allocatePlaceholders(int index, int prefetchDistance,
                                     int pageSize, Callback callback) {
    ....
        for (int pageIndex = minimumPage; pageIndex <= maximumPage; pageIndex++) {
            int localPageIndex = pageIndex - leadingNullPages;
            if (mPages.get(localPageIndex) == null) {
                Log.e("PagedStorage", "mPages.get(localPageIndex) == null" );
                //noinspection unchecked
                mPages.set(localPageIndex, PLACEHOLDER_LIST);
                callback.onPagePlaceholderInserted(pageIndex);
            }
        }
    }

ContiguousDataSource 在上拉的时候是调用 loadAfter 方法。
当 config.enablePlaceholders 为 false 时,会初始化 ContiguousWithoutPlaceholdersWrapper, 将 会初始化PositionalDataSource 包装为 ContiguousDataSource, 同时 PageList 初始化为 ContiguousPagedList。在ContiguousPagedList 的上拉刷新 loadAroundInternal 中调用scheduleAppend, 然后调用 ContiguousDataSource。dispatchLoadAfter,接着调用 ContiguousWithoutPlaceholdersWrapper.dispatchLoadAfter,最后调用到 PositionalDataSource.landRange 刷新数据。

dataSource.isContiguous() 是 dataSource 的抽象方法,由子类赋值, PositionDataSource / TitledDataSource 为 false, ContiguousDataSource 为 true . 同时 PagedList 中也有此参数,但是与 DataSource 中保持一致。

BoundaryCallback

// 如果设置为true,则mBoundaryCallback为非null,并且应在附近加载时调度
private boolean mBoundaryCallbackBeginDeferred = false;
private boolean mBoundaryCallbackEndDeferred = false;   

// loadAround访问的最低和最高索引。 用于决定何时应调度mBoundaryCallback
private int mLowestIndexAccessed = Integer.MAX_VALUE;
private int mHighestIndexAccessed = Integer.MIN_VALUE;


// 此方法在 DataSource 的 loadInitial() / loadRange() 调用后,调用 onResult(@NonNull List<T> data) 时调用
deferBoundaryCallbacks(final boolean deferEmpty,
            final boolean deferBegin, final boolean deferEnd)

// mLowestIndexAccessed / mHighestIndexAccessed已更新,因此请检查是否需要调度边界回调。 边界回调延迟到最后一项加载,并且访问发生在边界附近。
// 注意:我们在此处发布,因为RecyclerView可能希望在响应中添加项目,并且此调用发生在PagedListAdapter bind中。
tryDispatchBoundaryCallbacks(true);

关于 BoundaryCallback 的理解还不够透彻,目前有个疑惑就是在单数据源的时候,上拉到底部不会调用 BoundaryCallback.onItemAtEndLoaded() 方法,而是调用 DataSource 的加载更多的方法。目前还没有研究透彻,先搁置一边吧。

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