ListView的緩存是存在哪

我們都知道ListView的baseAdapter中,使用了一個view的緩存回收機(jī)制,我們經(jīng)常被告知會把不可見的view緩存起來,并且在新的view顯示時會重用之前回收的view,實(shí)際中在開發(fā)時會使用convertView去進(jìn)行相關(guān)處理。那么我們不禁會好奇,這個緩存回收機(jī)制到底是怎么實(shí)現(xiàn)的?我們不該僅僅會使用BaseAdapter重寫各個方法就夠了,我們需要往深處去挖點(diǎn)寶藏。今天我們就來看看listView和adapter在視圖回收和緩存環(huán)節(jié)是怎么做到的。

AbsListView

ListView是一個繼承自AbsListView的類,要想深入這一部分,我們需要看看AbsListView的源碼。AbsListView還是比較復(fù)雜的,但是我們可以在ListView的scrollListItemsBy,layoutChildren等方法中看到幾個叫mRecycler 和recycleBin的對象,看命名似乎是和視圖回收機(jī)制重用等有關(guān),mRecycler就是來自AbsListView的,我們可以繼續(xù)看下去。

/**
* The data set used to store unused views that should be reused during the next layout
* to avoid creating new ones
*/
final RecycleBin mRecycler = new RecycleBin();

涉及到這個功能,recycleBin是一個RecycleBin對象,RecycleBin是AbsListView的內(nèi)部類。那我們就來研究一下RecycleBin這個類。
我們先看看這個注釋。

/**
* The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of
* storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the
* start of a layout. By construction, they are displaying current information. At the end of
* layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that
* could potentially be used by the adapter to avoid allocating views unnecessarily.
*/

大意就是RecycleBin實(shí)現(xiàn)了布局中view的重用。RecycleBin有兩個層級的存儲。

  1. ActiveViews , 布局開始時要在屏幕中顯示的view
  2. ScrapViews, 布局結(jié)束后所有的ActiveViews就降級為ScrapViews。ScrapViews就是舊view,主要是可能被adapter為了避免不必要的視圖分配空間而重用。
    好了,RecycleBin的結(jié)構(gòu)我們搞懂了,那么關(guān)于ListView的核心問題就變成了RecycleBin是怎么運(yùn)用ActiveViews和ScrapViews的了。換句話說就是ActiveViews和ScrapViews是怎么產(chǎn)生、怎么添加、怎么交換的?

嗯,繼續(xù)看源碼。
private View[] mActiveViews = new View[0];
private ArrayList<View>[] mScrapViews;

mActiveViews就是一個ActiveViews堆,可以看到mActiveViews是一個View數(shù)組。
mScrapViews是一個ScrapViews堆,是一個ArrayList<View>數(shù)組。這里為什么要這樣設(shè)計(jì)存儲?我們先賣個關(guān)子。
在研究這兩個不同層級的View堆前,我們先看看在ListView中怎么使用RecycleBin的。

  1. setAdapter方法中,mRecycler.clear();
  2. setAdapter方法中,設(shè)計(jì)到了viewType的操作,因?yàn)闀胁煌囊晥D結(jié)構(gòu),mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
  3. onMeasure方法中,
if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(((LayoutParams) child.getLayoutParams()).viewType)) {
    mRecycler.addScrapView(child, 0);
}
  1. makeAndAddView方法中,mRecycler.getActiveView(position);
  2. layoutChildren方法中,
    final RecycleBin recycleBin = mRecycler;
    if (dataChanged) {
        for (int i = 0; i < childCount; i++) {
            recycleBin.addScrapView(getChildAt(i), firstPosition+i);
        }
    } else {
        recycleBin.fillActiveViews(childCount, firstPosition);
    }

    // Clear out old views
    detachAllViewsFromParent();
    recycleBin.removeSkippedScrap();
    ……
    // Flush any cached views that did not get reused above
    recycleBin.scrapActiveViews();

其中在layoutChildren中的用法特別典型,我們具體來看一看。
以上的這段代碼可以大概看出一些邏輯思路:

  1. 先判斷數(shù)據(jù)是否有改變,如果改變了就將當(dāng)前的children加到ScrapViews中,否則加到ActiveViews中。
  2. removeSkippedScrap,把舊的view都刪掉。
  3. 最后將以上沒有被重用的緩存的view都回收掉。將當(dāng)前的ActiveVies 移動到 ScrapViews。

