RecyclerView的復用機制

RecyclerView的復用機制

概述

RecyclerView是Android業(yè)務開發(fā)非常常用的組件半开。我們知道它有復用操漠,并且設(shè)計優(yōu)雅烹笔。可能看過源碼的同學還知道澄耍,它有幾層復用噪珊。
但看網(wǎng)上的博客會發(fā)現(xiàn),大多只是照著源碼看一遍齐莲,并不會仔細地分析和推敲痢站,RecyclerView為什么要設(shè)計這一層緩存,每一層緩存在什么情景下使用选酗,以及每一層緩存的設(shè)置阵难,對RecyclerView運行真正的影響。
所以星掰,筆者試圖通過本文多望,講清楚以下幾個問題:

  • RecyclerView 緩存原理
  • RecyclerView 每層緩存的作用及參數(shù)影響
  • 我們應該如何使用 RecyclerView

RecyclerView的緩存原理

RecyclerView的復用啟動嫩舟,取決于LayoutManager氢烘。不同的LayoutManageronLayoutChildren中有不同的實現(xiàn)怀偷,但它們都一定會調(diào)用一個方法。那就是getViewForPosition播玖,所以椎工,我們就從getViewForPosition開始講起。

        /**
         * Obtain a view initialized for the given position.
         *
         * This method should be used by {@link LayoutManager} implementations to obtain
         * views to represent data from an {@link Adapter}.
         * <p>
         * The Recycler may reuse a scrap or detached view from a shared pool if one is
         * available for the correct view type. If the adapter has not indicated that the
         * data at the given position has changed, the Recycler will attempt to hand back
         * a scrap view that was previously initialized for that data without rebinding.
         *
         * @param position Position to obtain a view for
         * @return A view representing the data at <code>position</code> from <code>adapter</code>
         */
        @NonNull
        public View getViewForPosition(int position) {
            return getViewForPosition(position, false);
        }

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

這個方法就是根據(jù)position去返回View的蜀踏,根據(jù)不同情況维蒙,可能從share pool里面取,可能從scrap view中取果覆,總而言之颅痊,就是盡量低成本地去獲取一個可用的View。

mAttachedScrap & mChangedScrap

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

這一層的代碼很簡單局待,就是從mChangedScrap中取ViewHolder斑响。首先我們要明白,mChangedScrap里面放的是什么钳榨。

我們可以注意到這一層緩存的判斷條件舰罚,isPrelayout。只有當我們需要用動畫改變屏幕上已有ViewHolder時薛耻,會通過這個條件营罢。并且此時在真正發(fā)生改變之前。

mChangedScrap 表示的是數(shù)據(jù)已經(jīng)改變的但還在屏幕中的ViewHolder列表饼齿。所以在改動之前饲漾,我們會從中獲取ViewHolder。

        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {

            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder 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;
                    }
                }
            }
            ...

我們可以看到這個方法中缕溉,我們會從mAttachedScrap尋找合適的ViewHolder考传。

mAttachedScrap 表示屏幕內(nèi)未與RecyclerView分離的ViewHolder列表。值得注意的是mAttachedScrap是不限制大小的倒淫。想一想也很容易明白伙菊,屏幕中顯示多少ViewHolder,是無法確定的敌土。并且ViewHolder既然都已經(jīng)顯示了镜硕,mAttachedScrap也不會造成額外的內(nèi)存占用。

通常我們把mChangedScrapmAttachedScrap稱為RecyclerView的第一級緩存返干,它們的共同特點就是兴枯,只緩存屏幕上的View,且沒有大小限制矩欠。

mCachedViews

mCachedViews是RecyclerView第二層緩存财剖。

當列表滑動出了屏幕時悠夯,ViewHolder會被緩存在 mCachedViews ,其大小由mViewCacheMax決定躺坟,默認DEFAULT_CACHE_SIZE為2沦补,可通過Recyclerview.setItemViewCacheSize()動態(tài)設(shè)置。

我們來看一下從mCachedViews中獲取ViewHolder的代碼:

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

