RecyclerView之三級緩存源碼解析

序言

  1. RecyclerView有三大典型的功能,一個是Recycler的緩存機制,一個LayoutManager的布局管理,一個ItemDecoration的分割線繪制;本文將結(jié)合源碼講解其緩存機制
  2. 更多相關的源碼解析見RecyclerView之ItemDecoration

正文

緩存機制

(1). RecycledViewPool的緩存

  • RecycledViewPool也叫第三級緩存
  • 文檔中說的是: 為多個RecyclerView提供的一個共用緩存池,如果想要通過RecyclerView緩存View的話,可以自己提供一個RecycledViewPool實例,并通過RecyclerView的setRecycledViewPool()方法來設置,如果不主動提供的話,RecyclerView會為自己主動創(chuàng)建一個
  • 首先來看其緩存方式: 其中有一個 SparseArray<ScrapData> 類型的mScrap來緩存ViewHolder,每一個View Type 類型的Item都會有一個該緩存(源碼如下),默認最大容量為5,但是可以通過recyclerView.getRecycledViewPool().setMaxRecycledViews(int viewType, int max);來設置;(作者推薦的是:如果屏幕上有很多相同類型的ItemView同時改變,那么推薦將該容量設置大一些,但是如果有一種類型的ItemView很少出現(xiàn),并且不超過一個,那么推薦將該容量設置為1,否則其遲早會被填滿而造成內(nèi)存浪費)
  static class ScrapData {
      ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
      int mMaxScrap = DEFAULT_MAX_SCRAP; //每個View Type默認容量為5
      long mCreateRunningAverageNs = 0;
      long mBindRunningAverageNs = 0;
  }
  SparseArray<ScrapData> mScrap = new SparseArray<>();
  • 至于這里的SparseArray,它是Android中的一個工具類,因為Android內(nèi)存限制,所以產(chǎn)生了這樣一個比HashMap輕量的類(具體可以參考博客)

  • 接下來看一下RecycledViewPool的存取方法;從這兩個方法中,我們可以看出,在RecycledViewPool中緩存的ViewHolder之間是依靠 View Type 來區(qū)分的,也就是說,同一個View Type之間的ViewHolder緩存在RecycledViewPool中是沒有區(qū)別的;如果我們沒有重寫ViewHolder的getItemViewType()方法,那么就默認只有一種View Type,默認為-1

  public ViewHolder getRecycledView(int viewType) {
      ...
      return scrapHeap.remove(scrapHeap.size() - 1);
  }

  public void putRecycledView(ViewHolder scrap) {
      final int viewType = scrap.getItemViewType();
       final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
      ...
      scrap.resetInternal();
      scrapHeap.add(scrap);
  }
  • 下面我們看一下在將一個ViewHolder放進RecycledViewPool之前,都會做什么處理(主要代碼如下);需要注意的是,下面的注釋中有這樣一句話:Pass false to dispatchRecycled for views that have not been bound.,大意為:當一個ViewHolder沒有綁定view的時候傳遞false給dispatchRecycled;換句話說就是,下面dispatchViewRecycled(holder);的功能就是清除ViewHolder相關綁定的操作;另外我們再來看一下對于RecycledViewPool的文檔描述中有這樣一句話:RecycledViewPool lets you share Views between multiple RecyclerViews.,即通過RecycledViewPool可以在不同的RecyclerView之間共享View(實際上是ViewHolder),所以,這里我們也就可以理解下面holder.mOwnerRecyclerView = null清除與原來RecyclerView關聯(lián)的操作了(因為不清除的話,在多個RecyclerView之間共享就會出現(xiàn)問題);那么到這里我們對于RecycledViewPool中的ViewHolder就有了大致的了解了,總結(jié)一下就是: 當一個ViewHolder被緩存進入該pool的時候,除了其自身的View Type以外,其自身與外界的綁定關系,flag標志,與原來RecyclerView的聯(lián)系等信息都被清除了,那么理所當然的是,對于處于pool中的ViewHolder的查詢,就應該通過View Type來確定了,也就是上面我們所說的
    /**
    * Pass false to dispatchRecycled for views that have not been bound.
    * @param dispatchRecycled True to dispatch View recycled callbacks.
    */
    void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
        ...
        if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {
            //標志(flag)清除
            holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
            ViewCompat.setAccessibilityDelegate(holder.itemView, null);
        }
        if (dispatchRecycled) {
            //綁定清除
            dispatchViewRecycled(holder);
        }
        //與RecyclerView的聯(lián)系清除
        holder.mOwnerRecyclerView = null;
        //緩存入pool
        getRecycledViewPool().putRecycledView(holder);
    }
  • 下面我們應該順著這條線索,繼續(xù)搜索哪種情況下會將一個ViewHolder扔進RecycledViewPool中;這里筆者找到以下幾種情況:
  1. 在View Cache(第一級緩存)中的Item被更新或者被刪除時(即從Cache中移出的ViewHolder會進入pool中);可以看出的時,更新和刪除操作時,將ViewHolder回收進pool中都是通過recycleCachedViewAt()方法,如下可知,其只是調(diào)用了上面的ViewHolder清除工作,同時刪除了Cache中的緩存
    //當View Cache中Item更新時
    //但是什么時候會更新呢: 可以想像的一種情況是當有Item緩存進入View Cache中時
    void updateViewCacheSize() {
        ...
        // first, try the views that can be recycled
        for (int i = mCachedViews.size() - 1;
                i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
            recycleCachedViewAt(i);
        }
    }

    //當View Cache中Item刪除時
    void recycleAndClearCachedViews() {
        final int count = mCachedViews.size();
        for (int i = count - 1; i >= 0; i--) {
            recycleCachedViewAt(i);
        }
        mCachedViews.clear();
        ...
    }
    
    //該方法中調(diào)用了上面所說的回收進pool中的清除工作,同時將Cache中的緩存刪除
    void recycleCachedViewAt(int cachedViewIndex) {
        ....
        addViewHolderToRecycledViewPool(viewHolder, true);
        mCachedViews.remove(cachedViewIndex);
    }
  1. LayoutManager在pre_layout過程中添加View,但是在post_layout過程中沒有添加該View;當然,在尋找該過程對應的源碼的時候,我們首先應該弄清楚的是pre_layout和post_layout是什么(所以在繼續(xù)講解之前,筆者打算先講一個小插曲)