以上是我們通過這一段代碼的一個猜測分析,現(xiàn)在一步步看看源碼。
dataChanged是一個AdapterView的boolean變量。
其中ListView 繼承自AbsListView, AbsListView 繼承自AdapterView,AdapterView繼承自ViewGroup.
對dataChanged的賦值主要是在AdapterView中的內(nèi)部類AdapterDataSetObserver中進(jìn)行的。我們知道listView的adapter使用了觀察者模式。這個是怎么做到的?
我們先看看AdapterDataSetObserver的源碼:

    class AdapterDataSetObserver extends DataSetObserver {

        private Parcelable mInstanceState = null;

        @Override
        public void onChanged() {
            mDataChanged = true;
            mOldItemCount = mItemCount;
            mItemCount = getAdapter().getCount();

            // Detect the case where a cursor that was previously invalidated has
            // been repopulated with new data.
            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
                    && mOldItemCount == 0 && mItemCount > 0) {
                AdapterView.this.onRestoreInstanceState(mInstanceState);
                mInstanceState = null;
            } else {
                rememberSyncState();
            }
            checkFocus();
            requestLayout();
        }

        @Override
        public void onInvalidated() {
            mDataChanged = true;

            if (AdapterView.this.getAdapter().hasStableIds()) {
                // Remember the current state for the case where our hosting activity is being
                // stopped and later restarted
                mInstanceState = AdapterView.this.onSaveInstanceState();
            }

            // Data is invalid so we should reset our state
            mOldItemCount = mItemCount;
            mItemCount = 0;
            mSelectedPosition = INVALID_POSITION;
            mSelectedRowId = INVALID_ROW_ID;
            mNextSelectedPosition = INVALID_POSITION;
            mNextSelectedRowId = INVALID_ROW_ID;
            mNeedSync = false;

            checkFocus();
            requestLayout();
        }

        public void clearSavedState() {
            mInstanceState = null;
        }
    }

代碼很簡單,繼承自DataSetObserver,重寫了onChanged和onInvalidated兩個方法,對mDataChanged的操作都是在數(shù)據(jù)發(fā)生改變后將mDataChanged設(shè)為true,那么在哪里會變成false呢?AdapterView中已經(jīng)沒有了。我們還需要回到ListView中繼續(xù)看。

在ListView中搜索這個變量會發(fā)現(xiàn),將其變?yōu)閒alse還是同樣的在layoutChildren中,并且是完成了對mActiveViews和mScrapViews的各種操作之后才變?yōu)閒alse。
并且在makeAndView中使用了false時的值。

    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) {
        View child;

        if (!mDataChanged) {
            // Try to use an existing view for this position
            child = mRecycler.getActiveView(position);
            if (child != null) {
                // Found it -- we're using an existing child
                // This just needs to be positioned
                setupChild(child, position, y, flow, childrenLeft, selected, true);

                return child;
            }
        }

        // Make a new view for this position, or convert an unused view if possible
        child = obtainView(position, mIsScrap);
        // This needs to be positioned and measured
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
        return child;
    }

makeAndAddView能夠獲取一個view并且把它添加到child的list中,并且返回了這個child,這個child可以是新view,也可以是沒有使用過的view
convert過來的,或者說是從緩存中重用的view。
這當(dāng)中有一個getActiveView方法,就是我們在之前提到的RecycleBin的第四個用法,也是在mDataChanged為false時一個處理方法。
好了,對mDataChanged的分析和“尋找”先到這里,我們接著看看RecycleBin的用法。

既然是我們之前講過的處理流程,我們先看看最初的一個RecycleBin使用情況。

void addScrapView(View scrap, int position) {
    final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
    if (lp == null) {
        return;
    }

    lp.scrappedFromPosition = position;
    ......

    //當(dāng)一個view有瞬態(tài)時不用被廢棄
    final boolean scrapHasTransientState = scrap.hasTransientState();
    if (scrapHasTransientState) {
        if (mAdapter != null && mAdapterHasStableIds) {
            // 如果adapter有穩(wěn)定的ids,那就能對相同的數(shù)據(jù)進(jìn)行view的重用
            if (mTransientStateViewsById == null) {
                mTransientStateViewsById = new LongSparseArray<View>();
            }
            mTransientStateViewsById.put(lp.itemId, scrap);
        } else if (!mDataChanged) {
            // 如果綁定的數(shù)據(jù)沒有改變,就能在舊位置重用view
            if (mTransientStateViews == null) {
                mTransientStateViews = new SparseArray<View>();
            }
            mTransientStateViews.put(position, scrap);
        } else {
            // 其他情況只能移除view并且從頭來過
            if (mSkippedScrap == null) {
                mSkippedScrap = new ArrayList<View>();
            }
            mSkippedScrap.add(scrap);
        }
    }else{
        if (mViewTypeCount == 1) {
            //這里的mCurrentScrap就是mScrapViews[0]
            mCurrentScrap.add(scrap);
        } else {
            mScrapViews[viewType].add(scrap);
        }

        if (mRecyclerListener != null) {
            mRecyclerListener.onMovedToScrapHeap(scrap);
        }
    }
}

