RecyclerView 源碼分析-緩存機制

在開始緩存前缨称,我們先從RecyclerView的繪制開始分析,都知道RecyclerView的繪制是在LayoutManager中祝迂,真正執(zhí)行LayoutManager繪制的地方dispatchLayoutStep2()睦尽,同樣,放上代碼:

 private void dispatchLayoutStep2() {
      ...

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

       ...
    }

mLayout就是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
        if (DEBUG) {
            Log.d(TAG, "is pre layout:" + state.isPreLayout());
        }
        if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
            if (state.getItemCount() == 0) {
                removeAndRecycleAllViews(recycler);
                return;
            }
        }
        if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
            mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
        }

        ensureLayoutState();
        mLayoutState.mRecycle = false;
        // 1. 確定布局方向
        resolveShouldLayoutReverse();

        final View focused = getFocusedChild();
//2.  計算錨點位置
        if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
                || mPendingSavedState != null) {
            mAnchorInfo.reset();
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            // calculate anchor position and coordinate
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            mAnchorInfo.mValid = true;
        } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
                        >= mOrientationHelper.getEndAfterPadding()
                || mOrientationHelper.getDecoratedEnd(focused)
                <= mOrientationHelper.getStartAfterPadding())) {
            mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
        }
        if (DEBUG) {
            Log.d(TAG, "Anchor info:" + mAnchorInfo);
        }

      ...
//3. item 放入緩存
        detachAndScrapAttachedViews(recycler);
        mLayoutState.mInfinite = resolveIsInfinite();
        mLayoutState.mIsPreLayout = state.isPreLayout();
        // noRecycleSpace not needed: recycling doesn't happen in below's fill
        // invocations because mScrollingOffset is set to SCROLLING_OFFSET_NaN
        mLayoutState.mNoRecycleSpace = 0;
        if (mAnchorInfo.mLayoutFromEnd) {
            // fill towards start
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForStart;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
            final int firstElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForEnd += mLayoutState.mAvailable;
            }
            // fill towards end
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForEnd;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;

            if (mLayoutState.mAvailable > 0) {
                // end could not consume all. add more items towards start
                extraForStart = mLayoutState.mAvailable;
                updateLayoutStateToFillStart(firstElement, startOffset);
                mLayoutState.mExtraFillSpace = extraForStart;
                fill(recycler, mLayoutState, state, false);
                startOffset = mLayoutState.mOffset;
            }
        } else {
            // fill towards end
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForEnd;
// 4. 具體填充方法
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;
            final int lastElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForStart += mLayoutState.mAvailable;
            }
            // fill towards start
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForStart;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;

            if (mLayoutState.mAvailable > 0) {
                extraForEnd = mLayoutState.mAvailable;
                // start could not consume all it should. add more items towards end
                updateLayoutStateToFillEnd(lastElement, endOffset);
                mLayoutState.mExtraFillSpace = extraForEnd;
                fill(recycler, mLayoutState, state, false);
                endOffset = mLayoutState.mOffset;
            }
        }

        // changes may cause gaps on the UI, try to fix them.
        // TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have
        // changed
        if (getChildCount() > 0) {
            // because layout from end may be changed by scroll to position
            // we re-calculate it.
            // find which side we should check for gaps.
            if (mShouldReverseLayout ^ mStackFromEnd) {
                int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
                startOffset += fixOffset;
                endOffset += fixOffset;
                fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
                startOffset += fixOffset;
                endOffset += fixOffset;
            } else {
                int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
                startOffset += fixOffset;
                endOffset += fixOffset;
                fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
                startOffset += fixOffset;
                endOffset += fixOffset;
            }
        }
        layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
        if (!state.isPreLayout()) {
            mOrientationHelper.onLayoutComplete();
        } else {
            mAnchorInfo.reset();
        }
        mLastStackFromEnd = mStackFromEnd;
        if (DEBUG) {
            validateChildOrder();
        }
    }

代碼1處解決布局方向,實際就是縱向還是橫向布局纠俭;代碼2 處在錨點信息已過期或者滾動位置不是初始位置沿量,或者預存儲狀態(tài)不為null,則重置錨點冤荆,默認mAnchorInfo.mValid 為false朴则,所以會進入if邏輯中,mLayoutFromEnd的值钓简,在VERTICAL下mShouldReverseLayout為false 佛掖,mStackFromEnd默認也為false,所以異或的結果mLayoutFromEnd為false涌庭,其中mStackFromEnd是用來確定是否為正向布局,簡單來說在VERTICAL下mStackFromEnd為false為從上到下欧宜,為true反之坐榆。代碼3處放入緩存,稍后分析冗茸;先看一下確定錨點后怎么填充的席镀,具體填充方法就是代碼4處,看fill是如何做的:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
     ...
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            if (RecyclerView.VERBOSE_TRACING) {
                TraceCompat.beginSection("LLM LayoutChunk");
            }
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
          ...
        return start - layoutState.mAvailable;
    }