(2) 一個小插曲: pre_layout和post_layout

  1. 關于這兩者應該看的是RecyclerView的onMeasure()方法;如下可知,onMeasure中主要是分為兩步,即dispatchLayoutStep1()和dispatchLayoutStep2();
protected void onMeasure(int widthSpec, int heightSpec) {
    if (mLayout.mAutoMeasure) {
        ...
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
        }
        // set dimensions in 2nd step. Pre-layout should happen with old dimensions for
        // consistency
        mLayout.setMeasureSpecs(widthSpec, heightSpec);
        dispatchLayoutStep2();
        ...
    } else {
        ...
    }
}
  1. 我們先來看即dispatchLayoutStep1()中做的事情;該方法的注釋中我們知道其做的事情: (1). 處理Adapter的更新; (2). 決定是否是否使用動畫; (3). 存儲與當前View相關的信息; (4). 進行預布局(pre_layout); 這里很明顯,我們關注的重點應該放在預布局上,從下面代碼中的注釋可以看出,預布局分為兩步: 第一步是找到所有沒有被remove的Item,進行預布局準備; 第二步是進行真正的預布局,從源代碼注釋中,我們可以看出,預布局時會使用Adapter改變前的Item(包括其位置和數(shù)量)來布局,同時其使用的Layout尺寸也是改變前的尺寸(這點可以從上面onMeasure()方法中對dispatchLayoutStep2()方法的注釋可以看出(大意為: 預布局應該發(fā)生在舊的尺寸上),這是為了和正真改變后的布局相對比,來決定Item的顯示(可能這里讀者還是不清楚pre_layout的作用,不要緊,下面會詳細解釋,這里需要了解的只是在該方法中所做的事情)
/**
 * The first step of a layout where we;
 * - process adapter updates
 * - decide which animation should run
 * - save information about current views
 * - If necessary, run predictive layout and save its information
 */
private void dispatchLayoutStep1() {
    ...
    //情況(1)和(2)
    processAdapterUpdatesAndSetAnimationFlags();
    //情況(3)
    ...
    //情況(4): 預布局
    if (mState.mRunSimpleAnimations) {
        // Step 0: Find out where all non-removed items are, pre-layout
    }
    if (mState.mRunPredictiveAnimations) {
        /**
        Step 1: run prelayout: This will use the old positions of items. The layout  manager is expected to layout everything, even removed items (though not to add removed items back to the container). This gives the pre-layout position of APPEARING views which come into existence as part of the real layout.
        */
    }
}
  1. 接下來是實現(xiàn)真正的布局,即dispatchLayoutStep2()進行的post_layout;可以看出,這里主要是對子View進行Layout,需要注意的是,在onMeasure()中,在進行dispatchLayoutStep2()操作之前,還進行了mLayout.setMeasureSpecs(widthSpec, heightSpec);也就是設置改變后真正的布局尺寸;但是當查看LayoutManager的onLayoutChildren()方法時,我們發(fā)現(xiàn)其是一個空方法,所以應該找其實現(xiàn)類(這里以LinearLayoutManager為例)
/**
 * The second layout step where we do the actual layout of the views for the final state.
 * This step might be run multiple times if necessary (e.g. measure).
 */
private void dispatchLayoutStep2() {
    ...
    // Step 2: Run layout
    mLayout.onLayoutChildren(mRecycler, mState);
}
  1. LinearLayoutManager的onLayoutChildren()過程: 在其源碼中介紹了Layout算法: (1). 首先找到獲得焦點的ItemView; (2). 從后往前布局或者從前往后布局(這個主要是與滾動出屏幕的Item的回收方向相關); (3). 滾動; 其中最主要的是一個fill()方法
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.
    ...
    fill(recycler, mLayoutState, state, false);
}
  1. fill()方法: 從其參數(shù)可以猜測的是,該方法與Item的填充和回收相關;其主要過程是通過下面while循環(huán)中不斷的填充(layoutChunk)和回收Item(recycleByLayoutState)完成;而在recycleByLayoutState()中分為兩種情況處理:即向上滾動和向下滾動,其中回收的條件是當Item滾動出屏幕且不可見時(在recycleViewsFromEnd()和recycleViewsFromStart()中都對滾動的邊界做了判斷),而最終回收調(diào)用的是recycleViewHolderInternal()方法;在recycleViewHolderInternal()中,其首先判斷了如果第一級緩存滿了的話,先將以前存入的Item移出,并存入Pool中,之后再緩存當前Item;這里也就是對應了RecycledViewPool緩存的第一種情況;還需要注意的是,當Item正在執(zhí)行動畫的時,會導致回收失敗,此時會在ItemAnimatorRestoreListener.onAnimationFinished()中進行回收
  •   int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
                  RecyclerView.State state, boolean stopOnFocusable) {
          while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
              ...
              layoutChunk(recycler, state, layoutState, layoutChunkResult);
              ...
              recycleByLayoutState(recycler, layoutState);
          }
      }
    
    
      private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
          if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
              recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
          } else {
              recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
          }
      }
    
      void recycleViewHolderInternal(ViewHolder holder) {
          ...
          int cachedViewSize = mCachedViews.size();
          if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
              recycleCachedViewAt(0); //回收進pool中
              cachedViewSize--;
          }
          /存入第一級緩存
          mCachedViews.add(targetCacheIndex, holder);
          ...
      }
    
  1. 在我們繼續(xù)進行下一步分析之前,筆者想先來總結(jié)一下上面我們在尋找pre_layout和post_layout區(qū)別的時候所經(jīng)過的過程: 我們主要圍繞的是RecyclerView的onMeasure()方法,經(jīng)過了dispatchLayoutStep1()和dispatchLayoutStep2()兩個主要的過程,前一個負責預布局(pre_layout),后一個負責真正的布局(post_layout);其實到這里,布局過程還沒有真正的完成,因為我們還沒有弄清楚的是Item的滾動動畫
  1. onMeasure過程之后,我們應該將目光聚焦在layout過程,在RecyclerView的onLayout()方法中,其關鍵的是調(diào)用了dispatchLayout(),關于該方法,源碼注釋給出了明確的說明:dispatchLayout()方法中封裝了與Item(出入)動畫相關的操作,當重新布局(可能原因比如:Adapter改變,Item滑動等)之后,Item的改變類型大概有一下幾種: (1). PERSISTENT: 即在pre_layout和post_layout中都是可見的(由animatePersistence()方法處理); (2). REMOVED: 在pre_layout中可見,但是被刪除了(對應數(shù)據(jù)的刪除)(由animateChange()方法處理);(3). ADDED: 在pre_layout中不存在,但是被添加進的Item(對應數(shù)據(jù)的添加)(由animateChange()方法處理); (4). DISAPPEARING: 數(shù)據(jù)集沒有改變,但是Item由可見變?yōu)椴豢梢?即Item滑動出屏幕)(由animateDisappearance()方法處理); (5). APPEARING: 數(shù)據(jù)集沒有改變,但是Item由不可見變?yōu)榭梢?對應Item滑動進入屏幕)(由animateAppearance()方法處理);
  1. 但是我們最終追尋下去,可以看出的是在dispatchLayout()中,又將一系列處理完全交給了dispatchLayoutStep3()方法來處理;從下面代碼中可以看出,其最終通過回調(diào)ViewInfoStore.ProcessCallback來處理上面的四種動畫
  private void dispatchLayoutStep3() {
      ...
      // Step 4: Process view info lists and trigger animations
      mViewInfoStore.process(mViewInfoProcessCallback);
  }
  1. 到這里為止,我們對于pre_layout和post_layout的區(qū)別應該很清楚了;這里舉個例子來進一步理解一下: 考慮一種情況,如果現(xiàn)在界面上有兩個Item a,b,并且占滿了屏幕,此時如果刪除b使得c需要進入界面的話,那么我們雖然知道c的最終位置,但是我們?nèi)绾沃纁該從哪里滑入屏幕呢,很明顯,不可能默認都從底部開始滑入,因為很明顯的是還有其他情況;所以在這里Google的解決辦法是請求兩個布局: pre_layout和post_layout; 當Adapter改變即這里的b被刪除的時候,作為一個事件觸發(fā),此時pre_layout將加載c(但是此時c仍然是不可見的),然后在post_layout中去加載改變后的Adapter的正常布局,通過前后兩個布局對c位置的比較,我們就可以知道c該從哪里滑入;另外,還有一種情況是,如果b只是被改變了呢(并沒有被刪除),那么此時,pre_layout仍然會加載c,因為b的改變可能會引起b高度的改變而使得c有機會進入界面;但是,當Adapter改變完成之后,發(fā)現(xiàn)b并沒有改變高度,換句話說,就是c還是不能進入界面的時候,此時Item c將被扔進該pool,這種情況也就是上面說的RecycledViewPool進行回收的第2種情況;話不多說,繼續(xù)分析(萬里長征還未過半...)
  1. 我們繼續(xù)進入mViewInfoStore.process()方法,該方法屬于ViewInfoStore類,對于該類的描述是:對View進行跟蹤并運行相關動畫,進一步解釋就是執(zhí)行Item改變過程中的一些動畫;繼續(xù)看其在process()方法做了什么:其實在該方法中進行了許多的情況的判斷,這里筆者只是抽取出了對應當前情況的處理,可以看出,當similar to appear disappear but happened between different layout passes時,只是簡單的調(diào)用了ProcessCallback.unused(),而在unused()中,也只是對Item進行了回收(如下);但是,值得注意的是,ViewInfoStore.process()方法進行的處理,遠不止如此,實際上,我們還有意外收獲,這里只需要記住該方法就好了,具體,下面還會再分析
  void process(ProcessCallback callback) {
      ...
      // similar to appear disappear but happened between different layout passes.
      // this can happen when the layout manager is using auto-measure
      callback.unused(viewHolder);
      ...
  }

  @Override
 public void unused(ViewHolder viewHolder) {
      mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
  }
  1. 最后筆者還想附帶提一下的是,關于Item出入屏幕動畫處理的那幾個方法(即上面的animatePersistence(),animateChange()等)都是位于ItemAnimator中,這是一個abstract的類,如果想要自定義的Item的出入動畫的話,可以繼承該類,并通過recyclerView.setItemAnimator();來進行設置

