Android RecyclerView的绘制流程和缓存机制源码剖析

前言

对于一个Android开发者来说,RecyclerView应该是日常开发中使用最频繁的控件之一了吧。自从谷歌在开发者大会上推出它以后,从前用来展示列表的控件ListView、GridView等就不再那么的受宠了,因为RecyclerView相比它们来说,实在是强大和好用多了。而究竟是什么原因让RecyclerView如此的受欢迎,这就需要我们走进它的源码,来了解一下它的实现原理(本文属于RecyclerView进阶学习,不会介绍其基本的使用方式,因此需要对RecyclerView的使用方式有着基本的了解)。文章将按照以下几个章节进行分析:

1.RecyclerView的定义。
2.从源码分析RecyclerView的绘制流程。
3.从源码理解RecyclerView的缓存机制。
4.LinearLayoutManager源码分析。

走进源码

1.定义:

既然RecyclerView是一个控件,那么它要么继承自View,要么继承自ViewGroup。源码中关于它的定义如下:

一种灵活的视图,提供一个有限的窗口展示大型的数据集合。

说到集合,说明它肯定不单单只可以展示一条数据,因此我们可以推测RecyclerView是继承自ViewGroup的。现实也是如此,RecyclerView的确是继承自ViewGroup的,那么首先我们就要对它的绘制流程有一个基本的了解,看看它究竟是如何将每一个条目itemView绘制到屏幕上的(这里要求对View的绘制流程有一定的了解,可以参考我前面的文章 Android 自定义View--从源码理解View的绘制流程)。

2.绘制流程:

2.1.onMeasure:

首先看一下RecyclerView的measure流程,这里贴出它的onMeasure方法的源码(请留意源码中的注释):

protected void onMeasure(int widthSpec, int heightSpec) {
1    if (mLayout == null) {
2        defaultOnMeasure(widthSpec, heightSpec);
3        return;
4    }
5    if (mLayout.isAutoMeasureEnabled()) {
6        final int widthMode = MeasureSpec.getMode(widthSpec);
7        final int heightMode = MeasureSpec.getMode(heightSpec);
         // LayoutManager中的onMeasure方法内部最终也是调用刚刚的defaultOnMeasure方法;
         // 之所以没有直接调用defaultOnMeasure方法是因为可能会破坏现有的一些三方代码;
8        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
9        final boolean measureSpecModeIsExactly =
10                widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
11       if (measureSpecModeIsExactly || mAdapter == null) {
12           return;
13       }
         // mState是一个State类型的成员变量,它的mLayoutStep变量默认值为State.STEP_START
         // State类是RecyclerView的一个内部类,它保存一些关于RecyclerView的有用信息
14       if (mState.mLayoutStep == State.STEP_START) {
15           dispatchLayoutStep1(); // 布局流程第一步
16       }
17       mLayout.setMeasureSpecs(widthSpec, heightSpec);
18       mState.mIsMeasuring = true;
19       dispatchLayoutStep2(); // 布局流程第二步
         // 通过子View来获取RecyclerView的宽度和高度
20       mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
         // 如果RecyclerView有不确切的宽度和高度并且至少有一个子View也有不确切的宽度和高度,我们必须重新测量。
21       if (mLayout.shouldMeasureTwice()) {
22           mLayout.setMeasureSpecs(
23                   MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
24                   MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
25           mState.mIsMeasuring = true;
26           dispatchLayoutStep2();
             // now we can get the width and height from the children.
27           mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
28       }
29   } else {
30      ........
     }
}

在这个方法中,首先我们要知道的是mLayout这个变量的含义,它就是我们设置给RecyclerView的LayoutManager。而通常情况下我们使用的都是LinearLayoutManager或GridLayoutManager(继承自LinearLayoutManager),在LinearLayoutManager中,isAutoMeasureEnabled方法的返回值为true,因此这里略去了else(代码第30行)中的逻辑,只分析一般场景下的measure流程。
首先,在第一行判断mLayout是否为空,如果为空,就执行defaultOnMeasure方法,然后调用return结束onMeasure方法。而在defaultOnMeasure方法中,其实就是调用LayoutManager中的chooseSize方法,根据当前RecyclerView宽度和高度的测量模式来分别获取宽和高的尺寸值,然后调用View中的setMeasuredDimension方法将测量出的宽和高的尺寸值保存。其方法的源码如下:

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

当mLayout不为空时进入if条件语句,在第8行调用LayoutManager的onMeasure方法保存测量出的宽和高的尺寸值;然后在第11行进行判断,如果宽和高的测量模式都为EXACTLY模式或者Adapter为空,调用return结束onMeasure方法;如果不满足继续向下执行到第14行,如果mState.mLayoutStep的值为State.STEP_START,就执行dispatchLayoutStep1方法,关于dispatchLayoutStep1方法,它的方法源码有点长并且逻辑很复杂,这里只贴出源码中针对此方法的注释:

该方法是布局流程的第一步,首先进行适配器的更新,决定应该执行哪个动画,然后保存当前视图的信息,如果有必要的话执行先前的布局操作并且保存它的信息。

根据源码注释我们可以得知,其实在dispatchLayoutStep1方法中的主要操作就是更新适配器中的内容确保即将绘制到屏幕上的视图信息的准确性,并且保存当前视图的信息,在方法的最后一步mState.mLayoutStep的值将被置为State.STEP_LAYOUT;接下来在第17行会调用LayoutManager的setMeasureSpecs方法将宽和高的测量模式和测量尺寸在LayoutManager中保存一份;然后再向下执行至19行,调用dispatchLayoutStep2方法,这个方法是布局流程的第二步,这里我们依然只贴出源码中关于该方法的注释:

在第二个布局步骤中,我们对最终状态的视图进行实际布局;如果需要,这个步骤可以运行多次。

在这个方法中,会对RecyclerView进行实际的布局操作,而在前面分析View的绘制流程的文章中我们知道,布局流程的实质就是ViewGroup类型的父布局来确定它的每一个子View在布局中的位置。而在dispatchLayoutStep2方法的内部,会调用LayoutManager的onLayoutChildren方法来进行RecyclerView的实际布局操作,也就是说RecyclerView的布局流程是由LayoutManager完成的。首先我们看下LayoutManager中的onLayoutChildren方法的源码,如下:

public void onLayoutChildren(Recycler recycler, State state) {
    Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
}

方法内仅打印一条Log日志,日志内容为你必须重写onLayoutChildren方法。因此,在使用自定义LayoutManager时,记得要重写它的onLayoutChildren方法,并在方法内部编写真正的布局逻辑。此时不知道你是否会有个疑问,貌似还没有对RecyclerView中的每一个条目itemView进行measure流程,怎么直接就进行layout流程了呢!这里我们拿LinearLayoutManager的onLayoutChildren方法为例,其实在方法的内部,会依次调用到LayoutManager中的measureChildWithMargins和layoutDecoratedWithMargins方法,两个方法的内部又分别会调用到每一个子View(条目itemView)的measure和layout方法。因此,其实在LinearLayoutManager的onLayoutChildren方法中不仅完成了对RecyclerView的layout流程,还完成了对RecyclerView的每一个条目的measure流程(后面会详细分析LinearLayoutManager中的onLayoutChildren方法)。最后,在dispatchLayoutStep2方法的结尾处会将mState.mLayoutStep的值置为State.STEP_ANIMATIONS;
现在,继续回到RecyclerView的onMeasure方法,在第20行调用LayoutManager的setMeasuredDimensionFromChildren方法来根据子View(条目itemView)来获取RecyclerView的宽度和高度,方法的源码如下:

void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) {
    final int count = getChildCount();
    if (count == 0) {
        mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
        return;
    }
    int minX = Integer.MAX_VALUE;
    int minY = Integer.MAX_VALUE;
    int maxX = Integer.MIN_VALUE;
    int maxY = Integer.MIN_VALUE;
    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
        final Rect bounds = mRecyclerView.mTempRect;
        getDecoratedBoundsWithMargins(child, bounds);
        if (bounds.left < minX) {
            minX = bounds.left;
        }
        if (bounds.right > maxX) {
            maxX = bounds.right;
        }
        if (bounds.top < minY) {
            minY = bounds.top;
        }
        if (bounds.bottom > maxY) {
            maxY = bounds.bottom;
        }
    }
    mRecyclerView.mTempRect.set(minX, minY, maxX, maxY);
    setMeasuredDimension(mRecyclerView.mTempRect, widthSpec, heightSpec); // LayoutManager中的方法
}