值得注意的是咪橙,holder.getLayoutPosition() == position夕膀,所以我們在mCachedViews中存的ViewHolder,雖然是屏幕外的美侦,但只能是對應position的产舞。也就是說,只能是RecyclerView的ViewHolder被滑出屏幕后菠剩,再滑回來顯示的情景易猫。

也不難看出,從mCachedViews中具壮,我們?nèi)〉玫腣iewHolder是不需要重新綁定數(shù)據(jù)的准颓。

那么,我們的ViewHolder是何時被加入mCachedViews中的呢嘴办?

            if (forceRecycle || holder.isRecyclable()) {
                if (mViewCacheMax > 0
                        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                        | ViewHolder.FLAG_REMOVED
                        | ViewHolder.FLAG_UPDATE
                        | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                    // Retire oldest cached view
                    int cachedViewSize = mCachedViews.size();
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }

                    int targetCacheIndex = cachedViewSize;
                    if (ALLOW_THREAD_GAP_WORK
                            && cachedViewSize > 0
                            && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                        // when adding the view, skip past most recently prefetched views
                        int cacheIndex = cachedViewSize - 1;
                        while (cacheIndex >= 0) {
                            int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                            if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                                break;
                            }
                            cacheIndex--;
                        }
                        targetCacheIndex = cacheIndex + 1;
                    }
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
                if (!cached) {
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            }

很容易看出瞬场,當我們將ViewHolder滑出屏幕時,我們會嘗試回收ViewHolder涧郊,將其放入mCachedViews中贯被。如果mCachedViews已滿,我們會將其中的第0個拿出來妆艘,放到mRecyclerPool中彤灶。

mRecyclerPoolmCachedViews最大的不同是,從mCachedViews中取出的ViewHolder是不需要重新bind數(shù)據(jù)的批旺。

我們可以通過以下方法來設(shè)置mCacheViews的最大值幌陕。

        /**
         * Set the maximum number of detached, valid views we should retain for later use.
         *
         * @param viewCount Number of views to keep before sending views to the shared pool
         */
        public void setViewCacheSize(int viewCount) {
            mRequestedCacheMax = viewCount;
            updateViewCacheSize();
        }

很明顯,這是一個空間換時間的設(shè)置項汽煮。我們增大mRequestedCacheMax搏熄,可以在展示已經(jīng)展示過的ViewHolder時,減少bind的次數(shù)暇赤,但需要緩存更多的ViewHolder心例。

mViewCacheExtension

mViewCacheExtension是RecyclerView的第三層緩存。當我們在mAttachedScrap & mChangedScrapmCachedViews中均未獲得ViewHolder時鞋囊,我們會嘗試從mViewCacheExtension中獲取View并創(chuàng)建ViewHolder止后。

                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 {

        /**
         * Returns a View that can be binded to the given Adapter position.
         * <p>
         * This method should <b>not</b> create a new View. Instead, it is expected to return
         * an already created View that can be re-used for the given type and position.
         * If the View is marked as ignored, it should first call
         * {@link LayoutManager#stopIgnoringView(View)} before returning the View.
         * <p>
         * RecyclerView will re-bind the returned View to the position if necessary.
         *
         * @param recycler The Recycler that can be used to bind the View
         * @param position The adapter position
         * @param type     The type of the View, defined by adapter
         * @return A View that is bound to the given position or NULL if there is no View to re-use
         * @see LayoutManager#ignoreView(View)
         */
        @Nullable
        public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position,
                int type);
    }

這一層看起來很簡單,就是RecyclerView為我們開發(fā)者在mCachedViewsRecycledViewPool中加了一層緩存。讓我們可以通過position和type返回一個View译株。然后RecyclerView幫我們找到View對應的ViewHolder瓜喇。這一層緩存的實現(xiàn)完全可以靠開發(fā)者的想象。

值得注意的是歉糜,這一層如果能成功獲得ViewHolder乘寒,也是不會綁定數(shù)據(jù)的。所以這一次緩存现恼,通常也用來獲取不可變的ViewHolder肃续。

