RecycleView緩存—版本1

分析RV的緩存機制,先思考一下什么時候用到緩存機制洽议,從源碼的哪個角度去看鹊汛,緩存肯定是在滑動的時候去做的硼端,所以我們從onTouchEvent()的Action_Move開始看

  public boolean onTouchEvent(MotionEvent e) {

   
            case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(mScrollPointerId);
                if (index < 0) {
                    Log.e(TAG, "Error processing scroll; pointer index for id "
                            + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                    return false;
                }

                final int x = (int) (e.getX(index) + 0.5f);
                final int y = (int) (e.getY(index) + 0.5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;

                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                    dx -= mScrollConsumed[0];
                    dy -= mScrollConsumed[1];
                    vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                    // Updated the nested offsets
                    mNestedOffsets[0] += mScrollOffset[0];
                    mNestedOffsets[1] += mScrollOffset[1];
                }

                if (mScrollState != SCROLL_STATE_DRAGGING) {
                    boolean startScroll = false;
                    if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                        if (dx > 0) {
                            dx -= mTouchSlop;
                        } else {
                            dx += mTouchSlop;
                        }
                        startScroll = true;
                    }
                    if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                        if (dy > 0) {
                            dy -= mTouchSlop;
                        } else {
                            dy += mTouchSlop;
                        }
                        startScroll = true;
                    }
                    if (startScroll) {
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }

                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);
                    }
                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
                        mGapWorker.postFromTraversal(this, dx, dy);
                    }
                }
            } break;



}

滑動的監(jiān)聽里,調(diào)用了scrollByInternal()內(nèi)部封裝的一個滑動方法:

    boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0, unconsumedY = 0;
        int consumedX = 0, consumedY = 0;

        consumePendingUpdateOperations();
        if (mAdapter != null) {
            eatRequestLayout();
            onEnterLayoutOrScroll();
            Trace.beginSection(TRACE_SCROLL_TAG);
            if (x != 0) {
                consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
                unconsumedX = x - consumedX;
            }
            if (y != 0) {
                consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
                unconsumedY = y - consumedY;
            }
            Trace.endSection();
            repositionShadowingViews();
            onExitLayoutOrScroll();
            resumeRequestLayout(false);
        }
        if (!mItemDecorations.isEmpty()) {
            invalidate();
        }

        if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) {
            // Update the last touch co-ords, taking any scroll offset into account
            mLastTouchX -= mScrollOffset[0];
            mLastTouchY -= mScrollOffset[1];
            if (ev != null) {
                ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
            }
            mNestedOffsets[0] += mScrollOffset[0];
            mNestedOffsets[1] += mScrollOffset[1];
        } else if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
            if (ev != null) {
                pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
            }
            considerReleasingGlowsOnScroll(x, y);
        }
        if (consumedX != 0 || consumedY != 0) {
            dispatchOnScrolled(consumedX, consumedY);
        }
        if (!awakenScrollBars()) {
            invalidate();
        }
        return consumedX != 0 || consumedY != 0;
    }

在內(nèi)部滑動方法中给梅,調(diào)用了mLayout.scrollHorizontallyBy()/mLayout.scrollVerticallyBy()假丧;

    @VisibleForTesting LayoutManager mLayout;

這是RV的一個成員變量,使用setLayoutManager設(shè)置动羽,RV將他的布局的操作都封裝到了LayoutManager中包帚,常見的有LinearLayoutManager,GridLayoutManager运吓;

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

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


    int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getChildCount() == 0 || dy == 0) {
            return 0;
        }
        mLayoutState.mRecycle = true;
        ensureLayoutState();
        final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
        final int absDy = Math.abs(dy);
        updateLayoutState(layoutDirection, absDy, true, state);
        final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);
        if (consumed < 0) {
            if (DEBUG) {
                Log.d(TAG, "Don't have any more elements to scroll");
            }
            return 0;
        }
        final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
        mOrientationHelper.offsetChildren(-scrolled);
        if (DEBUG) {
            Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled);
        }
        mLayoutState.mLastScrollDelta = scrolled;
        return scrolled;
    }
fill()
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
 ...  
            //  回收
            recycleByLayoutState(recycler, layoutState);
            //  復(fù)用
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
  ...
}

fill()中有兩個核心方法渴邦,回收和復(fù)用疯趟;

回收機制

滑動的回收入口
recycleViewHolderInternal()
        void recycleViewHolderInternal(ViewHolder holder) {
          ...
               recycleCachedViewAt(0);
          ...
        }

        void recycleCachedViewAt(int cachedViewIndex) {
            ...
                //  取出需要回收的VH
                ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
                //  放到回收池中
                addViewHolderToRecycledViewPool(viewHolder, true);
                //  刪除這個VH
                mCachedViews.remove(cachedViewIndex);
        }
