【進(jìn)階】RecyclerView源碼解析(二)——緩存機(jī)制

本系列博客基于com.android.support:recyclerview-v7:26.1.0
1.【進(jìn)階】RecyclerView源碼解析(一)——繪制流程
2.【進(jìn)階】RecyclerView源碼解析(二)——緩存機(jī)制
3.【進(jìn)階】RecyclerView源碼解析(三)——深度解析緩存機(jī)制
4.【進(jìn)階】RecyclerView源碼解析(四)——RecyclerView進(jìn)階優(yōu)化使用
5.【框架】基于AOP的RecyclerView復(fù)雜樓層樣式的開發(fā)框架难菌,樓層打通,支持組件化蔑滓,支持MVP(不用每次再寫Adapter了~)

接著上一篇博客分析完RecyclerView的繪制流程郊酒,其實對RecyclerView已經(jīng)有了一個大體的了解,尤其是RecyclerView和LayoutManager和ItemDecoration的關(guān)系烫饼。 本篇文章將對RecyclerView的緩存機(jī)制的講解猎塞,但由于緩存對于RecyclerView非常重要,所以準(zhǔn)備分幾部分進(jìn)行分析杠纵,本篇博客主要從源碼角度進(jìn)行分析緩存的流程荠耽。

前言

無論是原來使用的ListView還是RecyclerView,列表類型的視圖一直是原生使用的一個重頭戲比藻。無論是從使用功能上還是性能上铝量,原生的列表視圖都有著巨大的優(yōu)勢,而這個優(yōu)勢很重要的一方面其實就是對于視圖的復(fù)用機(jī)制银亲,也就是緩存慢叨。從ListView的RecycleBin到RecyclerView的Recycler,Google對于列表視圖的緩存的設(shè)計一直非澄耱穑考究值得我們學(xué)習(xí)和研究拍谐。而網(wǎng)頁的H5和火熱的RN對于復(fù)雜的列表視圖的渲染性能不好從這里面其實也可以尋找到一些原因。

總流程圖

放上一張Bugly的一篇博客對RecyclerView的緩存的流程圖吧(自己畫發(fā)現(xiàn)差不多就直接挪過來了...若侵立刪)

總流程

源碼分析

如果看過上一篇博客的人應(yīng)該還記得我們當(dāng)中提到了和緩存機(jī)制有關(guān)的那個函數(shù)馏段。

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
                     LayoutState layoutState, LayoutChunkResult result) {
        //next方法很重要
        View view = layoutState.next(recycler);
        //執(zhí)行addView
        //執(zhí)行measureChild操作

這里再放上這行代碼轩拨,沒錯就是next函數(shù)。

View next(RecyclerView.Recycler recycler) {
            //默認(rèn)mScrapList=null院喜,但是執(zhí)行l(wèi)ayoutForPredictiveAnimations方法的時候不會為空
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            //重要亡蓉,從recycler獲得View,mScrapList是被LayoutManager持有,recycler是被RecyclerView持有
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }

而next函數(shù)這里也放了上來喷舀,其實可以看到砍濒,除了我們平常認(rèn)知的RecyclerView中Recycler的緩存,這里其實還存在一級的緩存mScrapList硫麻,mScrapList是被LayoutManager持有爸邢,recycler是被RecyclerView持有。但是mScrapList其實一定程度上和動畫有關(guān)拿愧,這里就不做分析了杠河,所以可以看到,緩存的重頭戲還是在RecyclerView中的內(nèi)部類Recycler中。這里先對Recycler這個內(nèi)部類大體了解一下感猛。

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;
    ...
    }
    類的結(jié)構(gòu)也比較清楚,這里可以清楚的看到我們后面講到的四級緩存機(jī)制所用到的類都在這里可以看到:
    * 1.一級緩存:mAttachedScrap
    * 2.二級緩存:mCacheViews
    * 3.三級緩存:mViewCacheExtension
    * 4.四級緩存:mRecyclerPool