RecycledViewPool

    /**
     * RecycledViewPool lets you share Views between multiple RecyclerViews.
     * <p>
     * If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool
     * and use {@link RecyclerView#setRecycledViewPool(RecycledViewPool)}.
     * <p>
     * RecyclerView automatically creates a pool for itself if you don't provide one.
     */
    public static class RecycledViewPool {
        private static final int DEFAULT_MAX_SCRAP = 5;

        /**
         * Tracks both pooled holders, as well as create/bind timing metadata for the given type.
         *
         * Note that this tracks running averages of create/bind time across all RecyclerViews
         * (and, indirectly, Adapters) that use this pool.
         *
         * 1) This enables us to track average create and bind times across multiple adapters. Even
         * though create (and especially bind) may behave differently for different Adapter
         * subclasses, sharing the pool is a strong signal that they'll perform similarly, per type.
         *
         * 2) If {@link #willBindInTime(int, long, long)} returns false for one view, it will return
         * false for all other views of its type for the same deadline. This prevents items
         * constructed by {@link GapWorker} prefetch from being bound to a lower priority prefetch.
         */
        static class ScrapData {
            final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }
        SparseArray<ScrapData> mScrap = new SparseArray<>();

RecycledViewPool的結(jié)構(gòu)非常清晰:

  • SparseArray<ScrapData> mScrap中存放在viewType對應的ScrapData黍檩。
  • ScrapData中叉袍,則是緩存的ViewHolder

獲取方法很簡單:

        /**
         * Acquire a ViewHolder of the specified type from the pool, or {@code null} if none are
         * present.
         *
         * @param viewType ViewHolder type.
         * @return ViewHolder of the specified type acquired from the pool, or {@code null} if none
         * are present.
         */
        @Nullable
        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;
        }

mScrap中找到對應ViewType的ScrapData刽酱,然后從隊尾拿走一個喳逛。

插入方法稍微復雜一點:

        /**
         * Add a scrap ViewHolder to the pool.
         * <p>
         * If the pool is already full for that ViewHolder's type, it will be immediately discarded.
         *
         * @param scrap ViewHolder to be added to the pool.
         */
        public void putRecycledView(ViewHolder scrap) {
            final int viewType = scrap.getItemViewType();
            final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
            if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
                return;
            }
            if (DEBUG && scrapHeap.contains(scrap)) {
                throw new IllegalArgumentException("this scrap item already exists");
            }
            scrap.resetInternal();
            scrapHeap.add(scrap);
        }

包含了一下最大值和重復插入的容錯。其中resetInternal方法棵里,則是會清除ViewHolder中的所有內(nèi)容润文。讓它成為一個干干凈凈的ViewHolder。

        void resetInternal() {
            mFlags = 0;
            mPosition = NO_POSITION;
            mOldPosition = NO_POSITION;
            mItemId = NO_ID;
            mPreLayoutPosition = NO_POSITION;
            mIsRecyclableCount = 0;
            mShadowedHolder = null;
            mShadowingHolder = null;
            clearPayload();
            mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
            mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
            clearNestedRecyclerViewIfNotNested(this);
        }

RecyclerView每層緩存的作用

