Android源码分析之RecyclerView源码分析(一)——绘制流程

系列文章

  1. Android源码分析之ListView源码
  2. Android源码分析之RecyclerView源码分析(一)——绘制流程
  3. Android源码分析之RecyclerView源码分析(二)——缓存机制

前言

RecyclerView是Google在2014年的IO大会中提出来的,可以认为是用来代替ListView的,其是support-v7包中的组件,但随着AndroidX(谷歌对 android.support.xxx 包的整理产物,因为之前support包的管理较为混乱,所以谷歌推出了AndroidX)的出现,我们需要将项目中对其的使用迁移到 AndroidX 上。RecyclerView的继承关系如下图所示:

RecyclerView继承关系.png

相比ListView的两级缓存,RecyclerView做到了四级缓存,而且整体上的架构做到了解耦,每个模块分别负责不同的功能实现。其中Adapter负责提供数据,包括创建ViewHolder和绑定数据,LayoutManager负责ItemView的测量和布局,ItemAnimator负责每个ItemView的动画,这样功能性的解耦让RecyclerView使用十分方便,也方便了开发者扩展。这里需要提一下ViewHolder对于Adapter来说,一个ViewHolder就对应一个data

有关RecyclerView的使用这里也不做过多分析,具体可以参考Android RecyclerView 使用完全解析

绘制过程

类似ListViewRecyclerView本质上还是一个View,因此在绘制的过程中还是分为以下三步:onMeasure、onLayout、onDraw,下面先看下其onMeasure方法:

绘制第一步:onMeasure方法

protected void onMeasure(int widthSpec, int heightSpec) {
    if (mLayout == null) {
        // 情况1
    }
    
    if (mLayout.mAutoMeasure) {
        // 情况2
    } else {
        // 情况3
    }

onMeasure方法中主要是分了三种情况情况1是mLayout(LayoutManager对象)为nullLayoutManager负责控制 RecyclerView 中 item 的测量和布局,当LayoutManager为空时,RecyclerView是不能显示任何数据的(原因后续解释)

另外两种情况是mLayout对象不为空第二种情况是LayoutManager开启了自动测量,第三种情况是LayoutManager没有开启自动测量。

onMeasure情况1:LayoutManager为null

if (mLayout == null) {
    defaultOnMeasure(widthSpec, heightSpec);
    return;
}

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

这种情况下直接调用了defaultOnMeasure方法,该方法中通过LayoutManager.choose()方法来计算宽高值,然后调用setMeasuredDimension()设置宽高

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

chooseSize方法就是通过RecyclerView的不同测量模式来选取不同的值。

onMeasure情况2: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;
    // 调用LayoutManager.onMeasure方法测量
    mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
    // 如果可以跳过测量或者adapter为null,则直接返回
    if (skipMeasure || mAdapter == null) {
        return;
    }
    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
    }
    
    mLayout.setMeasureSpecs(widthSpec, heightSpec);
    mState.mIsMeasuring = true;
    dispatchLayoutStep2();

    // now we can get the width and height from the children.
    mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);

    // if RecyclerView has non-exact width and height and if there is at least one child
    // which also has non-exact width & height, we have to re-measure.
    if (mLayout.shouldMeasureTwice()) {
        mLayout.setMeasureSpecs(
                MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
        mState.mIsMeasuring = true;
        dispatchLayoutStep2();
        // now we can get the width and height from the children.
        mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
    }
}

这种情况下,首先调用LayoutManager.onMeasure方法测量,但是Android官方的三种LayoutManager(LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager)都没有复写此方法,此方法源码:

public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
    mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
}

可以看到LayoutManager.onMeasure方法默认是调用的RecyclerView的defaultOnMeasure方法,前面已经介绍过此方法了。

接下来判断mState.mLayoutStep这个变量,即当前绘制状态,如果为State.STEP_START,那么便会执行dispatchLayoutStep1方法,随后又调用了dispatchLayoutStep2方法,最后如果需要二次测量的话,那么会再调用一次dispatchLayoutStep2方法。