繼續(xù)跟進(jìn)getViewForPosition方法奢赂,其實可以發(fā)現(xiàn)最后進(jìn)入的是tryGetViewHolderForPositionByDeadline方法陪白。

/**
 * Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
 * cache, the RecycledViewPool, or creating it directly.
 **/
    /**
     * 注釋寫的很清楚,從Recycler的scrap膳灶,cache咱士,RecyclerViewPool,或者直接create創(chuàng)建
     **/
    @Nullable
    ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                                                     boolean dryRun, long deadlineNs) {
        if (position < 0 || position >= mState.getItemCount()) {
            throw new IndexOutOfBoundsException("Invalid item position " + position
                    + "(" + position + "). Item count:" + mState.getItemCount()
                    + exceptionLabel());
        }
        boolean fromScrapOrHiddenOrCache = false;
        ViewHolder holder = null;
        // 0) If there is a changed scrap, try to find from there
        if (mState.isPreLayout()) {
            //preLayout默認(rèn)是false,只有有動畫的時候才為true
            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)) {
                    //如果檢查發(fā)現(xiàn)這個holder不是當(dāng)前position的
                    // 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);
                        //從scrap中移除
                        if (holder.isScrap()) {
                            removeDetachedView(holder.itemView, false);
                            holder.unScrap();
                        } else if (holder.wasReturnedFromScrap()) {
                            holder.clearReturnedFromScrapFlag();
                        }
                        //放到ViewCache或者Pool中
                        recycleViewHolderInternal(holder);
                    }
                    //至空繼續(xù)尋找
                    holder = null;
                } else {
                    fromScrapOrHiddenOrCache = true;
                }
            }
        }
        if (holder == null) {
            final int offsetPosition = mAdapterHelper.findPositionOffset(position);
            if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
                throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                        + "position " + position + "(offset:" + offsetPosition + ")."
                        + "state:" + mState.getItemCount() + exceptionLabel());
            }

            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;
                }
            }
            //自定義緩存
            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);
                if (view != null) {
                    holder = getChildViewHolder(view);
                    if (holder == null) {
                        throw new IllegalArgumentException("getViewForPositionAndType returned"
                                + " a view which does not have a ViewHolder"
                                + exceptionLabel());
                    } else if (holder.shouldIgnore()) {
                        throw new IllegalArgumentException("getViewForPositionAndType returned"
                                + " a view that is ignored. You must call stopIgnoring before"
                                + " returning this view." + exceptionLabel());
                    }
                }
            }
            //pool
            if (holder == null) { // fallback to pool
                if (DEBUG) {
                    Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                            + position + ") fetching from shared pool");
                }
                holder = getRecycledViewPool().getRecycledView(type);
                if (holder != null) {
                    holder.resetInternal();
                    if (FORCE_INVALIDATE_DISPLAY_LIST) {
                        invalidateDisplayListInt(holder);
                    }
                }
            }
            //create
            if (holder == null) {
                long start = getNanoTime();
                if (deadlineNs != FOREVER_NS
                        && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
                    // abort - we have a deadline we can't meet
                    return null;
                }
                holder = mAdapter.createViewHolder(RecyclerView.this, type);
                if (ALLOW_THREAD_GAP_WORK) {
                    // only bother finding nested RV if prefetching
                    RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                    if (innerView != null) {
                        holder.mNestedRecyclerView = new WeakReference<>(innerView);
                    }
                }

                long end = getNanoTime();
                mRecyclerPool.factorInCreateTime(type, end - start);
            }
        }
        ....
        return holder;
    }

刪除了得到holder后的代碼(其實還想再刪點的...)轧钓,本篇博客主要是對緩存機(jī)制源碼的分析序厉。對于源碼的一個方法第一眼先看一下方法的注釋,這里專門把方法的注釋放了上來毕箍,可以發(fā)現(xiàn)注釋寫的很清楚從Recycler的scrap弛房,cache,RecyclerViewPool,或者直接create創(chuàng)建而柑,這可以說是對RecyclerView緩存流程的概述:四級緩存(不知道為什么官方的注釋沒有寫上自定義緩存...)文捶。接下來就一級一級分析吧。

 if (mState.isPreLayout()) {
            //preLayout默認(rèn)是false媒咳,只有有動畫的時候才為true
            holder = getChangedScrapViewForPosition(position);
            fromScrapOrHiddenOrCache = holder != null;
        }