我們可以看到,這里的邏輯還算很清晰,在關(guān)于view的重用的判斷時,涉及到一個概念還是需要解釋一下,就是view的瞬態(tài)。
View.hasTransientState()的代碼就不貼了,主要是幾個flag的運(yùn)算,雖然就一行,但是需要前后聯(lián)系,感興趣的朋友可以去看看源碼。
我們這里主要理解瞬態(tài),當(dāng)我們說一個view有瞬態(tài)時,我們指app無需再關(guān)心這個view的保存與恢復(fù),注釋指出一般用來播放動畫或者記錄選擇的位置等相似的行為。
當(dāng)一個view標(biāo)記位有瞬態(tài)時,在RecycleBin中,就有可能不會降級到ScrapView,而是mTransientStateViews或者mTransientStateViewsById將其保存起來,以便于
后來的重用。

private SparseArray<View> mTransientStateViews;
private LongSparseArray<View> mTransientStateViewsById;

在RecycleBin中涉及到瞬態(tài)的存儲結(jié)構(gòu)是上述代碼中的兩個,其實(shí)就是SparseArray,一個存的是position的key,一個存的是id。

這里介紹了mTransientStateViews的寫,我們再看看mTransientStateViews的讀。這里的讀也是讀的SparseArray,雖然功能類似于HashMap,但是讀數(shù)據(jù)時使用
的是valueAt方法。

View getTransientStateView(int position) {
    if (mAdapter != null && mAdapterHasStableIds && mTransientStateViewsById != null) {
        long id = mAdapter.getItemId(position);
        View result = mTransientStateViewsById.get(id);
        mTransientStateViewsById.remove(id);
        return result;
    }
    if (mTransientStateViews != null) {
        final int index = mTransientStateViews.indexOfKey(position);
        if (index >= 0) {
            View result = mTransientStateViews.valueAt(index);
            mTransientStateViews.removeAt(index);
            return result;
        }
    }
    return null;
}

上面這份代碼就是用來獲取瞬時態(tài)的view的,首先從mTransientStateViewsById中讀取,如果沒有就從mTransientStateViews中讀取。
這個方法是屬于RecycleBin的,但是這個是在哪里調(diào)用的呢?答案是AbsListView的obtainView方法。

這里是一個比較關(guān)鍵的地方了。

    View obtainView(int position, boolean[] isScrap) {
        ......

        final View transientView = mRecycler.getTransientStateView(position);
        if (transientView != null) {
            final LayoutParams params = (LayoutParams) transientView.getLayoutParams();

            // If the view type hasn't changed, attempt to re-bind the data.
            if (params.viewType == mAdapter.getItemViewType(position)) {
                final View updatedView = mAdapter.getView(position, transientView, this);

                // 重新綁定數(shù)據(jù)失敗,就廢棄獲取到的view
                if (updatedView != transientView) {
                    setItemViewLayoutParams(updatedView, position);
                    mRecycler.addScrapView(updatedView, position);
                }
            }
            ......
            return transientView;
        }

        final View scrapView = mRecycler.getScrapView(position);
        final View child = mAdapter.getView(position, scrapView, this);
        if (scrapView != null) {
            if (child != scrapView) {
                // Failed to re-bind the data, return scrap to the heap.
                mRecycler.addScrapView(scrapView, position);
            } else {
                isScrap[0] = true;

                child.dispatchFinishTemporaryDetach();
            }
        }

        ......

        setItemViewLayoutParams(child, position);

        ......

        return child;
    }