ViewGroup的onMeasure方法的作用通常来说有两个:一是测量自身的宽高从RecyclerView来看,它将自己的测量工作托管给了LayoutManager的onMeasure方法。所以,我们在自定义LayoutManager时,需要注意onMeasure方法的存在,不过官方提供的几个LayoutManager,都没有重写这个方法二是测量子View的宽高,不过到目前为止我们还没有看到具体的实现。

摘自RecyclerView 源码分析(一) - RecyclerView的三大流程

前面提到了mState.mLayoutStep这个变量,mStateRecyclerView中State类的对象,mLayoutStep有三个状态,其三个状态正好和三个dispatchLayoutStep方法(还有一个dispatchLayoutStep3)一一对应

State.mLayoutStep dispatchLayoutStep 含义说明
STEP_START dispatchLayoutStep1 STEP_STARTState.mLayoutStep的默认值,执行完dispatchLayoutStep1后会将该状态置为STEP_LAYOUT
STEP_LAYOUT dispatchLayoutStep2 表明处于layout阶段,调用dispatchLayoutStep2RecyclerView的子view进行layout,执行完之后会将该状态置为STEP_ANIMATIONS
STEP_ANIMATIONS dispatchLayoutStep3 表明处于执行动画阶段,调用dispatchLayoutStep3之后会将该状态置再次变为STEP_START

dispatchLayoutStep1方法

RecyclerView处于State.STEP_START状态时,会调用dispatchLayoutStep1方法,其源码如下:

private void dispatchLayoutStep1() {
    
    ...
    
    processAdapterUpdatesAndSetAnimationFlags();
   
    ...

    if (mState.mRunSimpleAnimations) {
        // Step 0:找出所有未移除的ItemView,进行预布局
    }
    if (mState.mRunPredictiveAnimations) {
        // Step 1:预布局
    } else {
        clearOldPositions();
    }
    onExitLayoutOrScroll();
    resumeRequestLayout(false);
    mState.mLayoutStep = State.STEP_LAYOUT;
}

可以看到dispatchLayoutStep1方法主要是根据mState.mRunSimpleAnimationsmState.mRunPredictiveAnimations两个值做出相应逻辑处理,而processAdapterUpdatesAndSetAnimationFlags()方法中计算了这两个值:

private void processAdapterUpdatesAndSetAnimationFlags() {

    ...
    
    mState.mRunSimpleAnimations = mFirstLayoutComplete && mItemAnimator != null &&
            (mDataSetHasChangedAfterLayout || animationTypeSupported ||
                    mLayout.mRequestedSimpleAnimations) &&
            (!mDataSetHasChangedAfterLayout || mAdapter.hasStableIds());
    mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations &&
            animationTypeSupported && !mDataSetHasChangedAfterLayout &&
            predictiveItemAnimationsEnabled();
}

此方法中我们注意下mFirstLayoutComplete变量,mRunSimpleAnimations和mFirstLayoutComplete有关,而mRunPredictiveAnimations 又和mRunSimpleAnimations有关,第一次绘制流程还未完成,mFirstLayoutComplete 为false,因此mRunSimpleAnimations 和mRunPredictiveAnimations都为false,所以不会加载动画,这也是很明显的道理,布局还未加载完成,怎么会进行加载动画呢。

dispatchLayoutStep1方法的最后一句是mState.mLayoutStep = State.STEP_LAYOUT,可见执行完之后其改变了当前绘制状态。
dispatchLayoutStep1的其余具体逻辑和ItemAnimator有关,后续分析ItemAnimator再详细说明。

dispatchLayoutStep2方法

onMeasure方法执行完dispatchLayoutStep1后,接着执行dispatchLayoutStep2方法,其中对RecyclerView的子View进行了layout:

private void dispatchLayoutStep2() {

    ...
    
    mState.mItemCount = mAdapter.getItemCount();
    mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

    // Step 2: Run layout
    mState.mInPreLayout = false;
    mLayout.onLayoutChildren(mRecycler, mState);

    ...
    
    mState.mLayoutStep = State.STEP_ANIMATIONS;
    
    ...
}