(1-). 又見RecycledViewPool緩存

  • 這里插曲可能稍微長了一點,但是,筆者感覺這是值得的;現(xiàn)在,讓我們繼續(xù)最初的話題: 什么情況下一個ViewHolder會被扔進Pool中呢?這里筆者再次回顧一下:
  1. 在View Cache中的Item被更新或者被刪除時(存滿溢出時)
  2. LayoutManager在pre_layout過程中添加View,但是在post_layout過程中沒有添加該View(數(shù)據(jù)集改變,如刪除)
  • 到這里RecyclerView的第三級緩存差不多就分析完了,接下來,我們再看一下與其緊密相關的第一級緩存

(3). View Cache緩存

  • View Cache也叫第一級緩存,主要指的是RecyclerView.Recycler中的mCachedViews字段,它是一個ArrayList,不區(qū)分view type,默認容量是2,但是可以通過RecyclerView的setItemViewCacheSize()方法來設置
  • 對于Recycler類的第一級緩存,我們需要注意的是以下三個字段
public final class Recycler {
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null;
        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
}
  • 現(xiàn)在,我們需要看的是什么時候,mCachedViews會緩存ViewHolder,通過追蹤,可以發(fā)現(xiàn),只有在recycleViewHolderInternal()中調(diào)用了mCachedViews.add(),而該方法上面分析第三級緩存的時候,分析的是,當Item被移出屏幕區(qū)域時,先是緩存進了mCachedViews中,因為處于mCachedViews中的ViewHolder是希望被原樣重用的;之所以這樣說,是因為從 recycleViewHolderInternal() 的源碼中可以看出,在 mCachedViews.add() 之前并沒有像上面存入第三級緩存之前那樣進行一系列的清理工作,也就是說ViewHolder相關的和重要的position,flag等標志都一并被緩存了;那么,從mCachedViews中取出的ViewHolder就不需要再進行綁定操作而可以直接使用了(實際上所以我們期望的也是在mCachedViews中的ViewHolder能夠被重用,并且還是在它原來的位置被重用,這樣就不需要再去bind了;)
  • 至于mChangedScrap和mAttachedScrap緩存的話,我們也可以從其add()方法入手(如下),可以看出,一個ViewHolder是緩存進入mChangedScrap還是mAttachedScrap,取決于其狀態(tài),如果一個Item被移除或者非法(如:與其view type 類型不再相符等),那么就會被放進mAttachedScrap中,反之,則進入mChangedScrap;說的更明顯一點就是,如果如果一個Item被移除,那么就會被放進mAttachedScrap中,如果調(diào)用了notifXXX()之類的方法,那么需要改變的ViewHolder就被放進mChangedScrap中
