RecyclerView学习

RecyclerView

RecyclerView,顾名思义,这个View代表的就是一个可循环使用的视图集合控件,它定义了ViewHolder类型标准,封装了View缓存判断逻辑,更强大的是它可以通过一个LayoutManager将一个RecyclerView显示为不同的样式。

目的: 在有限的屏幕内展示大量的内容。

RecyclerView的五虎将

RecyclerView.LayoutManager :负责Item视图的布局的显示管理

RecyclerView.ItemDecoration:给每一项Item视图添加子View,例如可以进行画分割线之类的

RecyclerView.ItemAnimator:负责处理数据添加或者删除时候的动画效果

RecyclerView.Adapter:为每一项Item创建视图

RecyclerView.ViewHolder:承载Item视图的子布局

常用方法

RecyclerView与ListView、GridView类似,都是可以显示同一类型View的集合的控件。首先看看最简单的用法,四步走:

0.介入build.gradle文件中加入

compile 'com.android.support:recyclerview-v7:24.0.0'

1.创建对象

RecyclerView recyclerview = (RecyclerView) findViewById(R.id.recyclerview);

2.设置RV的布局管理器,决定了RV的显示风格

recyclerview.setLayoutManager(new LinearLayoutManager(this,

LinearLayoutManager.VERTICAL, false));

RecyclerView将所有的显示规则交给LayoutManager去完成,LayoutManager是一个抽象类,系统已经为我们提供了三个默认的实现类,分别是LinearLayoutManager、GridLayoutManager、StaggerdGridlayoutManager,从名字我们就可以看出,分别是,线性显示,网格显示,瀑布流显示,当然你也可以通过继承这些类来扩展自己的LayoutManager。

3.设置适配器

recyclerview.setAdapter(adapter);

适配器,同ListView一样,用来设置每个item显示内容。通常,我们写ListView适配器,都是首先继承BaseAdapter,实现四个抽象方法(getView, getItem, getCount, getItemId),创建一个静态ViewHolder,getView()方法中判断convertView是否为空,创建还是获取viewHolder对象。

而RecyclerView也是类似的步骤,首先继承RecyclerView.Adapter类,实现三个抽象方法,创建一个静态类的ViewHolder,不过RecyclerView的ViewHolder创建稍微有些限制,类名就是上面继承的时候泛型中声明的类名,并且ViewHOder必须继承自RecyclerView.ViewHolder类

public class DemoAdapter extends RecyclerView.Adapter<DemoAdapter.VH> {

private List<Data> dataList;

private Context context;

public DemoAdapter(Context context, ArrayList<Data> datas) {

    this.dataList = datas;

    this.context = context;

}

@Override

public VH onCreateViewHolder(ViewGroup parent, int viewType) {

    return new VH(View.inflate(context, android.R.layout.simple_list_item_2, null));

}

@Override

public void onBindViewHolder(VH holder, int position) {

    holder.mTextView.setText(dataList.get(position).getNum());

}

@Override

public int getItemCount() {

    return dataList.size();

}

public static class VH extends RecyclerView.ViewHolder {

    TextView mTextView;

    public VH(View itemView) {

        super(itemView);

        mTextView = (TextView) itemView.findViewById(android.R.id.text1);

    }

}

}

其他方法

除了常用的方法,还有不常用的方法。

瀑布流与滚动方向

前面已经介绍过了,RecyclerView实现瀑布流,可以通过一句话设置:RecyclerView.setLayoutManager(new StaggeredGridLayoutManager(2,VERTICAL))就可以了。

其中StaggerGridLayoutManager的第一个参数表示列数,就好像GridView的列数一样,第二个参数表示方向,可以很方便的实现横向滚动或者纵向滚动。

添加删除item的动画

同ListView每次修改了数据后,都要调用notifyDataSetChanged刷新每项item类似,只不过RecyclerView还支持局部刷新notifyItemInserted(index);notifyItemRemoved(position);notifyItemChanged(position);

在添加或删除了数据时,recyclerView还提供了一个默认的动画效果,来改变显示,同时你也可以定制自己的动画效果:模仿DefaultItemAnimator或者直接继承这个类,实现自己的动画效果。并调用RecyclerView.setItemAnimator(new DefaultItemAnimator())设置上自己的动画。

LayoutManager的常用方法

findFistVisibleItemPosition();返回当前第一个可见Item的position

findFirstCompletelyVisibleItemPosition();返回当前第一个完全可见的Item的position