fill 核心的就是調(diào)用layoutChunk方法夏漱,點進去繼續(xù)看:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);
        if (view == null) {
            if (DEBUG && layoutState.mScrapList == null) {
                throw new RuntimeException("received null view when unexpected");
            }
            // if we are laying out views in scrap, this may return null which means there is
            // no more items to layout.
            result.mFinished = true;
            return;
        }
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        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);
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        int left, top, right, bottom;
        if (mOrientation == VERTICAL) {
            if (isLayoutRTL()) {
                right = getWidth() - getPaddingRight();
                left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
            } else {
                left = getPaddingLeft();
                right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
            }
            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                bottom = layoutState.mOffset;
                top = layoutState.mOffset - result.mConsumed;
            } else {
                top = layoutState.mOffset;
                bottom = layoutState.mOffset + result.mConsumed;
            }
        } else {
            top = getPaddingTop();
            bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);

            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                right = layoutState.mOffset;
                left = layoutState.mOffset - result.mConsumed;
            } else {
                left = layoutState.mOffset;
                right = layoutState.mOffset + result.mConsumed;
            }
        }
        // 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);
        if (DEBUG) {
            Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
                    + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
                    + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
        }
        // Consume the available space if the view is not removed OR changed
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }
        result.mFocusable = view.hasFocusable();
    }

這個方法就是關鍵的布局了豪诲,首先是調(diào)用layoutState.next(recycler)方法,這個方法就是在緩存中取出需要的view挂绰,然后繼續(xù)就是往ViewGroup中addview屎篱,畢竟RecyclerView還是繼承自ViewGroup。之后就是測量子view的寬高,在layout到指定位置交播。
到此重虑,我們在總結一下繪制的過程:

RecyclerView的繪制是由LayoutManager來處理,處理過程中先確認布局方向秦士,在根據(jù)布局方向確定錨點位置缺厉,之后是在確定填充方向(toStart 、toEnd)隧土,最后先存入緩存再在緩存中拿到子View提针,在addView到ViewGroup中

四級緩存

在繪制流程中,會將子view放置到緩存中曹傀,緩存怎么處理的辐脖,又有哪些緩存?首先卖毁,關于緩存都知道其存在四級緩存揖曾,先分別介紹各個緩存:

緩存級別 對應屬性 含義
一級緩存 mAttachedScrap和mChangedScrap detach的view相關, mAttachedScrap存儲的是當前還在屏幕中的ViewHolder亥啦。mChangedScrap存儲的是數(shù)據(jù)被更新的ViewHolder 炭剪,通常在預布局中使用。
二級緩存 mCachedViews 默認大小為2翔脱,remove掉的view會先進入此緩存
三級緩存 mViewCacheExtension 需自定義的奴拦,用不到
四級緩存 mRecyclerPool 按照viewType緩存holder,每個type對應的緩存大小為5

需要注意的是一級緩存跟detach的view相關届吁,其他是remove掉的错妖,這兩個的區(qū)別可以看這篇文章

簡單的介紹之后,我們還是在源碼角度看看怎么做的疚沐,還是在onLayoutChildren方法中暂氯,看到在找到錨點后會執(zhí)行detachAndScrapAttachedViews方法,點進去看看:

 public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
            final int childCount = getChildCount();
            for (int i = childCount - 1; i >= 0; i--) {
                final View v = getChildAt(i);
                scrapOrRecycleView(recycler, i, v);
            }
        }

        private void scrapOrRecycleView(Recycler recycler, int index, View view) {
            final ViewHolder viewHolder = getChildViewHolderInt(view);
            
            if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                    && !mRecyclerView.mAdapter.hasStableIds()) {
                removeViewAt(index);
                recycler.recycleViewHolderInternal(viewHolder);
            } else {
                detachViewAt(index);
                recycler.scrapView(view);
                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
            }
        }

首先得到child數(shù)量亮蛔,這地方需要注意的是getChildCount()并不等同于adapter.getItemCount(),而是attach到recyclerView的數(shù)量或者可以看的到的child痴施。其次就是scrapOrRecycleView開始放入緩存,我們看if邏輯判斷究流,當viewHolder是invalid狀態(tài)并且沒有移除且沒有設置StableIds標識會執(zhí)行removeView辣吃,invalid的狀態(tài)改變,在分析notifyDataSetChanged時在說芬探,這地方直接看else的代碼神得,先detach掉在scrapView,scrapView又是怎么操作的:

void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                    || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
                if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
                    throw new IllegalArgumentException("Called scrap view with an invalid view."
                            + " Invalid views cannot be reused from scrap, they should rebound from"
                            + " recycler pool." + exceptionLabel());
                }
                holder.setScrapContainer(this, false);
                mAttachedScrap.add(holder);
            } else {
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                holder.setScrapContainer(this, true);
                mChangedScrap.add(holder);
            }
        }

當調(diào)用notifyDataSetChanged時ViewHolder.FLAG_INVALID和holder.isUpdated()為true偷仿,所以會進入if控制中哩簿,然后就是添加到一級緩存mAttachedScrap中宵蕉,總結一下剛才的過程:

detachAndScrapAttachedViews 會將正在顯示的View 存入到一級緩存mAttachedScrap。

放入之后在什么地方會用到卡骂,回到layoutChunk方法国裳,第一行調(diào)用了View view = layoutState.next(recycler);現(xiàn)在看看是怎么取到View的:

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

調(diào)用了recycler.getViewForPosition(mCurrentPosition);,跟進去最后會調(diào)用到tryGetViewHolderForPositionByDeadline:

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
         ...
            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
            // 0) 預布局情況從mChangedScrap緩存中取
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
            // 1) Find by position from scrap/hidden list/cache
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                if (holder != null) {
                  // 無效會重新放入緩存中
                    if (!validateViewHolderForOffsetPosition(holder)) {
                        // recycle holder (and unscrap if relevant) since it can't be used
                        if (!dryRun) {
                            // we would like to recycle this but need to make sure it is not used by
                            // animation logic etc.
                            holder.addFlags(ViewHolder.FLAG_INVALID);
                            if (holder.isScrap()) {
                                removeDetachedView(holder.itemView, false);
                                holder.unScrap();
                            } else if (holder.wasReturnedFromScrap()) {
                                holder.clearReturnedFromScrapFlag();
                            }
                            recycleViewHolderInternal(holder);
                        }
                        holder = null;
                    } else {
                        fromScrapOrHiddenOrCache = true;
                    }
                }
            }
            if (holder == null) {
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
              
                final int type = mAdapter.getItemViewType(offsetPosition);
                // 2) Find from scrap/cache via stable ids, if exists
                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    if (holder != null) {
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    }
                }
        //3)mViewCacheExtension 獲取
                if (holder == null && mViewCacheExtension != null) {
                 ...
                }
                if (holder == null) { // fallback to pool
                   ...
          //4) 從RecycledViewPool中獲取
                    holder = getRecycledViewPool().getRecycledView(type);
                    if (holder != null) {
                        holder.resetInternal();
                        if (FORCE_INVALIDATE_DISPLAY_LIST) {
                            invalidateDisplayListInt(holder);
                        }
                    }
                }
                if (holder == null) {
                    long start = getNanoTime();
                    ...
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                   ...
                }
            }

           ...
            return holder;
        }

1處調(diào)用getScrapOrHiddenOrCachedHolderForPosition全跨,這個方法做的事情很簡單就是依次從mAttachedScrap缝左、hiddenViews和mCachedViews中獲取holder,需要注意的hiddenViews并不算緩存浓若,這個只和動畫有關渺杉,這個以后有時間在理解,2處會判斷是否設置了StableIds挪钓,設置了就調(diào)用getScrapOrCachedViewForId方法是越,這個方法會依次從mAttachedScrap和mCachedViews中獲取holder,getScrapOrHiddenOrCachedHolderForPositiongetScrapOrCachedViewForId調(diào)用都會在mAttachedScrap和mCachedViews中獲取holder碌上,不同的地方是一個通過position倚评,一個通過Id,也就是說通過position直接拿到的holder不用去判斷ItemViewType是否一致馏予,通過Id需要判斷ItemViewType的類型是否一致天梧。4處就是在RecycledViewPool中獲取了,具體看看這里面如何拿到的:

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

這地方先通過ItemViewType拿到指定類型的緩存霞丧,然后在得到指定的holder呢岗。
最后如果RecycledViewPool中沒有,就調(diào)用mAdapter.createViewHolder

問題

  1. notifyItemChanged 和notifyDataSetChanged區(qū)別
    我們看各自源碼做了什么
@Override
      //notifyDataSetChanged
        public void onChanged() {
            assertNotInLayoutOrScroll(null);
            mState.mStructureChanged = true;

            processDataSetCompletelyChanged(true);
            if (!mAdapterHelper.hasPendingUpdates()) {
                requestLayout();
            }
        }

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

