RecyclerView渲染分析

上一篇介紹基本使用,這一篇我們深入分析RecyclerView內(nèi)部被渲染的流程航徙,在這里我們從三個方面來了解流程锨侯,具體三個方面如下:

  • 啟動渲染:一般首次渲染的時候。
  • 滑動渲染: 手指觸摸屏幕滑動直到離開的過程早抠。
  • 通知渲染:一般調(diào)用notifyDataSetChanged() 或者 notifyItemChanged()時候。
1撬讽、啟動渲染

RecyclerView繼承ViewGroup蕊连,同樣準(zhǔn)守View的繪制過程悬垃,所以也會走onMeasure、onLayout甘苍、onDraw方法尝蠕,我們先來看看RecyclerView的onMeasure方法。

Override
 protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        if (mLayout.isAutoMeasureEnabled()) {
            //...省略
            // modes in both dimensions are EXACTLY.
            mLastAutoMeasureSkippedDueToExact =
            widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY
            if (mLastAutoMeasureSkippedDueToExact || mAdapter == null) {
                    return;
            }
            if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
            }
            mLayout.setMeasureSpecs(widthSpec, heightSpec);
            mState.mIsMeasuring = true;
            dispatchLayoutStep2();
            //...省略
        }else{
            //...省略
        }
 }

我們只看核心代碼载庭,現(xiàn)在我們以LinearLayoutManager為例子看彼,LinearLayoutManager重寫了isAutoMeasureEnabled()方法并且返回true,這樣確定了RecyclerView的繪制走了自動布局的邏輯囚聚。如果Recyclerview確定了畫布大芯搁拧(寬高都是MeasureSpec.EXACTLY)直接return,測量結(jié)束靡挥。重點(diǎn)還是dispatchLayoutStep1()序矩、dispatchLayoutStep2()邏輯。其中dispatchLayoutStep1()的代碼如下:

    /**
     * The first step of a layout where we;
     * - process adapter updates                處理adapter更新     
     * - decide which animation should run      決定哪個執(zhí)行動畫
     * - save information about current views   保存當(dāng)前view的信息
     * - If necessary, run predictive layout and save its information   是否進(jìn)行預(yù)加載并且保存預(yù)加載信息
     */
    private void dispatchLayoutStep1() {
        mState.mIsMeasuring = false;
        processAdapterUpdatesAndSetAnimationFlags();
        saveFocusInfo();
        //...省略
        if (mState.mRunSimpleAnimations) {
        //...省略  
        }
        if (mState.mRunPredictiveAnimations) {
        //...省略 
        }       
        // ..省略
       mState.mLayoutStep = State.STEP_LAYOUT;
    }

dispatchLayoutStep1()主要還是更新狀態(tài)(mState)和視圖(mViewInfoStore)為下一步繪制做準(zhǔn)備跋破。接下來看dispatchLayoutStep2()方法,代碼如下:

/**
     * The second layout step where we do the actual layout of the views for the final state.
     * This step might be run multiple times if necessary (e.g. measure).
     */
    private void dispatchLayoutStep2() {
        //..省略
        // Step 2: Run layout
        mState.mInPreLayout = false;
        mLayout.onLayoutChildren(mRecycler, mState);
        //..省略    
        mState.mLayoutStep = State.STEP_ANIMATIONS;
    }

