RecyclerView源码解析

首先是RecyclerView的一般用法

recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager = new LinearLayoutManager(this); 

//设置布局管理器 
recyclerView.setLayoutManager(layoutManager); 

//设置为垂直布局,这也是默认的 
layoutManager.setOrientation(OrientationHelper. VERTICAL); 

//设置Adapter 
recyclerView.setAdapter( recycleAdapter); 

//设置分隔线 
recyclerView.addItemDecoration( new DividerGridItemDecoration(this )); 

//设置增加或删除条目的动画 
recyclerView.setItemAnimator( new DefaultItemAnimator());
  1. constructor 构造函数根据设置属性进行了初始化工作,如果设置了 layoutManager 属性,也会通过反射实例化 layoutManager。
  2. setLayoutManager 会先清空之前缓存的View,通过 requestLayout 通知执行测量与绘制 onMeasure、onLayout、onDraw。
    RecyclerView 会将测量与布局交给 LayoutManager 来做,并且 LayoutManager 有一个 mAutoMeasure 的属性来控制自动测量,否则 LayoutManager 就要重写 onMeasure 来处理测量工作
    public void setLayoutManager(LayoutManager layout) {
        if (layout == mLayout) {
            return;
        }
        stopScroll();
        // TODO We should do this switch a dispatchLayout pass and animate children. There is a good
        // chance that LayoutManagers will re-use views.
        if (mLayout != null) {
            // end all running animations
            if (mItemAnimator != null) {
                mItemAnimator.endAnimations();
            }
            mLayout.removeAndRecycleAllViews(mRecycler);
            mLayout.removeAndRecycleScrapInt(mRecycler);
            mRecycler.clear();

            if (mIsAttached) {
                mLayout.dispatchDetachedFromWindow(this, mRecycler);
            }
            mLayout.setRecyclerView(null);
            mLayout = null;
        } else {
            mRecycler.clear();
        }
        // this is just a defensive measure for faulty item animators.
        mChildHelper.removeAllViewsUnfiltered();
        mLayout = layout;
        if (layout != null) {
            if (layout.mRecyclerView != null) {
                throw new IllegalArgumentException("LayoutManager " + layout
                        + " is already attached to a RecyclerView: " + layout.mRecyclerView);
            }
            mLayout.setRecyclerView(this);
            if (mIsAttached) {
                mLayout.dispatchAttachedToWindow(this);
            }
        }
        mRecycler.updateViewCacheSize();
        requestLayout();
    }

  1. onMeasure 如果宽高都为 EXACTLY是,可以直接设置对应的宽高,然后return,也就是 skipMeasure。
    @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        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);
            if (skipMeasure || mAdapter == null) {
                return;
            }
            if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
            }
            // set dimensions in 2nd step. Pre-layout should happen with old dimensions for
            // consistency
            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);
            }
        } else {
            if (mHasFixedSize) {
                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
                return;
            }
            ...
       }
}
  1. 如果不是 EXACTLY, onMeasure 就会根据 MeasureSpec 处理布局。dispatchLayoutStep1、dispatchLayoutStep2、dispatchLayoutStep3来布局,其中布局状态保存在 RecyclerView.State
    RecyclerView.State
    这个类封装了当前RecyclerView的有用信息。
public static class State {
       @IntDef(flag = true, value = {
                STEP_START, STEP_LAYOUT, STEP_ANIMATIONS
        })
        @Retention(RetentionPolicy.SOURCE)
        @interface LayoutState {}

        @LayoutState
        int mLayoutStep = STEP_START; // RecyclerView 的布局状态
}

step1负责记录状态,step2负责布局,step3则与step1进行比较,根据变化来触发动画。
RecyclerView是支持 WRAP_CONTENT 属性的,比如我们可以很容易的在 RecyclerView 的下面放置其它的View,RecyclerView 会根据子 View 所占大小动态调整自己的大小,这时候,RecyclerView 就会将子控件的 measure 与 layout 提前到 Recycler 的 onMeasure 中,因为它需要确定子空间的大小与位置后,再来设置自己的大小。所以这时候就会在 onMeasure 中完成 step1 与 step2。否则,就需要在 onLayout 中去完成整个布局过程

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