閱讀代碼可以發(fā)現(xiàn),在obtainView中,最重要的一個方法就是調(diào)用了adapter的getView方法,這個方法也是我們平常重寫的方法,getView返回的是一個view。
而在對這個view的獲取過程,有一個很明顯的兩層處理,首先就是嘗試獲取我們之前分析介紹的transientView,也就是擁有瞬時態(tài)的view,通過transientView
以完成復(fù)用。如果這一步走的失敗了,也就是transientView為null時,或者說這個view并不擁有瞬時態(tài),那么就從ScrapView中獲取一個scrapView,這里可能要對
scrapView多做一些處理,我在上面都省略了,不影響邏輯大局,感興趣的朋友可以去看看源碼。最后返回的是scrapView。

getTransientStateView我們已經(jīng)介紹過了,現(xiàn)在來看看失敗之后的getScrapView。

View getScrapView(int position) {
    if (mViewTypeCount == 1) {
        return retrieveFromScrap(mCurrentScrap, position);
    } else {
        final int whichScrap = mAdapter.getItemViewType(position);
        if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
            return retrieveFromScrap(mScrapViews[whichScrap], position);
        }
    }
    return null;
}

private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
    final int size = scrapViews.size();
    if (size > 0) {
        // 檢查對某一個position或者id是否還有一個對應(yīng)的view
        for (int i = 0; i < size; i++) {
            final View view = scrapViews.get(i);
            final AbsListView.LayoutParams params = (AbsListView.LayoutParams) view.getLayoutParams();

            if (mAdapterHasStableIds) {
                final long id = mAdapter.getItemId(position);
                if (id == params.itemId) {
                    return scrapViews.remove(i);
                }
            } else if (params.scrappedFromPosition == position) {
                final View scrap = scrapViews.remove(i);
                clearAccessibilityFromScrap(scrap);
                return scrap;
            }
        }
        final View scrap = scrapViews.remove(size - 1);
        clearAccessibilityFromScrap(scrap);
        return scrap;
    } else {
        return null;
    }
}

上面這份代碼還是寫的很清楚了,根據(jù)不同的viewType來進(jìn)行不同策略的取scrapView,但是實(shí)質(zhì)上都是走到了retrieveFromScrap。
思路還是比較清晰,但是需要注意的是在get到scrapView后,在scrapView堆中這個view就會被移除掉。以便以后不停的循環(huán)往復(fù)的重用。

以上的分析都是在layoutChildren中判斷數(shù)據(jù)改變后的過程,當(dāng)數(shù)據(jù)沒有改變時,會走到fillActiveViews方法。這個方法能夠?qū)bsListView的所有子view都
裝到activeViews中。代碼很簡單,就是一個for循環(huán),將listView的所有子view一一存到activeViews中。

接著我們最初的分析,這些都走完之后,會執(zhí)行recycleBin的removeSkippedScrap方法。還記得我們介紹的addScrapView方法嗎,當(dāng)一個view是有瞬時態(tài)的,但是
卻沒有一個保存到mTransientStateViewsById或者mTransientStateViews中時,會存在mSkippedScrap中。這是都會通過removeSkippedScrap全部清空。

在layoutChildren中,涉及到RecycleBin的最后還有一個方法就是scrapActiveViews。因?yàn)橐呀?jīng)完成了children的布局layout位置的擺放等,所以這個時候需要刷新
緩存,scrapActiveViews這部分代碼可能處理的過程有點(diǎn)多,但是最重要的一件事就是將現(xiàn)在mActiveViews還剩下的views都會移到mScrapViews中。

這個遷移過程也是和addScrapView的過程差不多。一開始是先對mActiveViews遍歷,每次保存當(dāng)前結(jié)點(diǎn)victim,并將mActiveViews[i]置空,然后對victim進(jìn)行遷移
操作,如果符合條件就加到mTransientStateViewsById或者mTransientStateViews中,否則就加到mScrapViews中。

好了,至此在addScrapView中對RecycleBin進(jìn)行的一系列操作就講完了,我們看看下一步是什么。

ListView的makeAndAddView方法。
makeAndAddView的代碼我們之前也貼過了,當(dāng)判斷數(shù)據(jù)沒有發(fā)生改變時,會走到RecycleBin的getActiveView方法。
那我們來看看getActiveView。

View getActiveView(int position) {
    int index = position - mFirstActivePosition;
    final View[] activeViews = mActiveViews;
    if (index >=0 && index < activeViews.length) {
        final View match = activeViews[index];
        activeViews[index] = null;
        return match;
    }
    return null;
}

getActiveView的代碼就很簡單了,根據(jù)丟過來的position計(jì)算出一個真實(shí)有效的index,然后從activeViews中獲取相應(yīng)的view。沒什么可講的。