虽然方法将近30行并不是很短,但是逻辑却是非常的简单易懂,就是遍历RecyclerView中的每一个条目,根据每一个条目的矩阵边界值(top、left、right、bottom)来不断的改变RecyclerView的矩阵边界值。然后在方法的最后一行调用LayoutManager的setMeasuredDimension方法,源码如下:

public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) {
    int usedWidth = childrenBounds.width() + getPaddingLeft() + getPaddingRight();
    int usedHeight = childrenBounds.height() + getPaddingTop() + getPaddingBottom();
    int width = chooseSize(wSpec, usedWidth, getMinimumWidth());
    int height = chooseSize(hSpec, usedHeight, getMinimumHeight());
    setMeasuredDimension(width, height);
}

方法内部根据传过来的矩阵信息和设置的Padding来计算RecyclerView的宽度和高度的尺寸值,再调用chooseSize方法(前面已经说明)根据测量模式获取最终的宽度和高度的尺寸值,最后调用
setMeasuredDimension方法(内部最终调用到View的setMeasuredDimension方法)保存测量出的宽度和高度的尺寸值。
最后回到onMeasure方法的第21行,根据LayoutManager的shouldMeasureTwice方法的返回值决定是否需要进行二次测量。我们还是看一下LinearLayoutManager中的shouldMeasureTwice方法的源码:

@Override
boolean shouldMeasureTwice() {
    return getHeightMode() != View.MeasureSpec.EXACTLY
                && getWidthMode() != View.MeasureSpec.EXACTLY
                && hasFlexibleChildInBothOrientations();
}
boolean hasFlexibleChildInBothOrientations() {
    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = getChildAt(i);
        final ViewGroup.LayoutParams lp = child.getLayoutParams();
        if (lp.width < 0 && lp.height < 0) {
            return true;
        }
    }
    return false;
}

方法的逻辑都能看懂,根据方法内部的判断逻辑,我们可以总结出一个结论,如果不想进行二次测量操作,最好将RecyclerView的宽度和高度中的至少一个的测量模式指定为EXACTLY模式。
到这里,RecyclerView的onMeasure方法就分析完了。关于这个onMeasure方法,我猜很多人会有疑问,为什么在它的内部会包含layout的流程,既然这个方法中包含了RecyclerView的layout流程,那么RecyclerView的onLayout方法是不是为一个空方法呢,带着这个疑问我们走进RecyclerView的onLayout方法。

2.2.onLayout:

首先,我们来看一下onLayout方法的源码:

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方法来进行layout操作,下面我们来看一下dispatchLayout方法的源码:

void dispatchLayout() {
1    if (mAdapter == null) {
2        Log.e(TAG, "No adapter attached; skipping layout");
3        return;
4    }
5    if (mLayout == null) {
6        Log.e(TAG, "No layout manager attached; skipping layout");
7        return;
8    }
9    mState.mIsMeasuring = false;
10   if (mState.mLayoutStep == State.STEP_START) {
11       dispatchLayoutStep1();
12       mLayout.setExactMeasureSpecsFrom(this);
13       dispatchLayoutStep2();
14   } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
15                  || mLayout.getHeight() != getHeight()) {
        // First 2 steps are done in onMeasure but looks like we have to run again due to changed size.
16       mLayout.setExactMeasureSpecsFrom(this);
17       dispatchLayoutStep2();
18   } else {
        // always make sure we sync them (to ensure mode is exact)
19       mLayout.setExactMeasureSpecsFrom(this);
20   }
21   dispatchLayoutStep3();
}

方法的开始判断Adapter和LayoutManager是否为空,如果为空,就调用return结束当前方法;否则继续向下执行。在第10行,判断mState.mLayoutStep的值是否等于State.STEP_START,如果等于就进入if条件体中,里面的dispatchLayoutStep1和dispatchLayoutStep2方法前面已经说过,这里我们看下LayoutManager的setExactMeasureSpecsFrom方法:

void setExactMeasureSpecsFrom(RecyclerView recyclerView) {
    setMeasureSpecs(MeasureSpec.makeMeasureSpec(recyclerView.getWidth(), MeasureSpec.EXACTLY),
            MeasureSpec.makeMeasureSpec(recyclerView.getHeight(), MeasureSpec.EXACTLY));
}

方法内部调用LayoutManager的setMeasureSpecs方法将RecyclerView的宽度和高度的测量模式和测量尺寸在LayoutManager中保存,而在保存之前先将宽度和高度的测量模式全部指定成EXACTLY模式,再调用MeasureSpec的makeMeasureSpec将尺寸值和模式合成一个32位的MeasureSpec值。
接着看dispatchLayout方法第14行的判断条件,根据源码中的注释我们可以理解为,布局的前两步流程已经在onMeasure方法中执行过,当发现RecyclerView的大小发生改变的时候我们要再次调用dispatchLayoutStep2方法布局子View(条目itemView),这种情况貌似不太常见。
再接着看第19行,在else语句中也调用了LayoutManager的setExactMeasureSpecsFrom方法,也就是说只要是正常的执行完了onLayout方法,RecyclerView的宽度和高度的测量模式都会变成EXACTLY模式,即使你最初在布局中设置RecyclerView的宽和高为wrap_content。其实这不难理解,因为在onLayout方法之前,我们已经通过onMeasure方法获取到了RecyclerView确切的宽和高的尺寸值了,因此这里将宽和高的测量模式都指定成EXACTLY模式也没什么不妥的了(感兴趣的可以自行验证下)。
最后来看下dispatchLayout方法的最后一行调用了dispatchLayoutStep3方法,这是layout流程的第三步也是最后一步,方法的源码有些长且逻辑复杂,因此这里也还是只贴出该方法源码中的注释内容:

布局的最后一步,保存关于视图的动画信息,触发动画并进行必要的清理。

由此可知在dispatchLayoutStep3方法中主要是做和动画相关的操作。至此,RecyclerView的layout流程也就分析完了。

2.3.measure、layout流程回顾:

在分析完measure和layout流程的逻辑之后,我们现在回过头来分析下二者之间逻辑执行的联系。前面在分析onMeasure方法的时候,我们看到在onMeasure方法中居然存在layout流程的前两步操作,而在什么情况下会在onMeasure中执行这两步布局操作呢?通过上面的分析我们知道在Adapter不为空的前提下,如果RecyclerView的宽度或者高度二者中只要有一个的测量模式不是EXACTLY模式(即被指定为wrap_content),那么就会在onMeasure中执行layout流程的前两步操作,而且一般情况下,如果两者的测量模式都不是EXACTLY模式,还有可能在onMeasure方法中进行二次测量和布局的操作;相反,如果二者的测量模式均为EXACTLY模式,那么onMeasure方法就会在执行完RecyclerView自身的measure流程后便结束掉。
再来看看onLayout方法中的dispatchLayout方法,如果mState.mLayoutStep的值为State.STEP_START,那么就会在dispatchLayout方法中执行layout流程的前两步操作,而在分析measure流程时我们提到过,在dispatchLayoutStep2方法的结尾会将mState.mLayoutStep的值置为State.STEP_ANIMATIONS。因此,如果在onMeasure方法中执行了layout流程的前两步操作(dispatchLayoutStep1和dispatchLayoutStep2),那么在dispatchLayout方法中就不会再次执行;反之,layout流程的前两步操作就会在dispatchLayout方法中进行的。这里用一张图总结如下:

mea-lay.png
2.4.onDraw:

到了RecyclerView绘制的最后一个流程-draw流程,在RecyclerView中它将draw和onDraw方法都重写了,源码如下:

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

在draw方法中略去的是绘制边界发光效果(EdgeEffect)的逻辑,这里不详细分析,我们重点看两个方法中关于mItemDecorations的操作。首先,这个mItemDecorations是一个存储ItemDecoration类型数据的集合,ItemDecoration就是我们一般情况下可能调用RecyclerView的addItemDecoration方法添加给RecyclerView每一个条目的装饰。现在,在RecyclerView的draw和onDraw方法中分别调用了它的onDrawOver和onDraw方法,难道一个ItemDecoration还要分两步绘制?我们还是先看下这两个方法的源码:

/**
 * 给RecyclerView绘制合适的装饰。使用此方法绘制的任何内容都将在绘制项目视图之后绘制,从而显示在视图上。
 */
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
    onDrawOver(c, parent);
}
@Deprecated
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent) {
}
/**
 * 给RecyclerView绘制合适的装饰。使用此方法绘制的任何内容都将在绘制项目视图之前绘制,因此将显示在视图之下。
 */
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) {
    onDraw(c, parent);
}
@Deprecated
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) {
}