void processDataSetCompletelyChanged(boolean dispatchItemsChanged) {
        markKnownViewsInvalid();
    }

    void markKnownViewsInvalid() {
        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.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
            }
        }
        markItemDecorInsetsDirty();
        mRecycler.markKnownViewsInvalid();
    }

不一樣的地方就是notifyDataSetChanged最后會添加flag值ViewHolder.FLAG_UPDATE和ViewHolder.FLAG_INVALID蛹尝,這兩個的作用是什么后豫,我們回到緩存相關的代碼

  private void scrapOrRecycleView(Recycler recycler, int index, View view) {
            final ViewHolder viewHolder = getChildViewHolderInt(view);
            
            if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                    && !mRecyclerView.mAdapter.hasStableIds()) {
                removeViewAt(index);
                recycler.recycleViewHolderInternal(viewHolder);
            } else {
                detachViewAt(index);
                recycler.scrapView(view);
                mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
            }
        }

看到會在if判斷中執(zhí)行,也就是會先remove掉view突那,在放入二級緩存中挫酿,這個復用性要比detach的低一些,具體可以參考這篇文章愕难,所以remove掉在add回來可能就會有圖片閃動的問題饭豹,這也是notifyDataSetChanged效率低的原因。

  1. 如何提高緩存復用或StableIds 如何提高效率的
    前面也說過务漩,notifyDataSetChanged會效率低些,不過在重寫mAdapter.hasStableIds()時它褪,可以讓緩存放入一級緩存中饵骨,另外在復用時通過ViewType方式來獲取ViewHolder,會優(yōu)先到一級或者二級緩存里面去尋找茫打,而不是直接去RecyclerViewPool里面去尋找居触。
  if (mAdapter.hasStableIds()) {
        holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
  }

參考:
RecyclerView 源碼分析(三) - RecyclerView的緩存機制
RecyclerView機制分析: Recycler

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末妖混,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子轮洋,更是在濱河造成了極大的恐慌制市,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,423評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件弊予,死亡現(xiàn)場離奇詭異祥楣,居然都是意外死亡,警方通過查閱死者的電腦和手機汉柒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,147評論 2 385
  • 文/潘曉璐 我一進店門误褪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人碾褂,你說我怎么就攤上這事兽间。” “怎么了正塌?”我有些...
    開封第一講書人閱讀 157,019評論 0 348
  • 文/不壞的土叔 我叫張陵嘀略,是天一觀的道長。 經(jīng)常有香客問我乓诽,道長帜羊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,443評論 1 283
  • 正文 為了忘掉前任问裕,我火速辦了婚禮逮壁,結果婚禮上,老公的妹妹穿的比我還像新娘粮宛。我一直安慰自己窥淆,他們只是感情好,可當我...
    茶點故事閱讀 65,535評論 6 385
  • 文/花漫 我一把揭開白布巍杈。 她就那樣靜靜地躺著忧饭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪筷畦。 梳的紋絲不亂的頭發(fā)上词裤,一...
    開封第一講書人閱讀 49,798評論 1 290
  • 那天,我揣著相機與錄音鳖宾,去河邊找鬼吼砂。 笑死,一個胖子當著我的面吹牛鼎文,可吹牛的內(nèi)容都是我干的渔肩。 我是一名探鬼主播,決...
    沈念sama閱讀 38,941評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼拇惋,長吁一口氣:“原來是場噩夢啊……” “哼周偎!你這毒婦竟也來了抹剩?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,704評論 0 266
  • 序言:老撾萬榮一對情侶失蹤蓉坎,失蹤者是張志新(化名)和其女友劉穎澳眷,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蛉艾,經(jīng)...
    沈念sama閱讀 44,152評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡钳踊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,494評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了伺通。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片箍土。...
    茶點故事閱讀 38,629評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖罐监,靈堂內(nèi)的尸體忽然破棺而出吴藻,到底是詐尸還是另有隱情,我是刑警寧澤弓柱,帶...
    沈念sama閱讀 34,295評論 4 329
  • 正文 年R本政府宣布沟堡,位于F島的核電站,受9級特大地震影響矢空,放射性物質發(fā)生泄漏航罗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,901評論 3 313
  • 文/蒙蒙 一屁药、第九天 我趴在偏房一處隱蔽的房頂上張望粥血。 院中可真熱鬧,春花似錦酿箭、人聲如沸复亏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缔御。三九已至,卻和暖如春妇蛀,著一層夾襖步出監(jiān)牢的瞬間耕突,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,978評論 1 266
  • 我被黑心中介騙來泰國打工评架, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留眷茁,地道東北人。 一個月前我還...
    沈念sama閱讀 46,333評論 2 360
  • 正文 我出身青樓纵诞,卻偏偏與公主長得像上祈,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,499評論 2 348

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