findLastVisibleItemPosition();返回当前最后一个可见Item的position

findComplete领了也VisivleItemPosition();返回当前最后一个完全可见Item的Position

ScrollBy();滚动到某个位置

LayoutManager工作原理

首先RecyclerView继承关系,可以看到与ListView不同,他是一个ViewGroup,既然是一个View,那么不可少的就是经历onMeasure()、onLayout()、onDraw()这三个方法,实际上RecyclerView就是将onMeasure()、onLayout()交给了LayoutManager去处理,因此如果给RecyclerView设置不同的LayoutManager就可以达到不同的显示效果,因为onMeasure()、onLayout()都不同。

ItemDecoration 工作原理

ItemDecoration是为了显示每个Item之间分隔样式的,它的本质上就是一个Drawable。当RecyclerView执行到onDraw()方法的时候,就会调用它的onDraw(),这时,如果你重写了这个方法,就相当于直接在RecyclerView上画了一个Drawable表现的东西,而最后,在它的内部还有一个叫getItemOffsets()的方法,从字面就可以理解,它是用来偏移每个Item视图的,当我们在每个Item视图之间强行插入一段Drawable,那么如果在照着原本的逻辑去给item视图,就会覆盖掉Decoration了,所以需要getItemOffsets()这个方法,让每个item往后偏移一点,不要覆盖到之前画的上的分个样式。

ItemAnimatior

每一个item在特定情况下都会执行的动画,说是特定情况,其实就是在视图发生改变,我们手动调用notifyxxxx()的时候,通常这个时候我们要传一个下标,那么从这个标记开始一直到结束,所有item都会被执行一次这个动画

Adapter工作原理

首先是适配器,适配器的作用都是类似的,用于提供每一个item,并返回给RecyclerView作为其子布局添加到内部,但是,与ListView不同的是:ListView的适配器是直接返回一个View,将这个View加入到ListView内部;RecyclerView是返回一个ViewHolder并且不是直接将这个holder加入到视图内部,而是加入一个缓存区域,在视图需要的时候去缓存区找到holder在间接找到holder包裹的View

ViewHolder

每一个ViewHolder的内部都是一个View,并且ViewHolder必须继承自RecyclerView.ViewHolder类,这主要是因为RecyclerView内部的缓存结构并不是像ListView那样去缓存一个View,而是直接缓存一个ViewHolder,在ViewHolder的内部又持有一个View,既然是缓存一个ViewHolder,那么当然所有ViewHolder都继承同一个类才能做到。

缓存与复用的原理

RecyclerView的内部维护了一个四级缓存,滑出界面的ViewHolder会暂时放到cache结构中,而从cache结构中移除的ViewHolder,则会放到一个叫做RecyclerViewPool的循环缓存池中。

顺带一说,RecyclerView的性能并不比ListView好多少,它的最大的优势在于其扩展性,但是有一点,在RecyclerView内部的这个第二级缓存池RecyclerViewPool是可以被多个RecyclerView公用的。这一点比起直接缓存View的ListView就要高明了很多,但是正是应为需要被多个RecyclerView公用,所以我们的ViewHolder必须继承自同一个基类。

默认的情况下,cache缓存2个holder,RecyclerViewPool缓存5个holder,对于二级缓存池中holder对象,会根据ViewType进行分类,不同类型的ViewType之间互不影响。缓存这块后面有具体分析。

源码解析

onMeasure

既然是一个自定义View,那么我们先从onMeasure()开始看

之前我们说RecyclerView的Measure和layout都交给了LayoutManager去做

if (mLayout.mAutoMeasure) {

    final int widthMode = MeasureSpec.getMode(widthSpec);

    final int heightMode = MeasureSpec.getMode(heightSpec);

    final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY

            && heightMode == MeasureSpec.EXACTLY;

    mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

    // 如果RV是 精确模式,则直接使用 mLayout测量宽高即可

    if(skipMeasure || mAdapter == null) return;

} else {

    mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

}


不论是否启用mAutoMeasure最终都会调用mlayout.onMeasure()方法中,而这个mLayout就是一个LayoutManager对象

我们先挑LinearLayoutManager来看,发现并没有它的onMeasure方法,LinearLayoutManager直接继承自LayoutManager,所以又回到了父类LayoutManager中。