此方法我们重点关注mLayout.onLayoutChildren(mRecycler, mState)这句话,onLayoutChildren 这个函数由 LayoutManager的子类实现,主要是决定了子View的布局方式,具体的相应代码逻辑可以查看Android官方LayoutManager之一LinearLayoutManager,后续我们再详细说明,此方法最后又将mState.mLayoutStep置为State.STEP_ANIMATIONS

onMeasure情况3:LayoutManager没有开启自动测量

最后再来看看LayoutManager没有开启自动测量的情况:

if (mHasFixedSize) {
    mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
    return;
}
// custom onMeasure
if (mAdapterUpdateDuringMeasure) {
    eatRequestLayout();
    processAdapterUpdatesAndSetAnimationFlags();

    if (mState.mRunPredictiveAnimations) {
        mState.mInPreLayout = true;
    } else {
        // consume remaining updates to provide a consistent state with the layout pass.
        mAdapterHelper.consumeUpdatesInOnePass();
        mState.mInPreLayout = false;
    }
    mAdapterUpdateDuringMeasure = false;
    resumeRequestLayout(false);
}

if (mAdapter != null) {
    mState.mItemCount = mAdapter.getItemCount();
} else {
    mState.mItemCount = 0;
}
eatRequestLayout();
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
resumeRequestLayout(false);
mState.mInPreLayout = false; // clear

如果mHasFixedSize为truesetHasFixedSize方法可以设置此变量),就直接调用LayoutManager.onMeasure方法进行测量;如果mHasFixedSize为false,则先判断是否有数据更新(mAdapterUpdateDuringMeasure变量)有的话先处理数据更新,再调用LayoutManager.onMeasure方法进行测量

绘制第二步:onLayout方法

测量过程之后便是layout过程,onLayout方法比较简单,只有下面几行,最重要的逻辑都在dispatchLayout方法中了:

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
    dispatchLayout();
    TraceCompat.endSection();
    mFirstLayoutComplete = true;
}

dispatchLayout方法

void dispatchLayout() {
    if (mAdapter == null) {
        Log.e(TAG, "No adapter attached; skipping layout");
        // leave the state in START
        return;
    }
    if (mLayout == null) {
        Log.e(TAG, "No layout manager attached; skipping layout");
        // leave the state in START
        return;
    }
    mState.mIsMeasuring = false;
    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() ||
            mLayout.getHeight() != getHeight()) {
        // First 2 steps are done in onMeasure but looks like we have to run again due to
        // changed size.
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else {
        // always make sure we sync them (to ensure mode is exact)
        mLayout.setExactMeasureSpecsFrom(this);
    }
    dispatchLayoutStep3();
}

dispatchLayout()方法也简洁明了,首先分别判断了mAdapter和mLayout,只要其中一个为null,则直接返回;这里需要注意到当mLayout为null时,即RecyclerView没有设置LayoutManager时,dispatchLayout方法直接返回了,因此不会处理layout过程,自然也解释了为什么不设置LayoutManager,RecyclerView就不会加载数据

dispatchLayout()方法中保证了RecyclerView必须经历三个过程,分别是dispatchLayoutStep1、dispatchLayoutStep2、dispatchLayoutStep3,前两个方法我们在onMeasure过程中已经提到了,下面看下dispatchLayoutStep3源码:

private void dispatchLayoutStep3() {
    
    ...
    
    mState.mLayoutStep = State.STEP_START;
  
    ...
   
}

它重新将mState.mLayoutStep的状态置为State.STEP_START,保证了第二次layout时仍会执行dispatchLayoutStep1、dispatchLayoutStep2、dispatchLayoutStep3三个方法,剩下的工作主要是和Item动画相关的,和ItemAnimator有关。

如果在RecyclerView中如果开启了自动测量,在measure阶段就已经将子View布局完成了,如果没有开启自动测量,那么会在layout阶段再布局子View。

绘制第三步:draw方法

public void draw(Canvas c) {
    super.draw(c);

    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDrawOver(c, this, mState);
    }
    // TODO If padding is not 0 and clipChildrenToPadding is false, to draw glows properly, we
    // need find children closest to edges. Not sure if it is worth the effort.
    ......
}