两个方法的内部都各自调用自己的重载方法,并且两个重载的方法也都是空方法。这就奇怪了,明明是两个空方法,而且也不是抽象方法,这就意味着我们在使用ItemDecoration时并不是一定要重写这两个方法,那为什么还要弄出两个不一样的空方法呢?细心的人可能已经通过上面代码中的注释看出了两个方法的不同了,其实就是调用的时机不同,一个会在绘制条目视图之前被调用,一个会在绘制条目视图之后被调用;那么在这两个方法中绘制的装饰内容将分别会呈现在条目视图的下面和上面。到这里可能还是有人会有疑问,虽然源码中的注释是这么解释的,但是拿什么证明它们两个的调用时机就是这样的呢!答案当然是在RecyclerView的draw和onDraw方法中啊。这里还是要考验你对View的绘制流程的掌握度了,在View绘制的draw流程中我们知道,View的draw方法将被父布局调用,然后在View的draw方法中,会依次调用到View的onDraw和dispatchDraw方法,其中dispatchDraw方法完成每个子View的绘制(RecyclerView没有重写dispatchDraw方法,直接复用ViewGroup中的dispatchDraw方法)。
现在再来看看刚刚的RecyclerView的draw和onDraw方法,在draw方法中先是调用了super.draw方法,这就说明RecyclerView的onDraw和dispatchDraw方法会先被调用到,而在onDraw方法中调用的是ItemDecoration的onDraw方法,因此现在可以证明ItemDecoration的onDraw方法是在绘制每个条目视图之前调用的了;而在执行完super.draw方法后,才会继续向下执行draw方法中的内容,因此也就验证了ItemDecoration的onDrawOver方法是在绘制每个条目视图之后调用的了。搞懂了两个方法的调用时机,我们在之后使用自定义ItemDecoration时就可以根据自身需求来选择实现对应的方法和逻辑了。

2.5.绘制流程总结:

到这里,RecyclerView的绘制流程大致就讲完了,通过对它的绘制流程的学习,我们可以从中总结出两点关于RecyclerView使用上的注意点:

1.必须给RecyclerView设置LayoutManager,因为RecyclerView的每一个条目itemView的测量和布局操作是在LayoutManager中完成的;如果不设置,RecyclerView将无法正常显示。
2.在设置RecyclerView的宽度和高度时,最好指定为match_parent或确切的数值,这样可以避免进行多次测量操作。

3.缓存机制:

在分析过了RecyclerView的绘制流程后,我们也算对其有了一个基本的了解。接下来我们就要再深入的了解一下它的缓存机制了,因为我们一直都说RecyclerView非常强大,但到底是什么原因让它这么强大呢?其实就是它的视图复用逻辑非常的完美,本质就是它的缓存机制做的非常的强大。

3.1.ViewHolder:

在分析RecyclerView的缓存机制之前,我们还要明确一些关于RecyclerView的知识点。那就是在RecyclerView中,每一个条目itemView都会与一个ViewHolder关联。对于一个ViewHolder来说,我们可以直接通过holder.itemView获取到对应的条目itemView;而对于itemView来说,我们又可以通过获取它的LayoutParams来获取到对应的mViewHolder,二者可以说是你中有我我中有你的关系。在RecyclerView的视图复用机制中,也正是从holder中获取到复用的视图itemView,关于ViewHolder源码中的解释为:

ViewHolder用来描述一个条目itemView以及它在RecyclerView中位置信息

3.2.onCreateViewHolder:

在了解了ViewHolder和itemView的关系之后,我们来一点一点揭开RecyclerView缓存机制的面纱,首先,要想缓存一个东西那么必须要先创建出这个东西,我们就从ViewHolder的创建说起。在我们实现一个Adapter的时候,必须要重写基类中的三个抽象方法,其中有一个方法就是onCreateViewHolder,ViewHolder就是在这个方法中创建的。关于onCreateViewHolder方法,源码中的解释说到,当RecyclerView需要一个新的给定类型的条目视图的时候这个方法会被调用,那么我们就先看一下onCreateViewHolder是在哪儿被调用的:

public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
    try {
        TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
        final VH holder = onCreateViewHolder(parent, viewType);
        if (holder.itemView.getParent() != null) {
            throw new IllegalStateException("ViewHolder views must not be attached when"
                    + " created. Ensure that you are not passing 'true' to the attachToRoot"
                    + " parameter of LayoutInflater.inflate(..., boolean attachToRoot)");
        }
        holder.mItemViewType = viewType;
        return holder;
    } finally {
        TraceCompat.endSection();
    }
}

在Adapter的createViewHolder方法中,我们找到了onCreateViewHolder方法的调用,在createViewHolder方法中通过调用onCreateViewHolder方法创建一个ViewHolder对象并返回。而之所以贴出它的源码是因为方法中可能会抛出异常,而抛出异常的原因应该都能看懂,当我们填充条目视图的时候不能直接将它附加到RecyclerView中,也就是在调用LayoutInflater的inflate方法时,attachToRoot参数记得传false,这个原因后面会讲到。

3.3.tryGetViewHolderForPositionByDeadline:

现在继续寻找createViewHolder方法的调用处,在Recycler类的tryGetViewHolderForPositionByDeadline方法中我们找到了createViewHolder方法的调用,并且createViewHolder方法仅仅只有这一处被调用的地方。该方法的源码如下:

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
    ........
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position); // step1
        fromScrapOrHiddenOrCache = holder != null;
    }
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); // step2
        ........
    }
    if (holder == null) {
        ........
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); // step3
            ........
        }
        if (holder == null && mViewCacheExtension != null) {
            final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view); // step4
                ........
            }
        }
        if (holder == null) { // fallback to pool
            holder = getRecycledViewPool().getRecycledView(type); // step5
            ........
        }
        if (holder == null) {
            ........
            holder = mAdapter.createViewHolder(RecyclerView.this, type); // step6
            ........
        }
    }
    ........
    return holder;
}