这里我们看到如果在onMeasure中已经完成了step1与step2,则只会执行step3,否则三步会依次触发。
现在我们来分析一下这三步

  • step1
    它的目的就是记录View的状态,首先遍历当前所有的View依次进行处理,mItemAnimator会根据每个View的信息封装成一个ItemHolderInfo,这个ItemHolderInfo中主要包含的就是当前View的位置状态等。然后ItemHolderInfo 就被存入mViewInfoStore中
private void dispatchLayoutStep1(){   
    ...
    if (mState.mRunSimpleAnimations) {
          int count = mChildHelper.getChildCount();
          for (int i = 0; i < count; ++i) {
              final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
              final ItemHolderInfo animationInfo = mItemAnimator
                      .recordPreLayoutInformation(mState, holder,
                              ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                              holder.getUnmodifiedPayloads());
              mViewInfoStore.addToPreLayout(holder, animationInfo);
              ...
          }
    }
    ...
    mState.mLayoutStep = State.STEP_LAYOUT;
}

数据被存储到 ViewInfoStore 中的,可以查看ArrayMap介绍

class ViewInfoStore {
    @VisibleForTesting
    final ArrayMap<ViewHolder, InfoRecord> mLayoutHolderMap = new ArrayMap<>();

    @VisibleForTesting
    final LongSparseArray<ViewHolder> mOldChangedHolders = new LongSparseArray<>();
    // ArrayMap和SparseArray一样,也会对key使用二分法进行从小到大排序,
    // 在添加、删除、查找数据的时候都是先使用二分查找法得到相应的index,
    // 然后通过index来进行添加、查找、删除等操作,
    // ArrayMap 的 key 可以是任意值, SparseArray 的 key 只能为int,
}

其中 addToPreLayout 的功能就是利用 ArrayMap 的二分查找先快速根据holder来查询InfoRecord信息,如果没有,则生成,然后将info信息赋值给InfoRecord的preInfo变量。最后标记FLAG_PRE信息。

void addToPreLayout(ViewHolder holder, ItemHolderInfo info) {
    InfoRecord record = mLayoutHolderMap.get(holder);
    if (record == null) {
        record = InfoRecord.obtain();
        mLayoutHolderMap.put(holder, record);
    }
    record.preInfo = info;
    record.flags |= FLAG_PRE;
}
// 主要包含的就是当前View的位置状态等
public static class ItemHolderInfo {
    public int left;
    public int top;
    public int right;
    public int bottom;
    @AdapterChanges
    public int changeFlags;
    public ItemHolderInfo() {
    }
}
  • step2
private void dispatchLayoutStep2() {
    ...
    mLayout.onLayoutChildren(mRecycler, mState);
     ...
    mState.mLayoutStep = State.STEP_ANIMATIONS;
}

onLayoutChildren

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {    
    ...
    if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION ||
            mPendingSavedState != null) {
        // 找到 anchor 点
        updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
    }
    ...
    // 确定布局方向
    if (mAnchorInfo.mLayoutFromEnd) {
        firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL :
                LayoutState.ITEM_DIRECTION_HEAD;
    } else {
        firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :
                LayoutState.ITEM_DIRECTION_TAIL;
    }
    ...
    onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
    ...
    if (mAnchorInfo.mLayoutFromEnd) {
       ...
    } else {
        // 根据anchor一直向 End 方向布局,直至填充满anchor点前面的所有区域
        updateLayoutStateToFillEnd(mAnchorInfo);
        fill(recycler, mLayoutState, state, false);
          ...     
        // 根据anchor一直向 Start 方向布局,直至填充满anchor点后面的所有区域
        updateLayoutStateToFillStart(mAnchorInfo);
          ...
        fill(recycler, mLayoutState, state, false);
        ...
    }
    ...
}

确定 anchor 是通过 updateAnchorFromChildren,首先寻找被focus的child,找到的话以此child作为anchor,否则根据布局的方向寻找最合适的child来作为anchor,如果找到则将child的信息赋值给anchorInfo,其实anchorInfo主要记录的信息就是view的物理位置与在adapter中的位置。找到后返回true,否则返回false则交由上一步的函数做处理。
还可以通过重写onAnchorReady方法,来实现定位。
确定布局方向后就调用 fill 进行填充

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    final int start = layoutState.mAvailable;
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        // 根据当前信息对不需要的 View 进行回收
        recycleByLayoutState(recycler, layoutState);
    }
    int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        ...
    }
    return start - layoutState.mAvailable;
 }