draw大概可以分为三步

  1. 调用super.draw,将子View的绘制分发给View类;在View类draw方法中又会回调RecyclerView的onDraw方法:
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);
    }
}

此方法中又将分割线的绘制分发给ItemDecoration的onDraw方法

  1. 调用ItemDecoration的onDrawOver方法。通过这个方法,我们在每个Item上自定义一些装饰
  2. 如果RecyclerView调用了setClipToPadding,会实现一种特殊的滑动效果--每个ItemView可以滑动到padding区域。

绘制流程补充举例:LinearLayoutManager.onLayoutChildren方法

在讲解ListView的绘制过程中,我们的重心就是layoutChildren方法,讲解了怎么对子View布局,到现在为止我们还没有进入RecyclerView对子View布局的讲解,前面描述dispatchLayoutStep2过程中,我们提到了onLayoutChildren 这个函数由LayoutManager的子类实现,那么下面我们就以LayoutManager的子类LinearLayoutManager为例,介绍下onLayoutChildren的工作过程:

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    // layout algorithm:
    // 1) by checking children and other variables, find an anchor coordinate and an anchor
    //  item position.
    // 2) fill towards start, stacking from bottom
    // 3) fill towards end, stacking from top
    // 4) scroll to fulfill requirements like stack from bottom.
    // create layout state
    
    ...
    
    // 第一步
    resolveShouldLayoutReverse();
    
    if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION ||
            mPendingSavedState != null) {
        mAnchorInfo.reset();
        mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
        // 计算锚点位置和坐标
        updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
        mAnchorInfo.mValid = true;
    }
    
    ...
    
    // 第二步
    onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
    detachAndScrapAttachedViews(recycler);
    mLayoutState.mInfinite = resolveIsInfinite();
    mLayoutState.mIsPreLayout = state.isPreLayout();
    
    // 第三步
    if (mAnchorInfo.mLayoutFromEnd) {
        // fill towards start
        updateLayoutStateToFillStart(mAnchorInfo);
        mLayoutState.mExtra = extraForStart;
        fill(recycler, mLayoutState, state, false);
        
        ...
        
        // fill towards end
        updateLayoutStateToFillEnd(mAnchorInfo);
        mLayoutState.mExtra = extraForEnd;
        mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
        fill(recycler, mLayoutState, state, false);
        
        ...
        
    } else {
        // fill towards end
        updateLayoutStateToFillEnd(mAnchorInfo);
        mLayoutState.mExtra = extraForEnd;
        fill(recycler, mLayoutState, state, false);
        
        ...
        
        // fill towards start
        updateLayoutStateToFillStart(mAnchorInfo);
        mLayoutState.mExtra = extraForStart;
        mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
        fill(recycler, mLayoutState, state, false);
        
        ...
        
    }

    ...
    
}