这里只贴出了tryGetViewHolderForPositionByDeadline方法中的关键代码,都是和获取ViewHolder实例相关的代码。首先说一下刚刚提到的Recycler这个类,它是RecyclerView的一个内部类,这个类是负责管理RecyclerView的视图以供重复利用的,也就是说RecyclerView的缓存机制其实就在这个Recycler类中。再来看下源码中关于这个tryGetViewHolderForPositionByDeadline方法的解释:

尝试获取给定位置的ViewHolder,可以从回收器碎片、缓存以及RecycledViewPool中获取或直接创建它。

由此可知tryGetViewHolderForPositionByDeadline这个方法就是用来获取ViewHolder的,在方法的内部会根据对应的条件从不同的地方获取到给定位置上的ViewHolder实例,方法中一共有6处可以获取到ViewHolder实例的地方(step1-step6),接下来我们一个一个分析。

3.3.1.getChangedScrapViewForPosition:

在step1处,当mState.isPreLayout()为true的时候首先通过getChangedScrapViewForPosition方法获取一个ViewHolder实例。而mState.isPreLayout()方法的返回值就是State类中的mInPreLayout变量的值(State类也是RecyclerView的一个内部类,它包含一些关于RecyclerView状态的有用信息),mInPreLayout变量的默认值为false,在预布局时(前面讲RecyclerView绘制流程中的dispatchLayoutStep1方法中),当RecyclerView的条目发生了增加或者移除并且有动画的时候,才有被置为true的可能,这种情况并不常见。在getChangedScrapViewForPosition方法的内部对Recycler类中的mChangedScrap集合进行遍历,先是对比ViewHolder的位置信息,如果未找到对应的ViewHolder,再对比ViewHolder的itemId(即我们通过实现Adapter的getItemId方法为每一个item指定的id),如果两次遍历均未找到对应的ViewHolder,那么就返回null(此方法源码简单易懂,请自行查看)。

3.3.2.getScrapOrHiddenOrCachedHolderForPosition:

在step2处,如果在step1处未获取到ViewHolder,那么调用getScrapOrHiddenOrCachedHolderForPosition方法来获取ViewHolder。首先看一下这个方法的源码:

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
    final int scrapCount = mAttachedScrap.size();
    // Try first for an exact, non-invalid match from scrap.
    for (int i = 0; i < scrapCount; i++) {
        final ViewHolder holder = mAttachedScrap.get(i);
        if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                        && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
            return holder;
        }
    }
    if (!dryRun) { // dryRun参数在方法的调用处均为false
        View view = mChildHelper.findHiddenNonRemovedView(position);
        if (view != null) {
            // This View is good to be used. We just need to unhide, detach and move to the scrap list.
            final ViewHolder vh = getChildViewHolderInt(view);
            ........
            return vh;
        }
    }
    // Search in our first-level recycled view cache.
    final int cacheSize = mCachedViews.size();
    for (int i = 0; i < cacheSize; i++) {
        final ViewHolder holder = mCachedViews.get(i);
        if (!holder.isInvalid() && holder.getLayoutPosition() == position
                && !holder.isAttachedToTransitionOverlay()) {
            ........
            return holder;
        }
    }
    return null;
}

在该方法中,会尝试从三个地方获取ViewHolder实例。首先对Recycler类中的mAttachedScrap集合进行遍历,当发现mAttachedScrap集合中存在某个ViewHolder的位置信息和方法中传入的位置信息一致并且这个ViewHolder是有效的,就返回这个ViewHolder实例;如果在mAttachedScrap中没有找到对应的ViewHolder,那么就继续向下执行调用mChildHelper的findHiddenNonRemovedView方法,这个mChildHelper是RecyclerView的一个成员变量,而这个ChildHelper类是一个负责管理RecyclerView条目的助手类,在它的findHiddenNonRemovedView方法中遍历它内部的mHiddenViews集合来返回一个对应位置上的隐藏视图,然后通过getChildViewHolderInt方法获取这个视图对应的ViewHolder实例然后返回;如果在mHiddenViews中还未找到就继续向下执行遍历Recycler类中的mCachedViews集合来寻找对应位置的ViewHolder实例。现在简单总结下在step2中获取ViewHolder实例的先后顺序:

mAttachedScrap(Recycler)-->mHiddenViews(ChildHelper)-->mCachedViews(Recycler)

3.3.3. getScrapOrCachedViewForId:

在step3处,当mAdapter.hasStableIds()为true的时候,会调用getScrapOrCachedViewForId来获取ViewHolder实例。在Adapter的hasStableIds方法内,返回的是Adapter内的成员变量mHasStableIds的值,这个值默认为false,只有当我们手动调用Adapter的setHasStableIds方法时才有可能将其置为true。而关于这个setHasStableIds方法源码中的解释为:

指示是否可以用唯一类型的标识符来表示数据集中的每一项

也就是说如果我们想要调用setHasStableIds方法将mHasStableIds变量置为true的话,我们必须要确保每一个条目都有一个同一类型的并且唯一的标识符可以用来识别它们,那到底要怎么设置这个唯一标识符呢?我们先来看一下刚刚getScrapOrCachedViewForId这个方法的源码:

ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
    // Look in our attached views first
    final int count = mAttachedScrap.size();
    for (int i = count - 1; i >= 0; i--) {
        final ViewHolder holder = mAttachedScrap.get(i);
        if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
            if (type == holder.getItemViewType()) {
                ........
                return holder;
            } 
            ........
        }
    }
    // Search the first-level cache
    final int cacheSize = mCachedViews.size();
    for (int i = cacheSize - 1; i >= 0; i--) {
        final ViewHolder holder = mCachedViews.get(i);
        if (holder.getItemId() == id && !holder.isAttachedToTransitionOverlay()) {
            if (type == holder.getItemViewType()) {
                ........
                return holder;
            } 
            ........
        }
    }
    return null;
}