回收 View,回收的是逃离边界的 View,recycleViewsFromStart -> recycleChildren -> removeAndRecycleViewAt

public void removeAndRecycleViewAt(int index, Recycler recycler) {
    final View view = getChildAt(index);
    removeViewAt(index);
    recycler.recycleView(view);
}

接下来是 layoutChunk


layoutChunk获取缓存
  • step3
private void dispatchLayoutStep3() {
    mState.mLayoutStep = State.STEP_START;
    if (mState.mRunSimpleAnimations) {
        for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
            ...
            final ItemHolderInfo animationInfo = mItemAnimator
                    .recordPostLayoutInformation(mState, holder);
                mViewInfoStore.addToPostLayout(holder, animationInfo);
        }
        mViewInfoStore.process(mViewInfoProcessCallback);
    }
    ...
}

这个方法在 onLayout 中执行,子View 已经布局完成,通过 recordPostLayoutInformation 和 addToPostLayout ,来记录布局之后的状态。最后调用 process 方法,这个方法就是根据mViewInfoStore中的View信息,来执行动画逻辑。
接下来就是draw

@Override
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);
    }
    ...
}
@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);
    }
}
  1. Adapter
    adapter 根据 data 返回绑定的 ViewHolder,同时在 setAdapter 时还注册了 RecyclerViewDataObserver ,可以在 notify 的时候,就会通知更新 View,在 setAdapter 还作了:
  • 如果之前存在 Adapter,先移除原来的,注销观察者,和从RecyclerView Detached。
  • 然后根据参数,决定是否清除原来的 ViewHolder
  • 然后重置 AdapterHelper,并更新 Adapter,注册观察者。
    现在来分析 RecyclerViewDataObserver
private class RecyclerViewDataObserver extends AdapterDataObserver {
    @Override
    public void onItemRangeInserted(int positionStart, int itemCount) {
        // 1) 断言不在布局或者滚动过程中
        assertNotInLayoutOrScroll(null);
        // 2) 这里小心,不要小看if括号中的内容,这是关键。我们去看看这个方法的实现。
        // 见下面注释 3),在 3) 返回true之后执行triggerUpdateProcessor方法,
        // triggerUpdateProcessor方法分析请看注释 4)。
        if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) {
            triggerUpdateProcessor();
        }
    }
    // 4) 触发更新处理操作,分为两种情况,在 版本大于16 且 已经Attach 并且 设置了大小固定 的情况下,
    // 进行mUpdateChildViewsRunnable中的操作。否则请求布局。
        void triggerUpdateProcessor() {
            if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
                RecyclerView.this.postOnAnimation(mUpdateChildViewsRunnable);
            } else {
                mAdapterUpdateDuringMeasure = true;
                requestLayout();
            }
        }
}
    // 5) 其中核心代码是consumePendingUpdateOperations()
    final Runnable mUpdateChildViewsRunnable = new Runnable() {
        @Override
        public void run() {
            ...
            consumePendingUpdateOperations();
        }
    };

private void consumePendingUpdateOperations() {
    ...
    if (mAdapterHelper.hasAnyUpdateTypes(UpdateOp.UPDATE) && !mAdapterHelper
            .hasAnyUpdateTypes(UpdateOp.ADD | UpdateOp.REMOVE | UpdateOp.MOVE)) {
        // 6) 如果只有更新类型的操作(这里指内容的更新,不影响View位置的改变)的情况下,
        // 先进行预处理,然后在没有View更新的情况下消耗延迟的更新操作,否则调用
        // dispatchLayout方法对RecyclerView中的View重新布局。那么接下来分析
        // preProcess()方法。
        mAdapterHelper.preProcess();
        if (!mLayoutRequestEaten) {
            if (hasUpdatedView()) {
                dispatchLayout();
            } else {
                mAdapterHelper.consumePostponedUpdates();
            }
        }
        resumeRequestLayout(true);
    } else if (mAdapterHelper.hasPendingUpdates()) {
        // 7) 在既有更新操作又有添加或者删除或者移动中任意一个的情况下,调用
        // dispatchLayout方法对RecyclerView中的View重新布局
        dispatchLayout();
    }
}