addViewHolderToRecycledViewPool()
        void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
          ...
                //  觸發(fā)Adapter和RV層設(shè)置的回調(diào)
               dispatchViewRecycled(holder);
                // 放入回收池
               getRecycledViewPool().putRecycledView(holder);
          ...
        }

        void dispatchViewRecycled(ViewHolder holder) {
            if (mRecyclerListener != null) {
                mRecyclerListener.onViewRecycled(holder);
            }
            if (mAdapter != null) {
                mAdapter.onViewRecycled(holder);
            }
            if (mState != null) {
                mViewInfoStore.removeViewHolder(holder);
            }
            
        }

        // 放入回收池
        public void putRecycledView(ViewHolder scrap) {
            //  獲取viewType
            final int viewType = scrap.getItemViewType();
            //  根據(jù)viewType取出SparseArray中的list
            final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
            scrap.resetInternal();
            //  添加到list中
            scrapHeap.add(scrap);
        }

復(fù)用機制

Recycler類

    public final class Recycler {
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null;

        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

        private final List<ViewHolder>
                mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

        private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
        int mViewCacheMax = DEFAULT_CACHE_SIZE;

        RecycledViewPool mRecyclerPool;

        private ViewCacheExtension mViewCacheExtension;

        static final int DEFAULT_CACHE_SIZE = 2;
}

Recycler類是RV復(fù)用機制的核心實現(xiàn)類,設(shè)計了四級緩存谋梭,優(yōu)先級順序是:Scrap信峻、CacheView、ViewCacheExtension瓮床、RecycledViewPool

  • mAttachedScrap:不參與滑動時的回收復(fù)用盹舞,只保存重新布局時從RecyclerView分離的item的無效、未移除纤垂、未更新的holder矾策。因為RecyclerView在onLayout的時候,會先把children全部移除掉峭沦,再重新添加進入贾虽,mAttachedScrap臨時保存這些holder復(fù)用。
  • mChangedScrap:mChangedScrap和mAttachedScrap類似吼鱼,不參與滑動時的回收復(fù)用蓬豁,只是用作臨時保存的變量,它只會負責(zé)保存重新布局時發(fā)生變化的item的無效菇肃、未移除的holder地粪,那么會重走adapter綁定數(shù)據(jù)的方法。
  • mCachedViews : 用于保存最新被移除(remove)的ViewHolder琐谤,已經(jīng)和RecyclerView分離的視圖蟆技;它的作用是滾動的回收復(fù)用時如果需要新的ViewHolder時,精準(zhǔn)匹配(根據(jù)position/id判斷)是不是原來被移除的那個item斗忌;如果是质礼,則直接返回ViewHolder使用,不需要重新綁定數(shù)據(jù)织阳;如果不是則不返回眶蕉,再去mRecyclerPool中找holder實例返回,并重新綁定數(shù)據(jù)唧躲。這一級的緩存是有容量限制的造挽,最大數(shù)量為2。
  • mViewCacheExtension: RecyclerView給開發(fā)者預(yù)留的緩存池弄痹,開發(fā)者可以自己拓展回收池饭入,一般不會用到,用RecyclerView系統(tǒng)自帶的已經(jīng)足夠了肛真。
  • mRecyclerPool:是一個終極回收站圣拄,真正存放著被標(biāo)識廢棄(其他池都不愿意回收)的ViewHolder的緩存池,如果上述mAttachedScrap毁欣、mChangedScrap庇谆、mCachedViews岳掐、mViewCacheExtension都找不到ViewHolder的情況下,就會從mRecyclerPool返回一個廢棄的ViewHolder實例饭耳,但是這里的ViewHolder是已經(jīng)被抹除數(shù)據(jù)的串述,沒有任何綁定的痕跡,需要重新綁定數(shù)據(jù)寞肖。它是根據(jù)itemType來存儲的纲酗,是以SparseArray嵌套一個ArraryList的形式保存ViewHolder的。