ok,關(guān)于RecycleBin的幾個主要方法和執(zhí)行流程就介紹完了。

我們可以暫時回顧小結(jié)一下,實(shí)際上RecycleBin的主要結(jié)構(gòu)就是三個,一個是activeView堆,結(jié)構(gòu)是一個View數(shù)組,另一個是scrapView堆,結(jié)構(gòu)是一個
ArrayList<View>數(shù)組。還有一個是transientViews,結(jié)構(gòu)是SparseArray,主要通過Id或者position存取。
幾個結(jié)構(gòu)可以理解為層級不同,activeView比scrapView高一點(diǎn),當(dāng)觸發(fā)了某種條件或者機(jī)制后,child的view就會從activieView中移到transientViews或者scrapView中進(jìn)行緩存。
當(dāng)ListView需要obtainView時,會先從有瞬時態(tài)的sparseArray中獲取view,當(dāng)失敗時就會去scrapViews中獲取view。
當(dāng)然,這些過程又是和一個boolean變量mDataChanged進(jìn)行配合的,具體的過程在上面的源碼分析中已經(jīng)解釋過了,諸位可以回過去看看。
基本思路是在給子view布局時,如果數(shù)據(jù)沒有發(fā)生改變,就使用當(dāng)前已經(jīng)存在ActiveViews的view。
在obtainView時,如果發(fā)生了改變,就addScrapView.否則就fill with activeView..

再次說到addScrapView,由于這是一個比較重要的方法,這里小結(jié)時我們也來看看哪些地方調(diào)用了addScrapView.
我們可以在ListView源碼中搜索addScrapView看看。

  1. onMeasure
  2. measureHeightOfChildren , measure listView指定范圍的高度, 在onMeasure中調(diào)用
  3. layoutChildren
  4. scrollListItemsBy , 以一定child數(shù)目滑動List,需要將滑出的child刪掉,在最后添加view

其實(shí)前三個中用到的addScrapView我們之前也已經(jīng)都講到了,addScrapView的實(shí)現(xiàn)過程也不算復(fù)雜,主要是和activeView以及有瞬態(tài)的view的配合使用。
第四個我們接下來講一下。

既然了解了RecycleBin的緩存結(jié)構(gòu)和基本方法后,我們來實(shí)戰(zhàn)看看,在一個Listview滑動過程中,到底是怎么實(shí)現(xiàn)view的回收的吧。

現(xiàn)在考慮滑動一個ListView的情況,也就是scrollListItemsBy方法。

private void scrollListItemsBy(int amount) {
    ...
    final AbsListView.RecycleBin recycleBin = mRecycler;
    if (amount < 0) { //上滑
        ...
        View last = getChildAt(numChildren - 1);
        while (last.getBottom() < listBottom) {
            final int lastVisiblePosition = mFirstPosition + numChildren - 1;
            if (lastVisiblePosition < mItemCount - 1) {
                last = addViewBelow(last, lastVisiblePosition);
                numChildren++;
            }
            ...
        }
        ...
        View first = getChildAt(0);
        while (first.getBottom() < listTop) {
            AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams();
            if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
                recycleBin.addScrapView(first, mFirstPosition);
            }
            ...
        }
        ...
    }else{ //往下滑
        ...
        View last = getChildAt(lastIndex);
        ...
        if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
            recycleBin.addScrapView(last, mFirstPosition + lastIndex);
        }
        ...
    }
}

可以看到,每次ListView的滑動事件要將一個view滑出屏幕時,會將頭部的或者尾部的(視方向而定)childView通過addScrapView緩存起來,
緩存的流程就是我們一開始分析的了,先保存有瞬時態(tài)的view,然后視情況存到scrapViews中。

等到每次obtainView時再依序從緩存的view中取出來。

這樣就完成了一個滑動的緩存與回收。

好了,關(guān)于ListView的回收機(jī)制到這里就講的差不多了,本質(zhì)上就是對AbsListView的內(nèi)部類RecycleBin的操作。
當(dāng)我們弄懂了這個機(jī)制,才能更好的思考更多的問題。

例如,最后我問大家一個問題:

根據(jù)我之前所講的,當(dāng)一個ListView的有若干個viewType, 當(dāng)滑出的view和滑入添加的view的type不一樣,比如說滑出了一個TextView的item,
滑入了一個ImageView的Item, 那這種情況下還能復(fù)用剛才的view嗎?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,621評論 2 380

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