子方法的开头注释说了整个大概的流程:

  • 1、确定锚点信息,找到一个锚点坐标与锚点(如果是在线性布局中,相当于找到当前界面内第一个View,与第一个view的坐标点
  • 2、根据锚点信息,进行填充
  • 3、填充完后,如果还有剩余的可填充大小,再填充一次

第一步:确定锚点信息

onLayoutChildren方法中大概做了三件事,第一步是确定锚点信息,首先执行resolveShouldLayoutReverse()方法判断是否需要倒着绘制

private void resolveShouldLayoutReverse() {
    if (mOrientation == VERTICAL || !isLayoutRTL()) {
        mShouldReverseLayout = mReverseLayout;
    } else {
        mShouldReverseLayout = !mReverseLayout;
    }
}

默认情况下mReverseLayout为false,是不会倒着绘制的。手动调用setReverseLayout()方法,可以改变mReverseLayout的值:

public void setReverseLayout(boolean reverseLayout) {
    assertNotInLayoutOrScroll(null);
    if (reverseLayout == mReverseLayout) {
        return;
    }
    mReverseLayout = reverseLayout;
    requestLayout();
}

接下来便是通过updateAnchorInfoForLayout()方法来计算锚点信息,这里对锚点做一些解释,mAnchorInfo(AnchorInfo类对象)就是我们要的锚点:

AnchorInfo(锚点)

class AnchorInfo {
    int mPosition;
    int mCoordinate;
    boolean mLayoutFromEnd;
    boolean mValid;
    
    ...
    
}

AnchorInfo中有四个重要的成员变量,mPositionm和mCoordinate易懂;mValid的默认值是false,一次测量之后设为true,onLayout完成后会回调执行reset方法,又变为falsemLayoutFromEnd变量在计算锚点过程中有如下赋值:

mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd

mShouldReverseLayout默认是false,mStackFromEnd默认是false,除非手动调用setStackFromEnd()方法,两个变量都是false,异或运算之后还是false

摘自RecyclerView源码解析(一)——绘制流程

接下来又调用了updateAnchorInfoForLayout()方法,此方法用来更新锚点信息,一共有三种计算方法

private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
        AnchorInfo anchorInfo) {
    // 第一种计算方法
    if (updateAnchorFromPendingData(state, anchorInfo)) {
        return;
    }
    
    // 第二种计算方法
    if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
        return;
    }
    
    // 第三种计算方法
    anchorInfo.assignCoordinateFromPadding();
    anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}
  1. 第一种计算方法如果存在未决的滚动位置或保存的状态,从该数据更新锚点信息并返回true
  2. 第二种计算方法根据子View来更新锚点信息,如果一个子View有焦点,则根据其来计算锚点信息;如有一个子View没有锚点,则根据布局方向选取第一个View或最后一个View
  3. 第三种计算方法前两种都未采用,则采取默认的第三种计算方式

第二步:回收子View

第二步是调用detachAndScrapAttachedViews()方法对所有的ItemView进行回收,这部分的内容属于RecyclerView缓存机制的部分,后面解释缓存的时候再说。

第三步:子View填充(fill)

第三步是便是子View的内容填充了首先是根据mAnchorInfo.mLayoutFromEnd来判断是否逆向填充,无论是正向还是逆向,都调用了至少两次fill()方法来进行填充如果是正向填充的话先向下填充,再向上填充;逆向的话和正向相反,两次fill之后,如果还有剩余空间还会再调用一次fill进行补充。我们来结合一张图了解下锚点和fill之间的关系

fill

摘自RecyclerView全面的源码解析

下面我们来看看fill方法:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    
    ...
    
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        ...
        
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        
        ...
        
    }
    
    ...
    
}

fill中真正填充子View的方法是layoutChunk(),再来看看layoutChunk()方法,源码如下:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    // 第一步
    View view = layoutState.next(recycler);
    
    ...
    
    // 第二步
    if (layoutState.mScrapList == null) {
        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);
    ...
    
    // 第四步
    layoutDecoratedWithMargins(view, left, top, right, bottom);

    ...
}

layoutChunk方法中的主要工作可以分为以下几步:

  • 1.调用LayoutState的next方法获得一个ItemViewnext方法的参数是Recycler对象,Recycler正是RecyclerView的缓存核心实现,可见RecyclerView中的缓存机制从此处开始,后续分析缓存时再来具体查看
  • 2.如果RecyclerView是第一次布局children(layoutState.mScrapList == null),会调用addView()方法将View添加到RecyclerView中
  • 3.调用measureChildWithMargins方法,测量每个ItemView的宽高,这里考虑了margin属性和ItemDecoration的offset
  • 4.调用了layoutDecoratedWithMargins对子View完成了布局

其中有很多细节并未展开描述,后续分析缓存机制时再进行讲解。

总结

  1. RecyclerView的measure过程分为三种情况,每种情况都有执行过程,一般情况下会走自动测量流程
  2. 自动测量根据mState.mLayoutStep状态值,调用不同的dispatchLayoutStep方法
  3. layout过程也根据mState.mLayoutStep状态来调用不同的dispatchLayoutStep方法
  4. draw过程大概可以分为三步
  5. 布局子View先获取锚点信息,再根据锚点信息和布局方向进行子View填充

摘自RecyclerView全面的源码解析

至此基本完成了RecyclerView三大绘制流程,还有最重要的缓存机制没有讲解,见后续。

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