四級復(fù)用:

  • Scrap:最輕量級的復(fù)用新蟆,包含mAttachedScrapmAttachedScrap兩個部分觅赊,Scrap不會參與滑動時的回收復(fù)用,作為重新布局的臨時緩存琼稻,當(dāng)RV重新布局時吮螺,mAttachedScrap負責(zé)保存其中沒有改變的ViewHolder;剩下的由mChangedScrap負責(zé)保存帕翻,布局結(jié)束時鸠补,Scrap列表應(yīng)該是空的,緩存的數(shù)據(jù)要么重新布局出來嘀掸,要么被清空紫岩;

  • CacheView:用于RV列表位置產(chǎn)生變動時,對剛剛移出屏幕的view進行回收復(fù)用睬塌。CacheView的最大容量是2泉蝌,如果一直向一個方向滑動,緩存的Item超過兩個揩晴,就將CacheView中第一個item放入RecycledViewPool中勋陪,再將新的放入CacheView,如果一直朝一個方向滾動文狱,CacheView并沒有在效率上產(chǎn)生幫助粥鞋,它只是把后面滑過的ViewHolder緩存起來缘挽,如果經(jīng)常來回滑動瞄崇,那么從CacheView根據(jù)對應(yīng)位置的item直接復(fù)用,不需要重新綁定數(shù)據(jù)壕曼,將會得到很好的利用苏研。

  • ViewCacheExtension:自定義緩存,額外提供了一層緩存池給開發(fā)者腮郊,開發(fā)者視情況而定是否使用ViewCacheExtension增加一層緩存池摹蘑;

  • RecycledViewPool:終極回收站,在Scrap轧飞、CacheView衅鹿、ViewCacheExtension都不愿意回收的時候撒踪,都會丟到RecycledViewPool中回收;

    public static class RecycledViewPool {
        //  mScrap 的大小
        private static final int DEFAULT_MAX_SCRAP = 5;
        static class ScrapData {
            final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
        }
        SparseArray<ScrapData> mScrap = new SparseArray<>();
    }
RecycledViewPool

RecycledViewPool的本質(zhì)是一個SparseArray,SparseArray的value是ScrapData(存放VH的ArrayList)大渤,緩存池定義了默認的緩存大小DEFAULT_MAX_SCRAP = 5制妄,這個數(shù)量不是說整個緩存池只能緩存這多個ViewHolder,而是不同itemType的ViewHolder的list的緩存數(shù)量泵三,即mScrap的數(shù)量耕捞,說明最多只有5組不同類型的mScrapHeap。mMaxScrap = DEFAULT_MAX_SCRAP說明每種不同類型的ViewHolder默認保存5個烫幕,當(dāng)然mMaxScrap的值是可以設(shè)置的俺抽。

SparseArray分析:避免裝箱,節(jié)省空間较曼,不需要hash映射

其實磷斧,Scrap緩存池不參與滾動的回收復(fù)用,CacheView緩存池被稱為一級緩存诗芜,又因為ViewCacheExtension緩存池是給開發(fā)者定義的緩存池瞳抓,一般不用到,所以RecycledViewPool緩存池被稱為二級緩存伏恐,那么這樣來說實際只有兩層緩存孩哑。
滑動的復(fù)用
layoutChunk()
 void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
...
        View view = layoutState.next(recycler);
                addView(view);
...
}

tryGetViewHolderForPositionByDeadline是RV復(fù)用機制的具體實現(xiàn)方法,一共有四層復(fù)用池翠桦,下面結(jié)合這個方法的代碼理解四層復(fù)用池:

tryGetViewHolderForPositionByDeadline() 第一步

        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
            // 0) If there is a changed scrap, try to find from there
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
      ...
      }

        ViewHolder getChangedScrapViewForPosition(int position) {
            // If pre-layout, check the changed scrap for an exact match.
            // find by position
            for (int i = 0; i < changedScrapSize; i++) {
                final ViewHolder holder = mChangedScrap.get(i);   //
                if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
                    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                    return holder;
                }
            }
            // find by id
            if (mAdapter.hasStableIds()) {
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {
                    final long id = mAdapter.getItemId(offsetPosition);
                    for (int i = 0; i < changedScrapSize; i++) {
                        final ViewHolder holder = mChangedScrap.get(i);     //
                        if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
                            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                            return holder;
                        }
                    }
                }
            }
            return null;
        }
  • 第一步判斷有沒有改變的VH横蜒,在mChangedScrap中尋找,如果沒有進入第二步销凑;

tryGetViewHolderForPositionByDeadline() 第二步

        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
            //  如果第一層是null 進入第二層
            // 1) Find by position from scrap/hidden list/cache
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
      ...
      }

        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) {
                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);
                    mChildHelper.unhide(view);
                    int layoutIndex = mChildHelper.indexOfChild(view);
                    if (layoutIndex == RecyclerView.NO_POSITION) {
                        throw new IllegalStateException("layout index should not be -1 after "
                                + "unhiding a view:" + vh + exceptionLabel());
                    }
                    mChildHelper.detachViewFromParent(layoutIndex);
                    scrapView(view);
                    vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
                            | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                    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);
                // invalid view holders may be in cache if adapter has stable ids as they can be
                // retrieved via getScrapOrCachedViewForId
                if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
                    if (!dryRun) {
                        mCachedViews.remove(i);
                    }
                    if (DEBUG) {
                        Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                                + ") found match in cache: " + holder);
                    }
                    return holder;
                }
            }
            return null;
        }
  • 第二步丛晌,根據(jù)position分別在scrap的mAttachedScrap、mChildHelper斗幼、mCachedViews中查找澎蛛;