首先可以看到這里有個判斷粹排,當(dāng)為true的時候也可以拿到holder,但是這里我們沒有并到常規(guī)緩存里面涩澡,首先可以看一下判斷條件是對mInPreLayout變量的判斷顽耳,mInPreLayout默認(rèn)是false,只有有動畫的時候才為true妙同。其次對于getChangedScrapViewForPosition方法射富,其實是從Recycler類中的mChangedScrap獲取ViewHolder,這也是為什么我們剛才沒有將mChangedScrap放到常規(guī)緩存里面渐溶。

第一次嘗試(從mAttachedScrap和mCacheView中)

if (holder == null) {
            holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
            if (holder != null) {
                if (!validateViewHolderForOffsetPosition(holder)) {
                    //如果檢查發(fā)現(xiàn)這個holder不是當(dāng)前position的
            ...
                        //從scrap中移除
                        if (holder.isScrap()) {
                            removeDetachedView(holder.itemView, false);
                            holder.unScrap();
                        } else if (holder.wasReturnedFromScrap()) {
                            ...
                        }
                        //放到ViewCache或者Pool中
                        recycleViewHolderInternal(holder);
                    }
                    //至空繼續(xù)尋找
                    holder = null;
                } else {
                    fromScrapOrHiddenOrCache = true;
                }
            }
        }

先大體看一下第一級緩存辉浦,可以看到,這里通過getScrapOrHiddenOrCachedHolderForPosition方法來獲取ViewHolder茎辐,并檢驗holder的有效性宪郊,如果無效,則從mAttachedScrap中移除拖陆,并加入到mCacheViews或者Pool中弛槐,并且將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.
        //先從scrap中尋找
        for (int i = 0; i < scrapCount; i++) {
            final ViewHolder holder = mAttachedScrap.get(i);
            return holder;
           ...
        }
        //dryRun為false
        if (!dryRun) {
            //從HiddenView中獲得乎串,這里獲得是View
            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.
                //通過View的LayoutParam獲得ViewHolder
                final ViewHolder vh = getChildViewHolderInt(view);
                //從HiddenView中移除
                mChildHelper.unhide(view);
                ....
                mChildHelper.detachViewFromParent(layoutIndex);
                //添加到Scrap中,其實這里既然已經(jīng)拿到了ViewHolder速警,可以直接傳vh進(jìn)去
                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.
        //從CacheView中拿
        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
            //holder是有效的叹誉,并且position相同
            if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
                if (!dryRun) {
                    mCachedViews.remove(i);
                }
                return holder;
            }
        }
        return null;
    }

這里可以看到也分了三個步驟:

  • 1.從mAttachedScrap中獲取
  • 2.從HiddenView中獲取
  • 3.從CacheView獲取
    關(guān)鍵的代碼注釋我已經(jīng)放上了鸯两,流程上可以用下面這個圖來理解:


    第一次判斷

第二次嘗試(對應(yīng)hasStablelds情況)

if (holder == null) {
        ...
            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;
                }
            }
           ....
        }