void defaultOnMeasure(int widthSpec, int heightSpec) {

    final int width = LayoutManager.chooseSize(widthSpec,

            getPaddingLeft() + getPaddingRight(),

            ViewCompat.getMinimumWidth(this));

    final int height = LayoutManager.chooseSize(heightSpec,

            getPaddingTop() + getPaddingBottom(),

            ViewCompat.getMinimumHeight(this));

    setMeasuredDimension(width, height);

}


有一句非常奇葩的注释:在这里直接调用LayoutManager静态方法并不完美,因为本身就是在内部类,更好的办法调用一个单独的方法

接着是chooseSize()方法,很简单,直接根据测量值和模式返回了最适大小

public static int chooseSize(int spec, int desired, int min) {

    final int mode = View.MeasureSpec.getMode(spec);

    final int size = View.MeasureSpec.getSize(spec);

    switch (mode) {

        case View.MeasureSpec.EXACTLY:

            return size;

        case View.MeasureSpec.AT_MOST:

            return Math.min(size, Math.max(desired, min));

        case View.MeasureSpec.UNSPECIFIED:

        default:

            return Math.max(desired, min);

    }

}


紧接着是对子控件Measure,调用了dispatchLayoutStep2(),调用了相同的方法,子控件的Measure在layout过程中讲解

onLayout

然后我们看layout过程,在onLayout()方法中间接调用到这么一个方法:dispatchLayoutStep2(),在它之中又调用到了mLaoutChildern()方法

这个方法在LayoutManager中的实现是空的,那么想必是在子类中实现的,然后找到LinearLayoutManager,根上面Measure过程一样,调用dispatchLayoutStep2()跟进去有这么一个方法:

fill(recycler, mLayoutState, state, false);


recycler,是一个全局的回收复用池,用于对每个itemView回收及其复用提供支持,

while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore (state)) {

    layoutChunk(recycler, state, layoutState, layoutChunkResult);

    layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;

    if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {

        layoutState.mScrollingOffset += layoutChunkResult.mConsumed;

      if (layoutState.mAvailable < 0) {

            layoutState.mScrollingOffset += layoutState.mAvailable;

        }

        recycleByLayoutState(recycler, layoutState);

    }

}

viod layoutChunk() {

    View view = layoutState.next(recycler);

        LayoutParams params = (LayoutParams) view.getLayoutParams();

        if (layoutState.mScrapList == null) { // 使用缓存的view,并添加进RV中

            if (mShouldReverseLayout == (layoutState.mLayoutDirection

                    == LayoutState.LAYOUT_START)) {

                addView(view);

            } else {

                addView(view, 0);

            }

        } else {

            if (mShouldReverseLayout == (layoutState.mLayoutDirection

                    == LayoutState.LAYOUT_START)) {

                addDisappearingView(view);

            } else {

                addDisappearingView(view, 0);

            }

        }

        measureChildWithMargins(view, 0, 0); // 测量被添加到 RV 中的 item 的宽高

        // To calculate correct layout position, we subtract margins.

        // 根据所设置的 Decoration,Margins等属性确定 item 的显示位置

        layoutDecoratedWithMargins(view, left, top, right, bottom);

    }


fill()作用就是根据当前状态LayoutState决定是应该从缓存池中取itemview填充,还是应该回收当前的itemview,完成子view 的测量布局操作。使用 while 循环判断是否有足够的空间来绘制一个完整的子view

其中layoutChunk()负责从缓存池recycler中取itemview,并调用addView()将取到的itemview添加到RecyclerView中去,并调用itemview自身的layout方法区布局item的位置

同时在这里,还调用MeasureChildWithMargins()来测绘子控件带下以及设置显示位置,这一步我们在下面的draw过程中讲。

而这全部的添加逻辑都放在一个while循环里面,不停的添加itemview到RecyclerView里面,直到塞满所有可见区域为止.

onDraw

@Override

public void onDraw(Canvas c) {

    super.onDraw(c);

    final int count = mItemDecorations.size();

    for (int i = 0; i < count; i++) {

        mItemDecorations.get(i).onDraw(c, this, mState);

    }

}


在onDraw中除了绘制自己以外,还多调用了一个mItemDecorations的onDraw方法,这个mItemDecorations就是前面提到的分割线的集合

之前在讲RecyclerView的五虎上将的时候就讲过这个ItemDecoration,当时我们还重写了一个方法叫getItemOffsets() 目的是为了不让itemview挡住分割线。

还记得layout时说的那个MeasureChildWithMargins(),就在这里