看到该方法的源码应该能猜到刚刚说到的唯一标识符是什么了吧,其实就是每一个ViewHolder的mItemId。当我们实现一个Adapter的时候,我们可以根据自身的需要来决定是否重写Adaper的getItemId方法,当我们重写了这个方法的时候,我们在方法中返回的long类型的值就会最终被赋值到ViewHolder的mItemId变量上的。因此,这里要提前说明两个问题,第一,当我们手动调用了Adapter的setHasStableIds方法将mHasStableIds置为true时,我们一定要确保我们重写了Adapter的getItemId方法为每一个条目都设置了唯一标识,因为这时会根据ViewHolder的mItemId变量来判断缓存中是否有对应的ViewHolder实例;第二,在重写的Adapter的getItemId方法内,我们一定要确保为每一个条目设置的标识都是唯一的,不能重复。以上两点一定要注意,否则在视图复用的时候会出现视图紊乱的情况(感兴趣可自行验证下)。
现在再回到getScrapOrCachedViewForId方法,方法内又一次对mAttachedScrap和mCachedViews进行遍历,只不过在这里是根据ViewHolder的mItemId变量进行匹配,同时还要确保ViewHolder的mItemViewType变量值一致。这个mItemViewType变量其实我们都熟悉,当我们实现Adapter时,通过重写Adapter的getItemViewType方法为每一个条目设置的类型值最终就会赋值给对应的ViewHolder的mItemViewType变量。

3.3.4. ViewCacheExtension:

在step4处,当mViewCacheExtension不为空时,调用ViewCacheExtension的getViewForPositionAndType方法获取一个视图。这个ViewCacheExtension是RecyclerView的一个抽象的内部类,内部只有getViewForPositionAndType这么一个抽象方法,这个类的定义如下:

ViewCacheExtension是一个帮助类,它提供了一个可以由开发人员控制的额外的视图缓存层。

也就是说这个类是供我们开发人员自行实现缓存逻辑的一个帮助类,我们可以通过重写它的抽象方法来实现具体的获取指定位置的视图缓存的逻辑。关于这一级缓存,可以根据自身的情况来选择性的使用,不过在不确保自己的缓存逻辑没问题的情况下还是慎用的。

3.3.5. RecycledViewPool:

在step5处,到了RecyclerView的最后一级缓存了,通过调用RecycledViewPool的getRecycledView方法获取一个ViewHolder实例。首先说一下RecycledViewPool,它是RecyclerView的一个内部类,在它的内部还有一个叫ScrapData的内部类,这个ScrapData类的源码如下:

static class ScrapData {
    final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
    int mMaxScrap = DEFAULT_MAX_SCRAP;
    long mCreateRunningAverageNs = 0;
    long mBindRunningAverageNs = 0;
}

在ScrapData类的内部拥有一个mScrapHeap集合用来存储ViewHolder,现在再来看一下RecycledViewPool的getRecycledView方法:

public ViewHolder getRecycledView(int viewType) {
    final ScrapData scrapData = mScrap.get(viewType);
    if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
        final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
        for (int i = scrapHeap.size() - 1; i >= 0; i--) {
            if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
                return scrapHeap.remove(i);
            }
        }
    }
    return null;
}

方法中的mScrap变量是RecycledViewPool中的一个SparseArray类型的数组,这个数组内部的value类型为ScrapData类型,它的key就是ViewHolder的mItemViewType值。在getRecycledView方法中,我们根据mItemViewType获取mScrap数组中对应的ScrapData数据,然后再遍历ScrapData中的mScrapHeap集合返回一个对应mItemViewType的ViewHolder实例。

3.3.6. createViewHolder:

在step6处,我们通过调用createViewHolder方法来创建一个新的ViewHolder实例,createViewHolder方法的内部实现原理在前面已经讲过。在方法执行到这里的时候,说明在前面的几处缓存中并不存在指定位置上的ViewHolder实例,这时就要新建ViewHolder,也就在此时我们在Adapter中实现的onCreateViewHolder方法就会被调用了。

3.4. 缓存小结:

现在,我们完成了对RecyclerView获取缓存视图的逻辑的分析,通过分析我们可以知道,RecyclerView一共存在五层缓存,它们分别为mChangedScrap、mAttachedScrap、mCachedViews、mViewCacheExtension以及mRecyclerPool,其中mViewCacheExtension一般情况下是不会用到这一级缓存的。而对于这几级缓存,通过分析Recyler类的源码注释以及这几处缓存的调用时机可以总结出它们之间的区别如下:

3.4.1. mChangedScrap:

这个集合存放ViewHolder对象,数量上不做限制,它存放的是发生了变化的ViewHolder,如果使用这里面缓存的ViewHolder是要重新走Adapter的绑定方法的。

3.4.2. mAttachedScrap:

这个集合也是存放ViewHolder对象,同样没有数量上的限制,存放在这里的ViewHolder数据是不做修改的,不会重新走Adapter的绑定方法。在上面的mChangedScrap以及当前的mAttachedScrap中存放的ViewHolder对应的视图仅仅是被detach掉了,当再次被使用时只需重新attach即可,并未与RecyclerView完全解除关系。

3.4.3. mCachedViews:

这个集合依然存放的是ViewHolder对象,但是不同于上面两层缓存的是,它里面存放的ViewHolder对应的视图已经被remove掉,和RecyclerView已经没有任何关系了,但是它里面的ViewHolder依然保存着之前的数据信息,比如position和绑定的数据等。这一级缓存是有容量限制的,默认是2。