// 8) 预处理做了以下几件事情,<1> 先将待处理操作重排。<2> 应用所有操作 <3> 清空待处理操作列表,
// 以ADD为例分析流程。
void preProcess() {
    mOpReorderer.reorderOps(mPendingUpdates);
    final int count = mPendingUpdates.size();
    for (int i = 0; i < count; i++) {
        UpdateOp op = mPendingUpdates.get(i);
        switch (op.cmd) {
            case UpdateOp.ADD:
                applyAdd(op);
                break;
            case UpdateOp.REMOVE:
                applyRemove(op);
                break;
            case UpdateOp.UPDATE:
                applyUpdate(op);
                break;
            case UpdateOp.MOVE:
                applyMove(op);
                break;
        }
        if (mOnItemProcessedCallback != null) {
            mOnItemProcessedCallback.run();
        }
    }
    mPendingUpdates.clear();
}
// 9) 直接看 postponeAndUpdateViewHolders
private void applyAdd(UpdateOp op) {
    postponeAndUpdateViewHolders(op);
}
// 10) 先将操作添加到推迟的操作列表中。然后将操作的内容交给回调处理。
private void postponeAndUpdateViewHolders(UpdateOp op) {
    mPostponedList.add(op);
    switch (op.cmd) {
        case UpdateOp.ADD:
            mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
            break;
        case UpdateOp.MOVE:
            mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
            break;
        case UpdateOp.REMOVE:
            mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart,
                    op.itemCount);
            break;
        case UpdateOp.UPDATE:
            mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
            break;
        default:
            throw new IllegalArgumentException("Unknown update op type for " + op);
    }
}
// 11) 直接看offsetPositionRecordsForInsert
@Override
public void offsetPositionsForAdd(int positionStart, int itemCount) {
    offsetPositionRecordsForInsert(positionStart, itemCount);
    mItemsAddedOrRemoved = true;
}
// 12) 该方法主要是遍历所有的ViewHolder,然后把在插入位置之后的ViewHolder的位置
// 向后移动插入的个数,最后在对Recycler中缓存的ViewHolder做同样的操作,最后申请重新布局。
void offsetPositionRecordsForInsert(int positionStart, int itemCount) {
    final int childCount = mChildHelper.getUnfilteredChildCount();
    for (int i = 0; i < childCount; i++) {
        final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
        if (holder != null && !holder.shouldIgnore() && holder.mPosition >= positionStart) {
            holder.offsetPosition(itemCount, false);
            mState.mStructureChanged = true;
        }
    }
    mRecycler.offsetPositionRecordsForInsert(positionStart, itemCount);
    requestLayout();
}
class AdapterHelper implements OpReorderer.Callback {
    // 3) 该方法将插入操作的信息存储到一个UpdateOp中,并添加到待处理更新操作列表 mPendingUpdates 中,
    // 如果操作列表中的值是1,就返回真表示需要处理操作,等于1的判断避免重复触发处理操作。
    // obtainUpdateOp内部是通过池来得到一个UpdateOp对象。那么下面回去看我们注释 4)。
    boolean onItemRangeInserted(int positionStart, int itemCount) {
        if (itemCount < 1) {
            return false;
        }
        mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount, null));
        mExistingUpdateTypes |= UpdateOp.ADD;
        return mPendingUpdates.size() == 1;
    }
}