這個方法更新了部分State,核心還是onLayoutChildren()方法瓶蝴,由LayoutManager代理毒返,其他onLayoutChildren()實現(xiàn)了RecyclerView的絕大部分視圖渲染邏輯。我們這邊以LinearLayoutMananger為例子進(jìn)行繪制分析舷手,接下來看看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
        //以上注釋已經(jīng)說明了繪制算法:1拧簸、先確定錨點(diǎn)(anchor)2、布局方向由下往上或者由上往下繪制男窟。
        //...省略
       if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
            || mPendingSavedState != null) {  //首次繪制確定錨點(diǎn)盆赤、滿足次條件。
            mAnchorInfo.reset();
            //確定繪制步驟歉眷,決定由上往下還是由下往上繪制牺六。
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            // calculate anchor position and coordinate 計算anchor的position 和 coordinate
            //計算anchor由一套規(guī)則,anchor依次由滑動mPendingSavedState汗捡、聚焦view來確定淑际,如果未發(fā)現(xiàn)初始化anchor(position = 0)。
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            mAnchorInfo.mValid = true;
        } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
           >= mOrientationHelper.getEndAfterPadding()
                || mOrientationHelper.getDecoratedEnd(focused)
                <= mOrientationHelper.getStartAfterPadding())) {
                //在有focused聚焦視圖情況下扇住,滿足條件更新錨點(diǎn)信息春缕。
                mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
        }
        //...省略
        if (mAnchorInfo.mLayoutFromEnd) {
          // fill towards start 
          updateLayoutStateToFillStart(mAnchorInfo);
          fill(recycler, mLayoutState, state, false);
          // fill towards end
          updateLayoutStateToFillEnd(mAnchorInfo);
          fill(recycler, mLayoutState, state, false);
        } else {
          // fill towards end
          updateLayoutStateToFillEnd(mAnchorInfo);
          fill(recycler, mLayoutState, state, false);
          // fill towards start
          updateLayoutStateToFillStart(mAnchorInfo);
          fill(recycler, mLayoutState, state, false);
        }
        //...省略
 }

以上代碼體現(xiàn)錨點(diǎn)的計算非常重要,我們來看一下錨點(diǎn)(AnchorInfo)和布局狀態(tài)(LayoutState)重要屬性艘蹋。

    /**
     * Simple data class to keep Anchor information
     */
    static class AnchorInfo {
        OrientationHelper mOrientationHelper; //計算start锄贼,end等輔助類
        int mPosition;             //Anchor 啟點(diǎn)的位置
        int mCoordinate;           //Anchor Y軸上起始繪制偏移量
        boolean mLayoutFromEnd;    //布局方向
        boolean mValid;            //錨點(diǎn)是否合法
    }

    /**
     * Helper class that keeps temporary state while {LayoutManager} is filling out the empty
     * space.
     */
    static class LayoutState {
        /**
         * Current position on the adapter to get the next item.
        * 當(dāng)前位置
         */
        int mCurrentPosition;
         /**
         * Number of pixels that we should fill, in the layout direction.
         * 布局方向有多少空間需要被填充
         */
        int mAvailable;
        /**
         * Used if you want to pre-layout items that are not yet visible.
         * 提前預(yù)加載空間
         * The difference with {@link #mAvailable} is that, when recycling, distance laid out for
         * {@link #mExtraFillSpace} is not considered to avoid recycling visible children.
         */
        int mExtraFillSpace = 0;
        /**
         * Pixel offset where layout should start
         * 開始布局的偏移位置
         */
        int mOffset;
         /**
         * Defines the direction in which the layout is filled.
         * 布局方向LAYOUT_START或者LAYOUT_END
         * Should be {@link #LAYOUT_START} or {@link #LAYOUT_END}
         */
        int mLayoutDirection;
         /**
         * Used when LayoutState is constructed in a scrolling state.
         * 滾動的距離
         * It should be set the amount of scrolling we can make without creating a new view.
         * Settings this is required for efficient view recycling.
         */
        int mScrollingOffset;
    }

了解重要屬性之后,下面我們通過圖片來表達(dá)核心的繪制邏輯女阀。


圖一
圖二

圖一為首次RecyclerView繪制的時候宅荤,AnchorInfo{mPosition = 0,mCoordinate = 0} 屑迂,mLayoutDirection = LayoutState.LAYOUT_START 時候往上繪制空間為0,所以只考慮往下繪制(mAvailable + mExtraFillSpace)膘侮。圖二為多數(shù)滑動或者聚焦view(比如resume界面)的時候渲染經(jīng)過屈糊。在onLayoutChildren方法中調(diào)用updateLayoutStateToFillStart或者updateLayoutStateToFillEnd將計算結(jié)果更新至LayoutState,最后交付給fill進(jìn)行填充繪制琼了。

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
             RecyclerView.State state, boolean stopOnFocusable) {
        //...省略
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            /**
             * Consume the available space if:
             * * layoutChunk did not request to be ignored
             * * OR we are laying out scrap children
             * * OR we are not doing pre-layout
             */
            if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                    || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                // we keep a separate remaining space because mAvailable is important for recycling
                remainingSpace -= layoutChunkResult.mConsumed;
            }
            //...省略
       }
}
protected static class LayoutChunkResult {
        public int mConsumed; //item高度
        //...省略
}