3.4.4. mRecyclerPool:

这个RecycledViewPool在前面已经讲过它内部的存储结构,它的内部实际存储的也是ViewHolder对象。只不过这里面保存的ViewHolder对应的视图不仅仅是已经被remove掉的视图,而且是没有绑定任何数据信息的视图了,如果使用这里缓存的ViewHolder是需要重新走Adapter的绑定方法了。

关于RecyclerView的缓存机制这里就分析到这,接下来我们再针对LinearLayoutManager的部分源码进行分析,从而对RecyclerView整体的工作机制有着更加深入的理解。

4.LinearLayoutManager:

通过最开始分析RecyclerView的绘制流程我们知道,LayoutManager在RecyclerView的使用中扮演着非常重要的角色。首先,在它的onLayoutChildren方法中将完成对每一个条目的测量和布局流程;其次在它的scrollBy方法中还会对RecyclerView的滑动事件进行响应处理。接下来我们就来分析下两个方法的源码:

4.1.onLayoutChildren:

关于LinearLayoutManager的onLayoutChildren方法,由于方法的源码比较长,这里不打算贴出源码了。在这个方法中进行的主要操作按照顺序依次如下:

1.在LinearLayoutManager中存在一个LayoutState类,这个类是RecyclerView在填充空白区域时存储临时状态的帮助类,在onLayoutChildren方法中,先对其进行初始化操作(如果mLayoutState为空)。
2.确定RecyclerView的布局方向,通过LinearLayoutManager的resolveShouldLayoutReverse方法。
3.确定锚点的位置和坐标,它决定了条目布局的起始位置。其中AnchorInfo是LinearLayoutManager的一个内部类,用来保存锚点信息的。
4.通过调用LayoutManager的detachAndScrapAttachedViews方法对当前存在的条目进行暂时的回收缓存,每一个条目会根据对应的条件缓存到不同的地方。
5.根据锚点信息向start和end方向填充条目视图,通过调用LinearLayoutManager的fill方法。
6.调用layoutForPredictiveAnimations方法进行和PredictiveAnimation相关的预布局操作。

通过以上的步骤可以看出,在onLayoutChildren方法内就是完成每一个条目向RecyclerView上的填充操作。而真正的将每一个条目添加到RecyclerView上的操作是在步骤5,步骤5中调用了LinearLayoutManager中的fill方法,在fill方法中存在一个while循环,根据mLayoutState来判断是否存在可填充的条目视图,如果存在就会在while循环内部调用layoutChunk方法将条目视图添加到RecyclerView上,我们一起看下这个layoutChunk方法的源码:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
1    View view = layoutState.next(recycler); 
2    ........
3    RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
4    if (layoutState.mScrapList == null) {
5        if (mShouldReverseLayout == (layoutState.mLayoutDirection
6                    == LayoutState.LAYOUT_START)) {
7            addView(view);
8        } else {
9            addView(view, 0);
10       }
11    } else {
12        ........
13    }
14    measureChildWithMargins(view, 0, 0);
15    result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
16    int left, top, right, bottom;
17    if (mOrientation == VERTICAL) {
18        if (isLayoutRTL()) {
19            right = getWidth() - getPaddingRight();
20            left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
21        } else {
22            left = getPaddingLeft();
23            right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
24        }
25        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
26            bottom = layoutState.mOffset;
27            top = layoutState.mOffset - result.mConsumed;
28        } else {
29            top = layoutState.mOffset;
30            bottom = layoutState.mOffset + result.mConsumed;
31        }
32    } else {
33       ........
34    }
35    layoutDecoratedWithMargins(view, left, top, right, bottom);
36    ........
}

只保留了方法中一些常规情况下的代码片段,在第一行通过调用LayoutState的next方法获取一个条目视图,关于这个next方法我们接下来会单独分析;在接下来的第4行判断LayoutState的mScrapList是否为空,这个mScrapList仅仅在layoutForPredictiveAnimations方法被调用过程中才可能不为空,因此这次我们暂时不考虑它的存在;在第5行根据布局的方向来调用addView方法将条目视图添加到RecyclerView上,关于这个addView方法接下来也会单独分析;接着就是在第14行调用measureChildWithMargins对条目视图进行测量操作;最后在第35行调用layoutDecoratedWithMargins方法对条目视图进行布局操作。其实这个方法非常的简单易懂,因此有些地方就不做详细解读,我们重点看一下刚刚说到的两个方法,首先看一下LayoutState的next方法。

4.2.LayoutState的next方法:

我们直接看一下这个方法的源码:

View next(RecyclerView.Recycler recycler) {
1    if (mScrapList != null) {
2        return nextViewFromScrapList();
3    }
4    final View view = recycler.getViewForPosition(mCurrentPosition);
5    mCurrentPosition += mItemDirection;
6    return view;
}

public View getViewForPosition(int position) {
    return getViewForPosition(position, false);
}