整體來說RecyclerView的緩存可分為四層殿怜。每一層緩存的目的都不盡相同典蝌。當我們想要修改某一層緩存的配置,甚至重寫某一層緩存時头谜,我們需要慎重地考慮這一層緩存的作用骏掀,當我對它進行修改會帶來什么樣的后果。

  • mAttachedScrap和mChangedScrap,是緩存的屏幕上的可見內(nèi)容。它本身的大小是無限的傅联,因為屏幕上顯示多少item是無法限制的措嵌,這一層緩存并不會帶來額外的緩存。當我們改變它時暇唾,改變的是在屏幕內(nèi)的item,收到刷新通知時的行為。通常來說坡锡,這樣的需求是比較少的。
  • mCachedViews窒所,是緩存的屏幕外的內(nèi)容鹉勒。mCachedViews中的緩存是攜帶了ViewHolder的數(shù)據(jù)的。也就是說墩新,它只能緩存已經(jīng)顯示過的ViewHolder贸弥。顯而易見,它的主要作用是讓已經(jīng)顯示過的ViewHolder需要再次顯示時海渊,能夠快速顯示绵疲。RecyclerView中哲鸳,mCachedViews的默認大小為2 。但mCachedViews我們是可以修改的盔憨,緩存的越多徙菠,用戶回看時就越快,同時消耗的內(nèi)存也越多郁岩。這是一個內(nèi)存和時間置換的配置婿奔。當我們內(nèi)存充裕,或者顯示的item比較小時问慎,可以考慮適當?shù)胤糯筮@個配置萍摊,來增加回看的流暢性。
  • mViewCahceExtension如叼,是一層自定義緩存冰木,位于mCacheViews之后,RecycledViewPool之前笼恰。首先踊沸,我們要明確,mViewCahceExtension還是緩存的帶數(shù)據(jù)的ViewHolder社证,所以逼龟,它本質(zhì)上和mCachedViews一樣,是提升回看性能的追葡。 所以我們通常用它來提升某個特定position的ItemView的回看性能腺律。比如,我們有某個ItemView辽俗,界面構(gòu)建很廢時疾渣,處在RecyclerView的固定位置中,且界面不需要刷新崖飘。這樣的ItemView在內(nèi)存允許的情況下榴捡,我們建議在mViewCacheExtension中單獨緩存。它不會因為mCachedViews中緩存到上限被回收朱浴,回看時也不需要重新構(gòu)建View吊圾。
  • RecycledViewPool,是RecyclerView緩存的最后一層翰蠢。當我們在上面三層緩存都沒取到時项乒,才會用到RecycledViewPool。RecycledViewPool也是唯一可以用于尚未展示過的ItemView的一層緩存梁沧。RecycledViewPool中存放的都是被清除了數(shù)據(jù)的ViewHolder檀何。也就是說,它保持著onCreateView后ViewHolder最初的狀態(tài)。當我們要使用ViewHolder時频鉴,就從RecycledViewPool中栓辜,拿出對應ViewType的ViewHolder,然后綁上數(shù)據(jù)垛孔,刷新界面藕甩。我們從它的結(jié)構(gòu)可以看出,RecycledViewPool幾乎是和RecycerView解耦的周荐,它只與ViewHolder有關(guān)狭莱,和position、數(shù)據(jù)一概沒有關(guān)系概作。所以腋妙,我們甚至可以讓多個RecyclerView共用一個RecycledViewPool,以此來優(yōu)化內(nèi)存仆嗦。

如何使用RecyclerView

RecyclerView除了基本的onCreateViewHolder和onBindViewHolder外辉阶,會有很多工具和配置來提升性能。這些工具和配置為什么需要開發(fā)單獨配置呢瘩扼?因為它們只在特定的場景下有效。所以作為開發(fā)者垃僚,需要了解它集绰,然后在合適的場景使用合適的配置,來提升我們RecyclerView的性能谆棺。

DiffUtil

是一個對比新老數(shù)據(jù)的不同工具類栽燕,幫助我們尋找新老數(shù)據(jù)的最小差異,而不用全量更新改淑。同時碍岔,DiffUtil可以幫助我們子線程更新。這里就不展開朵夏,DiffUtil能提供的功能很多蔼啦。

setHasFixedSize

如果在提前確定RecyclerView Item的寬高不會受數(shù)據(jù)影響時,就可以通過setHasFixedSize為設(shè)置true仰猖,來優(yōu)化RecyclerView的刷新性能捏肢。但是,notifyDataSetChanged調(diào)用后饥侵,item的大小還是會重新計算鸵赫。

看源碼會發(fā)現(xiàn),只有在調(diào)用以下四個方法時躏升,會省去item的大小計算:

onItemRangeChanged(),

onItemRangeInserted(),

onItemRangeRemoved(),

onItemRangeMoved(),

notifyDataSetChanged被調(diào)用時辩棒,一定會調(diào)用requestLayout(),從而重新測量寬高。

共用RecycledViewPool