fill代碼中不斷計算remainingSpace剩余高度逻锐,當(dāng)remainingSpace需要填充的時候調(diào)用layoutChunk()方法進(jìn)行item繪制,并且不斷更新對應(yīng)mConsumed雕薪,接下來我們看下layoutChunk方法昧诱。

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
                     LayoutState layoutState, LayoutChunkResult result) {
      View view = layoutState.next(recycler); //這個方法非常重要,直接包含了recyclerview的緩存機(jī)制所袁,此處暫時沒進(jìn)行解釋盏档,緩存講解的時候再來看這個方法。
      RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
      //首次繪制layoutState.mScrapList == null
      if (layoutState.mScrapList == null) {
           if (mShouldReverseLayout == (layoutState.mLayoutDirection
                   == LayoutState.LAYOUT_START)) {
               addView(view);
           } else {
               addView(view, 0);
           }
       } else {
          //此處暫時不看燥爷,這邊預(yù)加載動畫相關(guān)的view添加蜈亩。
           if (mShouldReverseLayout == (layoutState.mLayoutDirection
                   == LayoutState.LAYOUT_START)) {
               addDisappearingView(view);
           } else {
               addDisappearingView(view, 0);
           }
       }
       measureChildWithMargins(view, 0, 0);
       result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
       // We calculate everything with View's bounding box (which includes decor and margins)
       // To calculate correct layout position, we subtract margins.
       layoutDecoratedWithMargins(view, left, top, right, bottom);
       //...省略
}

以上代碼將item繪制出來并且addview到recyclerview中,并且通過layoutDecoratedWithMargins調(diào)整到最終的位置前翎。到此處我們來看一下item的包裝類稚配,補(bǔ)充下OrientationHelper,這個類協(xié)助remainingSpace空間的計算港华。


圖三
2道川、滑動渲染

滑動分為兩種,一種是正沉⒁耍滑動冒萄,一種是快速滑動(Flinger),以下分析這兩種滑動代碼橙数。

@Override
public boolean onTouchEvent(MotionEvent e) {
      //...省略
     switch (action) {
            case MotionEvent.ACTION_DOWN: {
                  mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
                  mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
            }
            //...省略
            case MotionEvent.ACTION_MOVE: {
              if (mScrollState != SCROLL_STATE_DRAGGING) {
                   if (canScrollHorizontally) {
                          if (dx > 0) {
                              dx = Math.max(0, dx - mTouchSlop);
                          } else {
                              dx = Math.min(0, dx + mTouchSlop);
                          }
                          if (dx != 0) {
                              startScroll = true;
                          }
                    }
                    if (canScrollVertically) {
                        if (dy > 0) {
                            dy = Math.max(0, dy - mTouchSlop);
                        } else {
                            dy = Math.min(0, dy + mTouchSlop);
                        }
                        if (dy != 0) {
                            startScroll = true;
                        }
                    }
                }
            }
            //...省略
           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);
                }
            }
            //...省略
          case MotionEvent.ACTION_UP: {
            //...省略
            final float yvel = canScrollVertically ?
                    -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0;
            if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                setScrollState(SCROLL_STATE_IDLE);
            }
            resetTouch();
          }
          //...省略
}

以上代碼為手指觸摸到屏幕后尊流,記錄mLastTouchX、mLastTouchY商模,經(jīng)過ACTION_MOVE奠旺,計算dy或者dx,并且mTouchSlop為滑動閥值(這個值可初始化時候決定施流,能決定滑動偏移值)响疚,dy或者dx大于這個值觸發(fā)scrollByInternal方法,我們來看一下scrollByInternal方法瞪醋。

boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
    //...省略
    if (mAdapter != null) {
        //...省略
       scrollStep(x, y, mReusableIntPair);
        //...省略
    }
    //...省略
}