View getViewForPosition(int position, boolean dryRun) {
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

这里我们依然不考虑mScrapList不为空的情况。在第4行会调用Recycler的getViewForPosition方法,而这个方法内部经过逐层的调用,最终就会调用到前面我们说到的tryGetViewHolderForPositionByDeadline方法。这也就说明在RecyclerView的绘制流程中,是通过Recycler的tryGetViewHolderForPositionByDeadline方法来获取每一个条目视图的。

4.3.LayoutManager的addViewInt方法:

接着我们看一下刚刚说到的addView方法,这个方法其实是在LayoutManager中,在它的方法内部最终会调用到addViewInt方法,方法的源码如下:

private void addViewInt(View child, int index, boolean disappearing) {
    final ViewHolder holder = getChildViewHolderInt(child);
    ........
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    if (holder.wasReturnedFromScrap() || holder.isScrap()) {
        ........
        mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
    } else if (child.getParent() == mRecyclerView) { // it was not a scrap but a valid child
        // ensure in correct position
        int currentIndex = mChildHelper.indexOfChild(child);
        if (index == -1) {
            index = mChildHelper.getChildCount();
        }
        if (currentIndex == -1) {
            throw new IllegalStateException("Added View has RecyclerView as parent but"
                            + " view is not a real child. Unfiltered index:"
                            + mRecyclerView.indexOfChild(child) + mRecyclerView.exceptionLabel());
        }
        if (currentIndex != index) {
            mRecyclerView.mLayout.moveView(currentIndex, index);
        }
    } else {
        mChildHelper.addView(child, index, false);
        ........
    }
    ........
}

在方法的内部根据条目视图的来源不同而进行不同的操作处理,第一,如果视图来自scrap缓存,那么调用mChildHelper的attachViewToParent方法将视图重新依附到RecyclerView上;如果视图不是来自scrap缓存并且它的父布局就是当前的RecyclerView,那么就验证它的位置的合法性,如果位置不合法就把它从当前位置移动到另一个位置;如果前两种情况都不满足说明当前的视图并未添加到RecyclerView上,那么就调用mChildHelper的addView方法将视图添加到RecyclerView上。
方法中的逻辑很容易理解,这里重点看一下刚刚说到的这个mChildHelper,前面已经对这个ChildHelper类做过简单的介绍,它是管理RecyclerView条目的一个帮助类,在addViewInt方法中调用到的它的几个方法最终都会经过它内部的Callback接口回调到RecyclerView中,最终通过调用RecyclerView的addView、attachViewToParent、removeViewAt等方法完成每一个条目视图的添加,依附,移除等操作。可以说ChildHelper起到一个桥梁的作用,帮助RecyclerView更好的完成对每一个条目的管理工作。
到这里LinearLayoutManager的onLayoutChildren以及它内部的几个重要方法,我们就分析完了,通过分析我们可以证实当我们绘制一个RecyclerView的时候,每一个条目视图的获取,展示,测量以及布局操作是在LayoutManager的onLayoutChildren方法中完成的。

4.4.scrollBy方法:

在LinearLayoutManager中有一个scrollBy方法,在这个方法的内部完成了对RecyclerView滑动事件的处理。说到滑动,首先我们会想到的是RecyclerView的onTouchEvent方法,再准确些可以说是onTouchEvent方法中针对ACTION_MOVE事件的处理逻辑,我们一起看下源码:

public boolean onTouchEvent(MotionEvent e) {
    ........
    final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
    final boolean canScrollVertically = mLayout.canScrollVertically();
    ........
    switch (action) {
        ........
        case MotionEvent.ACTION_MOVE: {
            ........
            if (scrollByInternal(
                    canScrollHorizontally ? dx : 0,
                    canScrollVertically ? dy : 0,
                    e)) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            ........
        } break;
        ........
    }
    ........
    return true;
}

这里略去了大部分代码,首先在方法的开始会确定当前RecyclerView在哪个方向上可以滑动,在LinearLayoutManager中canScrollHorizontally和canScrollVertically方法的返回值取决于LinearLayoutManager中的mOrientation变量的值。这个mOrientation的默认值为LinearLayout.VERTICAL,此时canScrollHorizontally方法返回false,canScrollVertically方法返回true,当mOrientation的值为LinearLayout.HORIZONTAL时,canScrollHorizontally方法就返回true,而canScrollVertically方法返回false,这个逻辑应该很好理解,这里不多说了。接着看下在ACTION_MOVE事件处理中的scrollByInternal方法,在这个方法的内部经过逐层调用最终会根据滑动方向以及滑动偏移量来决定调用LayoutManager的scrollVerticallyBy或scrollHorizontallyBy方法中的其一,在LinearLayoutManager中,这两个方法的内部最终都会调用到scrollBy方法。scrollBy方法的源码如下:

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
    ........
    final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);
    ........
    return scrolled;
}

这里仅贴出scrollBy方法的少许源码,目的其实就是想让大家看到fill方法的调用,从而让大家明白RecyclerView的视图复用逻辑也是通过fill方法完成的,而刚刚fill方法的逻辑也已经分析过了。说到视图复用,就不得不想到数据更新,Adapter的onBindViewHolder方法就是负责实现RecyclerView每一个条目数据更新逻辑的,在onBindViewHolder方法中我们要根据方法中的位置参数来获取数据源中对应的数据来设置到视图上,从而确保RecyclerView在滑动过程中不会出现数据显示不正确的问题。而onBindViewHolder方法的最终调用处也是在Recycler的tryGetViewHolderForPositionByDeadline方法中,这里就不贴出具体的源码了,可以自行查看源码中的调用逻辑。

4.5.LinearLayoutManager小结:

到这里,关于LinearLayoutManager的主要逻辑也就分析的差不多了,通过分析我们可以得到以下这些结论:

1.LinearLayoutManager的onLayoutChildren方法中会完成RecyclerView的条目的测量和布局操作。
2.LinearLayoutManager的scrollBy方法中会完成对RecyclerView滑动事件的处理。
3.Recycler这个类在RecyclerView的整个工作机制中扮演着非常重要的作用,它不仅仅完成视图的回收和复用逻辑,同时创建一个条目视图的逻辑也在其内部完成。

结语

关于RecyclerView的知识点远不止文章中提到的这些,也只有自己走进源码认真的阅读它的内部实现逻辑才能更好的掌握它的工作机制。文章中可能有写的不正确的地方,还望批评指正!

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

推荐阅读更多精彩内容