注意,在上面 4) 步的时候有个优化 POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached ,就是可以通过 setHasFixedSize(true),来减少一定程度的计算,绕过requestlayout,只走自身的布局流程,提高性能。

  1. 滑动
    RecyclerView的滑动过程可以分为2个阶段:手指在屏幕上移动,使RecyclerView滑动的过程,可以称为scroll;手指离开屏幕,RecyclerView继续滑动一段距离的过程,可以称为fling。
    我们可以参考下图
    RecyclerView滑动过程
    1. 当RecyclerView接收到ACTION_MOVE事件后,会先计算出手指移动距离(dy),并与滑动阀值(mTouchSlop)比较,当大于此阀值时将滑动状态设置为SCROLL_STATE_DRAGGING,而后调用scrollByInternal()方法,使RecyclerView滑动,这样RecyclerView的滑动的第一阶段scroll就完成了。其中 scrollByInternal调用的是 LinearLayoutManager.scrollBy方法
    2. 当接收到ACTION_UP事件时,会根据之前的滑动距离与时间计算出一个初速度yvel,这步计算是由 VelocityTracker 实现的,然后再以此初速度,调用方法fling(),完成RecyclerView滑动的第二阶段fling。fling 方法调用的是 mViewFlinger.fling,其实借助的是 Scroller 实现的
      postOnAnimation()保证了RecyclerView的滑动是流畅,这里涉及到著名的“android 16ms”机制,简单来说理想状态下,上段代码会以16毫秒一次的速度执行,也就是每16毫秒平移一次。
      我们再看看 scrollBy 方法
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    final int absDy = Math.abs(dy);
    updateLayoutState(layoutDirection, absDy, true, state);
    final int consumed = mLayoutState.mScrollingOffset
            + fill(recycler, mLayoutState, state, false);
    ...
    final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
    // mOrientationHelper.offsetChildren()作用就是平移ItemView
    mOrientationHelper.offsetChildren(-scrolled);
    ...
}
public void fling(int velocityX, int velocityY) {
     setScrollState(SCROLL_STATE_SETTLING);
     mLastFlingX = mLastFlingY = 0;
     mScroller.fling(0, 0, velocityX, velocityY,
               Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
     postOnAnimation();
}                                   

在ViewFling 的 run 方法中,我们看到,如果没有结束,会继续滑动下去

...
 if (scroller.isFinished() || !fullyConsumedAny) {
     setScrollState(SCROLL_STATE_IDLE); // setting state to idle will stop this.
     if (ALLOW_THREAD_GAP_WORK) {
           mPrefetchRegistry.clearPrefetchPositions();
      }
} else {
     postOnAnimation();
     if (mGapWorker != null) {
           mGapWorker.postFromTraversal(RecyclerView.this, dx, dy);
      }
}
...
  1. 缓存
    根据列表位置获取ItemView,先后从scrapped、cached、exCached、recycled集合中查找相应的ItemView,如果没有找到,就创建(Adapter.createViewHolder()),最后与数据集绑定。其中scrapped、cached和exCached集合定义在RecyclerView.Recycler中,分别表示将要在RecyclerView中删除的ItemView、一级缓存ItemView和二级缓存ItemView,cached集合的大小默认为2,exCached是需要我们通过RecyclerView.ViewCacheExtension自己实现的,默认没有;recycled集合其实是一个Map,定义在RecyclerView.RecycledViewPool中,将ItemView以ItemType分类保存了下来,通过RecyclerView.RecycledViewPool可以实现在不同的RecyclerView之间共享ItemView,只要为这些不同RecyclerView设置同一个RecyclerView.RecycledViewPool就可以了。
  2. 回收
    View的回收并不像View的创建那么复杂,这里只涉及了两层缓存mCachedViews与mRecyclerPool,mCachedViews相当于一个先进先出的数据结构,每当有新的View需要缓存时都会将新的View存入mCachedViews,而mCachedViews则会移除头元素,并将头元素放入mRecyclerPool,所以mCachedViews相当于一级缓存,mRecyclerPool则相当于二级缓存,并且mRecyclerPool是可以多个RecyclerView共享的

最后与 AdapterView 比较

区别 AdapterView RecyclerView
点击事件 提供click、LongClick事件 使用 onTouchListener(复杂)
分割线 divider ItemDecoration(复杂,定制型强)
布局 listView(流式布局)和GridView(网格时) LayoutManager(简单)
缓存 RecyclerBin Recycler
局部刷新 复杂 notifyItemChanged(position,payload)
动画 复杂 简单
嵌套布局 支持头部和尾部 通过 Type实现
多选 自身优点 复杂

参考:
https://juejin.im/entry/586a12c5128fe10057037fba

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

推荐阅读更多精彩内容