上面講緩存時一睁,我們看到藕赞,RecyclerView的最后一層緩存就是RecycledViewPool。這一層緩存儲存著清空了數(shù)據(jù)的ViewHolder卖局。既然如此斧蜕,當我們頁面上有多個RecyclerView時,我們是否可以共用RecycledViewPool砚偶?答案是可以的批销。

我們可以給多個RecyclerView調(diào)用setRecycledViewPool設(shè)置相同的RecycledViewPool,達到緩存共用的目的染坯。

setRecycleChildrenOnDetach

    /**
     * Set whether LayoutManager will recycle its children when it is detached from
     * RecyclerView.
     * <p>
     * If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set
     * this flag to <code>true</code> so that views will be available to other RecyclerViews
     * immediately.
     * <p>
     * Note that, setting this flag will result in a performance drop if RecyclerView
     * is restored.
     *
     * @param recycleChildrenOnDetach Whether children should be recycled in detach or not.
     */
    public void setRecycleChildrenOnDetach(boolean recycleChildrenOnDetach) {
        mRecycleChildrenOnDetach = recycleChildrenOnDetach;
    }

注釋寫得非常清晰了均芽,會在detached時決定是否要回收ViewHolder。這個主要運用在我們多個RecyclerView共用一個RecycledViewPool時单鹿。在RecyclerView從頁面中消失時掀宋,我們可以清空它的ViewHolder到RecycledViewPool中,為我們其他RecyclerView提供更多的緩存仲锄。

setHasStableIds

setHasStableIds 保證相同id下數(shù)據(jù)不會變化劲妙。這樣,當我們刷新數(shù)據(jù)時儒喊,RecyclerView就能確認是否數(shù)據(jù)沒有變化镣奋,ViewHolder也直接復用,減少重新布局的煩惱怀愧。同時侨颈,由于ViewHolder沒有變動,可以去掉動畫芯义。

但這個使用的前提是數(shù)據(jù)的id一定是唯一的哈垢。如果id不變,但數(shù)據(jù)發(fā)生變化扛拨,可能就不會刷新了耘分。

onViewRecycled

當 ViewHolder 已經(jīng)確認被回收,且要放進 RecyclerViewPool 中前鬼癣,該方法會被回調(diào)陶贼。值得注意的是,這里并不是Item一離開屏幕就會調(diào)用待秃,而是等前面幾級緩存都填滿時拜秧,將要放進RecyclerViewPool時,才會調(diào)用此方法章郁。一旦ViewHolder放入RecyclerViewPool后枉氮,數(shù)據(jù)就會被清空了志衍。

我們可以在這個時間點,做一些內(nèi)存釋放的工作聊替,幫助App減小內(nèi)存壓力楼肪。

總結(jié)

以上就是RecyclerView的緩存原理與常見優(yōu)化。我們在學習時惹悄,需要時常思考春叫,這一切都是為了什么?為什么RecyclerView要設(shè)計這四級緩存泣港,每一級起到了什么作用暂殖。然后那些優(yōu)化方式,為什么要當作外部API來提供当纱,而不是直接在內(nèi)部幫我們優(yōu)化呛每,限制又是什么?只有這樣坡氯,我們才能感受到RecyclerView的巧妙設(shè)計晨横,也為我們后續(xù)自己工作中的設(shè)計提供思路。

技術(shù)嘛箫柳,需要知其然手形,知其所以然。

?著作權(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é)果婚禮上停做,老公的妹妹穿的比我還像新娘。我一直安慰自己大莫,他們只是感情好蛉腌,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著只厘,像睡著了一般烙丛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上懈凹,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天蜀变,我揣著相機與錄音,去河邊找鬼介评。 笑死库北,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的们陆。 我是一名探鬼主播寒瓦,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼坪仇!你這毒婦竟也來了杂腰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤椅文,失蹤者是張志新(化名)和其女友劉穎喂很,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體皆刺,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡少辣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了羡蛾。 大學時的朋友給我發(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
  • 正文 我出身青樓,卻偏偏與公主長得像尖殃,于是被迫代替她去往敵國和親丈莺。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344