public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {

    final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);

    widthUsed += insets.left + insets.right;

    heightUsed += insets.top + insets.bottom;

    if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {

        child.measure(widthSpec, heightSpec);

    }

}


在itemview Measure的时候,会把偏移量也计算出来,也就是说:其实ItemDecoration的宽高是计算在itemview中的,只不过itemview本身绘制区域没有那么大,留出来的地方正好的透明的,于是就透过itemview显示出来,ItemDecoration,那么就很有意思了,如果我故意在ItemDecoration的偏移量中写成0,那么itemview就会挡住ItemDecoration,而在itemview的增加或删除的时候,会短暂的消失,这时候又可以透过itemview看到ItemDecoration的样子,使用这种组合还可以做出意想不到的动画效果。

小结 RV 将 测量和布局的工作放心的委托给了 LayoutManager 来执行。不同的布局风格使用不同的 LayoutManager ,这是一种策略模式。

滚动

前面我们已经完整的走完了RecyclerView的绘制流程,接下来我们再看看它在滚动的时候代码又是怎么调用的,自然要看onTouch()方法的MOVE状态

case MotionEvent.ACTION_MOVE: {

    final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId);

    final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);

    final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);

    int dx = mLastTouchX - x;

    int dy = mLastTouchY - y;

    if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {

...

    }

    if (mScrollState != SCROLL_STATE_DRAGGING) {

...

        if (startScroll) {

            setScrollState(SCROLL_STATE_DRAGGING);

        }

    }

    if (mScrollState == SCROLL_STATE_DRAGGING) {

        mLastTouchX = x - mScrollOffset[0];

        mLastTouchY = y - mScrollOffset[1];

        if (scrollByInternal(

                canScrollHorizontally ? dx : 0,

                canScrollVertically ? dy : 0,

                vtev)) {

            getParent().requestDisallowInterceptTouchEvent(true);

        }

    }

} break;


看到这个代码的时候,我们就会有疑问MotionEventCompa这个类是干什么的,它是V4包里面提供的一个工具类,用于兼容低版本的触摸屏手势,平时用的时候更多的是用它来处理多点触控的情况,当成MotionEvent就可以了。

dispatchNestedPreScroll(),用于处理嵌套逻辑,例如在ScrollView里面放一个RecyclerView,如果是以前用ListView,还得把高度写死,禁止ListView的复用和滚动逻辑,而RecyclerView则完全不需要更多处理,直接用就是了,而且有一个非常好的地方,如果放到ScrollView里面,ListView的Itemview是不会复用的,如果RecyclerView因为是全局公用一套缓存池,虽说嵌套到ScrollView效率会低很多,但是比起ListView嵌套要好很多,之后将缓存池的时候我们继续讲,

再之后,如果在相应方向上手指move的距离达到最大值,则认为需要滚动,并且设置为滚动状态。

接着走出if块,如果是滚动状态,则调用滚动方法scrollByInternal()执行相应方向的滚动,滚动的距离当然就是手指移动的距离,跟进去,它就是调用了LinearLayoutManager.scrollBy()方法

回收与复用

前面讲layout、滚动的时候,都出现了一个东西,叫Recycler。

public final class Recycler {

final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();

private ArrayList<ViewHolder> mChangedScrap = null;

final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

private final List<ViewHolder>

        mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

private RecycledViewPool mRecyclerPool;

private ViewCacheExtension mViewCacheExtension;


Recycler 的缓存复用机制就是通过Recycler 中的这些数据容器来实现的。它根据访问优先级从上到下可以分为

4级。

mAttachedScrap&mChangedScrap

mCachedViews

ViewCacheExtension

RecycledViewPoll

第一级缓存 mAttachedScrap&mChangedScrap

主要用来缓存屏幕内的ViewHolder。为什么要缓存屏幕内的VH那?

当我们通过下拉刷新来更新列表内容时,不会创建新的VH,只是在原来的VH 基础上进行重新绑定新的数据即可,而这些旧的VH就被保存在这两个集合中。即当我们调用 notifyXXX 方法时,就会向这两个列表进行填充,将旧VH缓存起来。

第二级缓存:mCachedViews

就是上面的一系列mCachedViews,如果仍依赖于RecyclerView(比如已经滑出可视范围,但还没有被移除掉),但已经被标记移除的ItemView集合会被添加到mAttachScrap中,然后如果mAttachedScrap中不在依赖时会被加入到mCachedViews中,mChangedScrap则是存储notifyxxxx方法时需要改变ViewHolder

用来缓存移除至屏幕外的VH,默认情况下缓存个数是2个,可以通过 setViewCachedSize 来改变缓存的容量大小。如果mCachedViews已满,则会根据FIFO 移除旧VH添加新VH。

试想一下,刚被移除屏幕的VH 有可能接下来马上就会再次使用,所以RV不会立即将其设置为无效的VH,而是将他们保存到 cache 中。出于内存消耗的问题,cache 不能将所有移除屏幕外的VH 都缓存起来,所以它的默认容量是2.

第三级缓存 ViewCacheExtension

ViewCacheExtension是一个抽象静态类,用于充当附加的缓存池,当RecyclerView从第一级缓存找不到需要的View时,将会从ViewCacheExtension中找,不过这个缓存是由开发者维护的,如果没有设置它,则不会启用,通常我们也不会去设置它,系统已经预先提供了两级缓存,除非有特殊需求,比如在调用系统的缓存池之前,返回一个特定的视图,才会用到它。

第四级缓存 RecycledViewPool

同样是用来缓存屏幕外的ViewHolder. 当 mCachedViews 中的个数已满,则从mCachedViews 中淘汰的 VH会缓存到RecycledViewPool中。RecycledViewPool 会将 VH 的内部数据全部清理,因此从RecycledViewPool中取出来的 VH 需要重新调用 onBindViewHolder 绑定数据

之前讲了,与ListView直接缓存Itemview不同,从上面代码中我们也能看到,RecyclerView缓存的是ViewHolder,而ViewHolder里面包含了一个View这也就是为什么写Adapter的时候,必须继承一个固定的ViewHolder的原因,我们来看一下RecyclerViewPool

public static class RecycledViewPool {

// 根据 viewType 保存的被废弃的 ViewHolder 集合,以便下次使用

private SparseArray<ArrayList<ViewHolder>> mScrap = new SparseArray<ArrayList<ViewHolder>>();

/**

  * 从缓存池移除并返回一个 ViewHolder

  */

  public ViewHolder getRecycledView(int viewType) {

    final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);

    if (scrapHeap != null && !scrapHeap.isEmpty()) {

      final int index = scrapHeap.size() - 1;

      final ViewHolder scrap = scrapHeap.get(index);

      scrapHeap.remove(index);

      return scrap;

    }

      return null;

    }

public void putRecycledView(ViewHolder scrap) {

    final int viewType = scrap.getItemViewType();

    final ArrayList scrapHeap = getScrapHeapForType(viewType);

    if (mMaxScrap.get(viewType) <= scrapHeap.size()) {

      return;

    }

    scrap.resetInternal();

    scrapHeap.add(scrap);

  }

  /**

  * 根据 viewType 获取对应缓存池

  */

  private ArrayList<ViewHolder> getScrapHeapForType(int viewType) {

    ArrayList<ViewHolder> scrap = mScrap.get(viewType);

      if (scrap == null) {

        scrap = new ArrayList<>();

        mScrap.put(viewType, scrap);

          if (mMaxScrap.indexOfKey(viewType) < 0) {

            mMaxScrap.put(viewType, DEFAULT_MAX_SCRAP);

          }

      }

    return scrap;

  }

}


从名字看来,他是一个缓存池,实现上,是通过一个默认大小为5的ArrayList实现的,这一点,同ListView的RecyclerBin这个类一样,很奇怪为什么不用LinkedList来做,按理说这种不需要索引读取的缓存池,用链表最合适的.

LinkedList 使用了双向链表,在内存占用上时ArrayList的两倍。虽然,该缓存池的删除添加操作比较频繁,而LinkedList在这方便比较有优势,但是ArrayList的容量比较小,性能上消耗也不是太大。总的来说,这是一种比较折中的设计方案。

然后每一个ArrayList又都是放在一个Map里面,SparseArray这个类我们在讲性能优化的时候已经多次提到了,就是用两个数组,用来代替HashMap

把所有的ArrayList放在一个Map里面,这也是RecyclerView最大的亮点,这样根据itemType来取不同的缓存Holder,每一个Holder都有对应的缓存,而只需要为这些不同RecyclerView设置同一个Pool就可以了。

为什么RecyclerView比ListView好

在适配器中通过onBindViewHolder和onCreateViewHolder两个方法屏蔽了一些固定的逻辑,使得适配器使用更简单,又通过了LayoutManager实现具体的布局,使得RecyclerView具有更强大的定制能力,所以RecyclerView比ListView好。

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