void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        ...
        holder.setScrapContainer(this, false);
        mAttachedScrap.add(holder);
    } else {
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        holder.setScrapContainer(this, true);
        mChangedScrap.add(holder);
    }
}
  • 第二級緩存相對來說比較簡單,所以就暫時分析到這里

(4). ViewCacheExtension

  • 交由用戶決定的緩存,也是第二級緩存
  • 從文檔對其的描述中可以看出的是,這是一個用戶自定義邏輯的緩存類,在查找一個緩存的ViewHolder的時候,會按照mCachedViews -> ViewCacheExtension -> RecycledViewPool的順序來查找
  • 這是一個abstract的類,使用的時候,只需要實現(xiàn)一個View getViewForPositionAndType(Recycler recycler, int position, int type);方法
  • 下面,我們通過一個例子來看一下什么時候可以使用該緩存:(: 下面的例子來源于文末的參考文章)考慮現(xiàn)在有這樣的一些Item
  1. 其position固定(比如廣告之類)
  2. 不會改變(view type等)
  3. 數(shù)量合理,以便可以保存在內(nèi)存中
    現(xiàn)在,為了避免這些Item的重復綁定,就可以使用ViewCacheExtension(需要注意的是,這里不能使用RecycledViewPool,因為其緩存的ViewHolder需要重新綁定,同時也能使用View Cache,因為其中的ViewHolder是不區(qū)分view type的),比如下面的示例代碼