在onTouchEvent中忿晕,當(dāng)ACTION_UP手指抬起來的時候,經(jīng)過VelocityTrackerCompat根據(jù)初速度來計算是否觸發(fā)fling()事件银受,我們先來看看fling的調(diào)用過程践盼。

public boolean fling(int velocityX, int velocityY) {
    //...省略
    velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
    velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
    mViewFlinger.fling(velocityX, velocityY);
   //...省略
}

接著看ViewFlinger中的fling鸦采、postOnAnimation、internalPostOnAnimation方法

public void fling(int velocityX, int velocityY) {
     //...省略
    postOnAnimation();
    //...省略
}

void postOnAnimation() {
     if (mEatRunOnAnimationRequest) {
          mReSchedulePostAnimationCallback = true;
      } else {
           internalPostOnAnimation();
      }
}

private void internalPostOnAnimation() {
      removeCallbacks(this);
      ViewCompat.postOnAnimation(RecyclerView.this, this);
 }

最終調(diào)用的是ViewFlinger方法中的run方法咕幻。

@Override
public void run() {
     //...省略
     if (mAdapter != null) {
         //...省略
        scrollStep(unconsumedX, unconsumedY, mReusableIntPair);
         //...省略
     }
     //...省略
}

接下來分析scrollStep方法

void scrollStep(int dx, int dy, @Nullable int[] consumed) {
     //...省略
     if (dx != 0) {
            consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
     }
    if (dy != 0) {
          consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
    }
     //...省略
}

最終調(diào)用了LayoutMananger中的scrollHorizontallyBy和scrollVerticallyBy方法渔伯,我們以LinearLayoutMananger為例子。以下分析LinearLayoutMananger中scrollHorizontallyBy為例子肄程。

/**
* {@inheritDoc}
*/
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
                                RecyclerView.State state) {
    if (mOrientation == VERTICAL) {
          return 0;
    }
     return scrollBy(dx, recycler, state);
}

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
     //...省略
    updateLayoutState(layoutDirection, absDelta, true, state);
    final int consumed = mLayoutState.mScrollingOffset
            + fill(recycler, mLayoutState, state, false);
     //...省略    
}

到此處就可以看到updateLayoutState和fill方法锣吼,通過這兩個方法不斷的測量可繪空間并且填充,可繪制的空間加上了滑動的偏移量蓝厌。綜述完成了滑動繪制的講述玄叠。

3、通知渲染

通知渲染也有兩種方式:notifyDataSetChanged全局刷新和notifyItemChanged局部刷新拓提。

  • 先來看看notifyDataSetChanged()
public final void notifyDataSetChanged() {
       mObservable.notifyChanged();
}
public void notifyChanged() {
          // since onChanged() is implemented by the app, it could do anything, including
          // removing itself from {@link mObservers} - and that could cause problems if
          // an iterator is used on the ArrayList {@link mObservers}.
          // to avoid such problems, just march thru the list in the reverse order.
          for (int i = mObservers.size() - 1; i >= 0; i--) {
              mObservers.get(i).onChanged();
          }
}

這個mObservers的注冊在setAdapter方法中读恃,調(diào)用的順序依次setAdapter -> setAdapterInternal -> registerAdapterDataObserver -> registerObserver,接下來代碼如下:

  public void registerObserver(T observer) {
        if (observer == null) {
            throw new IllegalArgumentException("The observer is null.");
        }
        synchronized(mObservers) {
            if (mObservers.contains(observer)) {
                throw new IllegalStateException("Observer " + observer + " is already registered.");
            }
            mObservers.add(observer);
        }
    }

最終可以看出來調(diào)用的是RecyclerView內(nèi)部的成員RecyclerViewDataObserver代态,RecyclerViewDataObserver中的onChanged是最后調(diào)用的目的寺惫,代碼如下:

   @Override
   public void onChanged() {
        assertNotInLayoutOrScroll(null);
        mState.mStructureChanged = true;
        processDataSetCompletelyChanged(true);
        if (!mAdapterHelper.hasPendingUpdates()) {
              requestLayout();
        }
   }