這里首先先看一個重點:mAdapter.getItemViewType(offsetPosition);熟悉的方法有木有,可以看到這里調(diào)用了我們平常使用RecyclerView進(jìn)行多樣式item的方法长豁,也就是說前面對于一級緩存mAttachedScrap和mCacheViews是不區(qū)分type的钧唐,從現(xiàn)在開始的判斷是區(qū)分type的緩存。這里對于我們研究多type類型的RecyclerView很有幫助匠襟。
接下來的判斷可以看到很明顯這是對于我們重寫hasStableIds()方法為true的情況钝侠。

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--) {
            //在attachedScrap中尋找
            final ViewHolder holder = mAttachedScrap.get(i);
            if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
                //id相同并且不是從scrap中返回的
                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.
                    //從scrap中移除
                    mAttachedScrap.remove(i);
                    removeDetachedView(holder.itemView, false);
                    //加入cacheView或者pool
                    quickRecycleScrapView(holder.itemView);
                }
            }
        }
        //從cacheView中找
        // 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) {
                        //從cache中移除
                        mCachedViews.remove(i);
                    }
                    return holder;
                } else if (!dryRun) {
                    //從cacheView中移除,但是放到pool中
                    recycleCachedViewAt(i);
                    return null;
                }
            }
        }
        return null;
    }

可以看到這里的判斷其實和上面那一次差不多酸舍,需要注意的是多了對于id的判斷和對于type的判斷帅韧,也就是當(dāng)我們將hasStableIds()設(shè)為true后需要重寫holder.getItemId() 方法,來為每一個item設(shè)置一個單獨的id啃勉。具體流程圖如下:

第二次判斷

第三次嘗試(對應(yīng)于自定義緩存)

其實這種對于我們平常的使用來說已經(jīng)很陌生了忽舟,甚至很多人都不知道RecyclerView的這一項特性。

//自定義緩存
            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);
                if (view != null) {
                    holder = getChildViewHolder(view);
                    if (holder == null) {
                        throw new IllegalArgumentException("getViewForPositionAndType returned"
                                + " a view which does not have a ViewHolder"
                                + exceptionLabel());
                    } else if (holder.shouldIgnore()) {
                        throw new IllegalArgumentException("getViewForPositionAndType returned"
                                + " a view that is ignored. You must call stopIgnoring before"
                                + " returning this view." + exceptionLabel());
                    }
                }
            }

這里對于流程的理解沒有什么好說的璧亮,我們可以看一下這個自定義緩存的類ViewCacheExtension萧诫。

public abstract static class ViewCacheExtension {
    public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
}

可以看到這里類很恐怖,為什么這樣說哪枝嘶?

1.首先這個類基本上沒有什么限制帘饶,也就是說無論是緩存使用的數(shù)據(jù)結(jié)構(gòu)還有緩存算法(LRU還是什么)完全自定義,都由開發(fā)者自己決定群扶,這一點可以說既給了開發(fā)者很大的便利及刻,也給開發(fā)者帶來了很大的隱患。
2.對于平常的緩存竞阐,我們的理解在怎么說至少get-add|push-pop都是成對出現(xiàn)缴饭,為什么這樣說的,也就是緩存至少有進(jìn)也有出骆莹。而這里可以看到這里的抽象類只定義了出的方法颗搂,也就是只出不進(jìn),進(jìn)的時機(jī)幕垦,大小丢氢,時效等完全沒有規(guī)定。

第四次嘗試(對應(yīng)于Pool)

終于到了最后一次的嘗試先改,這個緩存是針對Pool的疚察,可以說RecyclerView內(nèi)部提供的Pool是RecyclerView的一大特性,這也是和ListView不同的地方仇奶,RecyclerView提供了這種緩存形式貌嫡,支持多個RecyclerView之間復(fù)用View,也就是說通過自定義Pool我們甚至可以實現(xiàn)整個應(yīng)用內(nèi)的RecyclerView的View的復(fù)用。

if (holder == null) { // fallback to pool
                ......
                holder = getRecycledViewPool().getRecycledView(type);
               ......
            }

同樣這里對于流程沒有什么好說的了岛抄,可以看一下RecyclerPool的類的結(jié)構(gòu)别惦。

public static class RecycledViewPool {
    private static final int DEFAULT_MAX_SCRAP = 5;
    static class ScrapData {
        ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }
    SparseArray<ScrapData> mScrap = new SparseArray<>();

    private int mAttachCount = 0;
    ...
    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;
    }
    ......
  }