tryGetViewHolderForPositionByDeadline() 第三步

        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
                   // 2) Find from scrap/cache via stable ids, if exists
                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
      ...
      }

        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()) {
                        holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                        if (holder.isRemoved()) {
                            // this might be valid in two cases:
                            // > item is removed but we are in pre-layout pass
                            // >> do nothing. return as is. make sure we don't rebind
                            // > item is removed then added to another position and we are in
                            // post layout.
                            // >> remove removed and invalid flags, add update flag to rebind
                            // because item was invisible to us and we don't know what happened in
                            // between.
                            if (!mState.isPreLayout()) {
                                holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE
                                        | ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
                            }
                        }
                        return holder;
                    } else if (!dryRun) {
                        // if we are running animations, it is actually better to keep it in scrap
                        // but this would force layout manager to lay it out which would be bad.
                        // Recycle this scrap. Type mismatch.
                        mAttachedScrap.remove(i);
                        removeDetachedView(holder.itemView, false);
                        quickRecycleScrapView(holder.itemView);
                    }
                }
            }

            // 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) {
                    if (type == holder.getItemViewType()) {
                        if (!dryRun) {
                            mCachedViews.remove(i);
                        }
                        return holder;
                    } else if (!dryRun) {
                        recycleCachedViewAt(i);
                        return null;
                    }
                }
            }
            return null;
        }
  • 第三步,通過id在scrap的mAttachedScrap蜕窿、mCachedViews中查找谋逻;

tryGetViewHolderForPositionByDeadline() 第四步

        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
                if (holder == null && mViewCacheExtension != null) {
                    // We are NOT sending the offsetPosition because LayoutManager does not
                    // know it.
                    final View view = mViewCacheExtension
                            .getViewForPositionAndType(this, position, type);
      ...
      }

        public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
  • 第四步,在mViewCacheExtension中找桐经,這是開發(fā)者自定義的復(fù)用機制毁兆;

tryGetViewHolderForPositionByDeadline() 第五步

        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
                 if (holder == null) { // fallback to pool
                 holder = getRecycledViewPool().getRecycledView(type);
               }
      ...
      }

         public ViewHolder getRecycledView(int viewType) {
            final ScrapData scrapData = mScrap.get(viewType);
            if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
                final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
                return scrapHeap.remove(scrapHeap.size() - 1);
            }
            return null;
        }
  • 第五步,使用mRecyclerPool 回收池阴挣,根據(jù)ViewType取出對應(yīng)VH并移除回收池气堕;

        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      ...
                holder = mAdapter.createViewHolder(RecyclerView.this, type);   
      ...
      }
  • 第六步,如果還沒有,就創(chuàng)建一個新的VH茎芭;


    tryGetViewHolderForPositionByDeadline()流程
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末揖膜,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子梅桩,更是在濱河造成了極大的恐慌次氨,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摘投,死亡現(xiàn)場離奇詭異煮寡,居然都是意外死亡,警方通過查閱死者的電腦和手機犀呼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門幸撕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人外臂,你說我怎么就攤上這事坐儿。” “怎么了宋光?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵貌矿,是天一觀的道長。 經(jīng)常有香客問我罪佳,道長逛漫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任赘艳,我火速辦了婚禮酌毡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蕾管。我一直安慰自己枷踏,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布掰曾。 她就那樣靜靜地躺著旭蠕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪旷坦。 梳的紋絲不亂的頭發(fā)上掏熬,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機與錄音塞蹭,去河邊找鬼孽江。 笑死讶坯,一個胖子當(dāng)著我的面吹牛番电,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼漱办,長吁一口氣:“原來是場噩夢啊……” “哼这刷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起娩井,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤暇屋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后洞辣,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咐刨,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年扬霜,在試婚紗的時候發(fā)現(xiàn)自己被綠了定鸟。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡著瓶,死狀恐怖联予,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情材原,我是刑警寧澤沸久,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站余蟹,受9級特大地震影響卷胯,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜威酒,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一诵竭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧兼搏,春花似錦卵慰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吓著,卻和暖如春鲤嫡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背绑莺。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工暖眼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人纺裁。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓诫肠,卻偏偏與公主長得像司澎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子栋豫,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

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