最終調(diào)用requestLayout方法,進(jìn)行重新繪制蹦疑。其中processDataSetCompletelyChanged(true)會將所有狀態(tài)進(jìn)行更新肌蜻,保證不進(jìn)行動畫執(zhí)行。

@Override
public void requestLayout() {
       if (mEatRequestLayout == 0 && !mLayoutFrozen) {
           super.requestLayout();
       } else {
           mLayoutRequestEaten = true;
       }
}

通過super.requestLayout()的調(diào)用會觸發(fā)onLayout()方法必尼,最后調(diào)用dispatchLayout進(jìn)行繪制。

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

以上方法由進(jìn)行dispatchLayoutStep1()篡撵、dispatchLayoutStep2()判莉、dispatchLayoutStep3()方法。其中dispatchLayoutStep3()和動畫繪制和執(zhí)行相關(guān)育谬。
其中dispatchLayoutStep由mState.mLayoutStep的狀態(tài)決定券盅。其中mState.mLayoutStep的狀態(tài)對應(yīng)State.STEP_START、State.STEP_LAYOUT膛檀、State.STEP_ANIMATIONS锰镀。

  • 接著看notifyItemChanged局部刷新
 public final void notifyItemChanged(int position, @Nullable Object payload) {
            mObservable.notifyItemRangeChanged(position, 1, payload);
 }
 public void notifyItemRangeChanged(int positionStart, int itemCount,
                                           @Nullable Object payload) {
            // since onItemRangeChanged() is implemented by the app, it could do anything, including
            // removing itself from {@link mObservers} - and that could cause problems if
            // an iterator is used on the ArrayList {@link mObservers}.
            // to avoid such problems, just march thru the list in the reverse order.
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload);
            }
}

最終調(diào)用了RecyclerViewDataObserver方法中的onItemRangeChanged方法。

 @Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
     assertNotInLayoutOrScroll(null);
     if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
         triggerUpdateProcessor();
     }
}

void triggerUpdateProcessor() {
         if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
               ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
         } else {
              mAdapterUpdateDuringMeasure = true;
              requestLayout();
          }
}

mAdapterHelper.onItemRangeChanged方法將需要執(zhí)行的item加入到AdapterHelper.UpdateO中咖刃,需要執(zhí)行的item的動作(ADD泳炉、REMOVE、UPDATE嚎杨、MOVE)通過AdapterHelper.UpdateOp進(jìn)行記錄花鹅,放入到一個等待隊列中去,并且觸發(fā)requestLayout枫浙,最終經(jīng)過dispatchLayoutStep3()方法執(zhí)行動畫操作刨肃。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末古拴,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子真友,更是在濱河造成了極大的恐慌黄痪,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盔然,死亡現(xiàn)場離奇詭異桅打,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)轻纪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門油额,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人刻帚,你說我怎么就攤上這事潦嘶。” “怎么了崇众?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵掂僵,是天一觀的道長。 經(jīng)常有香客問我顷歌,道長锰蓬,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任眯漩,我火速辦了婚禮芹扭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘赦抖。我一直安慰自己舱卡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布队萤。 她就那樣靜靜地躺著轮锥,像睡著了一般。 火紅的嫁衣襯著肌膚如雪要尔。 梳的紋絲不亂的頭發(fā)上舍杜,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機(jī)與錄音赵辕,去河邊找鬼既绩。 笑死,一個胖子當(dāng)著我的面吹牛匆帚,可吹牛的內(nèi)容都是我干的熬词。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼互拾!你這毒婦竟也來了歪今?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤颜矿,失蹤者是張志新(化名)和其女友劉穎寄猩,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體骑疆,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡田篇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了箍铭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泊柬。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖诈火,靈堂內(nèi)的尸體忽然破棺而出兽赁,到底是詐尸還是另有隱情,我是刑警寧澤冷守,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布刀崖,位于F島的核電站,受9級特大地震影響拍摇,放射性物質(zhì)發(fā)生泄漏亮钦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一充活、第九天 我趴在偏房一處隱蔽的房頂上張望蜂莉。 院中可真熱鬧,春花似錦混卵、人聲如沸巡语。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至荤堪,卻和暖如春合陵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背澄阳。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工拥知, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人碎赢。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓低剔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子襟齿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容