可以看到RecyclerdViewPool內(nèi)部使用到了Google推薦的數(shù)據(jù)結(jié)構(gòu)類型SparseArray,而SparseArray內(nèi)部的key就是我們的ViewType夫椭,而value存放的是ArrayList<ViewHolder>步咪。而默認(rèn)的每個ArrayList<ViewHolder>的大小是5個。這里還有一個要注意的點就是getRecycledView這個方法可以看到拿到viewholder其實是通過remove拿到的益楼,也就是通過remove拿到的。

最終創(chuàng)建

//create
            if (holder == null) {
                ......
                holder = mAdapter.createViewHolder(RecyclerView.this, type);
                ......
            }

終于看到了我們經(jīng)常重寫的方法createViewHolder点晴,當(dāng)所有的的嘗試從緩存中獲取都失敗后感凤,只能調(diào)用我們自己重寫的createViewHolder方法,重新創(chuàng)建一個粒督。

總結(jié)

本篇博客主要從源碼的角度將RecyclerView內(nèi)部的緩存獲取的流程梳理了一遍陪竿,對于RecyclerView的緩存機(jī)制還遠(yuǎn)遠(yuǎn)不止如此,后面還會從別的角度學(xué)習(xí)RecyclerView的緩存機(jī)制屠橄。從這篇博客主要能看到以下幾點:

1.RecyclerView內(nèi)部大體可以分為四級緩存:mAttachedScrap,mCacheViews,ViewCacheExtension,RecycledViewPool.
2.mAttachedScrap,mCacheViews在第一次嘗試的時候只是對View的復(fù)用族跛,并且不區(qū)分type,但在第二次嘗試的時候是區(qū)分了Type锐墙,是對于ViewHolder的復(fù)用礁哄,ViewCacheExtension,RecycledViewPool是對于ViewHolder的復(fù)用,而且區(qū)分type溪北。
3.如果緩存ViewHolder時發(fā)現(xiàn)超過了mCachedView的限制桐绒,會將最老的ViewHolder(也就是mCachedView緩存隊列的第一個ViewHolder)移到RecycledViewPool中。

相關(guān)

基于AOP的RecyclerView復(fù)雜樓層樣式的開發(fā)框架之拨,樓層打通茉继,支持組件化,支持MVP(不用每次再寫Adapter了~)-EMvp
Star??支持一下~
歡迎提issues討論~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蚀乔,一起剝皮案震驚了整個濱河市烁竭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吉挣,老刑警劉巖派撕,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異听想,居然都是意外死亡腥刹,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進(jìn)店門汉买,熙熙樓的掌柜王于貴愁眉苦臉地迎上來衔峰,“玉大人,你說我怎么就攤上這事〉媛保” “怎么了威彰?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長穴肘。 經(jīng)常有香客問我歇盼,道長,這世上最難降的妖魔是什么评抚? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任豹缀,我火速辦了婚禮,結(jié)果婚禮上慨代,老公的妹妹穿的比我還像新娘邢笙。我一直安慰自己,他們只是感情好侍匙,可當(dāng)我...
    茶點故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布氮惯。 她就那樣靜靜地躺著,像睡著了一般想暗。 火紅的嫁衣襯著肌膚如雪妇汗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天说莫,我揣著相機(jī)與錄音杨箭,去河邊找鬼。 笑死储狭,一個胖子當(dāng)著我的面吹牛告唆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播晶密,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼擒悬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了稻艰?” 一聲冷哼從身側(cè)響起懂牧,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎尊勿,沒想到半個月后僧凤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡元扔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年躯保,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片澎语。...
    茶點故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡途事,死狀恐怖验懊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情尸变,我是刑警寧澤义图,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站召烂,受9級特大地震影響碱工,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜奏夫,卻給世界環(huán)境...
    茶點故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一怕篷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧酗昼,春花似錦匙头、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽舔示。三九已至碟婆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間惕稻,已是汗流浹背竖共。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留俺祠,地道東北人公给。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像蜘渣,于是被迫代替她去往敵國和親淌铐。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,630評論 2 359