SparseArray<View> specials = new SparseArray<>();
...
recyclerView.getRecycledViewPool().setMaxRecycledViews(SPECIAL, 0);
recyclerView.setViewCacheExtension(new RecyclerView.ViewCacheExtension() {
    @Override
    public View getViewForPositionAndType(RecyclerView.Recycler recycler,
                                            int position, int type) {
        return type == SPECIAL ? specials.get(position) : null;
    }
});
...
class SpecialViewHolder extends RecyclerView.ViewHolder {
        ...     
    public void bindTo(int position) {
        ...
        specials.put(position, itemView);
    }
}

(5). 小結(jié)

  • 到這里為止,RecyclerView三級緩存相關的源碼分析就結(jié)束了;但是由于筆者能力有限,很多細節(jié)和理解可能不到位,更多的還是需要自己動手多看源碼:)

(6). 參考文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市麸折,隨后出現(xiàn)的幾起案子膏孟,更是在濱河造成了極大的恐慌,老刑警劉巖矮湘,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件褐着,死亡現(xiàn)場離奇詭異逃顶,居然都是意外死亡脾歧,警方通過查閱死者的電腦和手機甲捏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鞭执,“玉大人司顿,你說我怎么就攤上這事芒粹。” “怎么了大溜?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵是辕,是天一觀的道長。 經(jīng)常有香客問我猎提,道長,這世上最難降的妖魔是什么旁蔼? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任锨苏,我火速辦了婚禮,結(jié)果婚禮上棺聊,老公的妹妹穿的比我還像新娘伞租。我一直安慰自己,他們只是感情好限佩,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布葵诈。 她就那樣靜靜地躺著,像睡著了一般祟同。 火紅的嫁衣襯著肌膚如雪作喘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天晕城,我揣著相機與錄音泞坦,去河邊找鬼。 笑死砖顷,一個胖子當著我的面吹牛贰锁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播滤蝠,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼豌熄,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了物咳?” 一聲冷哼從身側(cè)響起锣险,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎览闰,沒想到半個月后囱持,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡焕济,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年纷妆,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晴弃。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡掩幢,死狀恐怖逊拍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情际邻,我是刑警寧澤芯丧,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站世曾,受9級特大地震影響缨恒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜轮听,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一骗露、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧血巍,春花似錦萧锉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鲫凶,卻和暖如春禀崖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背螟炫。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工帆焕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人不恭。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓叶雹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親换吧。 傳聞我的和親對象是個殘疾皇子折晦,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355