RecyclerView之三級緩存源碼解析

序言

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

正文

緩存機(jī)制

(1). RecycledViewPool的緩存

  • RecycledViewPool也叫第三級緩存
  • 文檔中說的是: 為多個RecyclerView提供的一個共用緩存池,如果想要通過RecyclerView緩存View的話,可以自己提供一個RecycledViewPool實例,并通過RecyclerView的setRecycledViewPool()方法來設(shè)置,如果不主動提供的話,RecyclerView會為自己主動創(chuàng)建一個
  • 首先來看其緩存方式: 其中有一個 SparseArray<ScrapData> 類型的mScrap來緩存ViewHolder,每一個View Type 類型的Item都會有一個該緩存(源碼如下),默認(rèn)最大容量為5,但是可以通過recyclerView.getRecycledViewPool().setMaxRecycledViews(int viewType, int max);來設(shè)置;(作者推薦的是:如果屏幕上有很多相同類型的ItemView同時改變,那么推薦將該容量設(shè)置大一些,但是如果有一種類型的ItemView很少出現(xiàn),并且不超過一個,那么推薦將該容量設(shè)置為1,否則其遲早會被填滿而造成內(nèi)存浪費)
  static class ScrapData {
      ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
      int mMaxScrap = DEFAULT_MAX_SCRAP; //每個View Type默認(rèn)容量為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()方法,那么就默認(rèn)只有一種View Type,默認(rèn)為-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放進(jìn)RecycledViewPool之前,都會做什么處理(主要代碼如下);需要注意的是,下面的注釋中有這樣一句話:Pass false to dispatchRecycled for views that have not been bound.,大意為:當(dāng)一個ViewHolder沒有綁定view的時候傳遞false給dispatchRecycled;換句話說就是,下面dispatchViewRecycled(holder);的功能就是清除ViewHolder相關(guān)綁定的操作;另外我們再來看一下對于RecycledViewPool的文檔描述中有這樣一句話:RecycledViewPool lets you share Views between multiple RecyclerViews.,即通過RecycledViewPool可以在不同的RecyclerView之間共享View(實際上是ViewHolder),所以,這里我們也就可以理解下面holder.mOwnerRecyclerView = null清除與原來RecyclerView關(guān)聯(lián)的操作了(因為不清除的話,在多個RecyclerView之間共享就會出現(xiàn)問題);那么到這里我們對于RecycledViewPool中的ViewHolder就有了大致的了解了,總結(jié)一下就是: 當(dāng)一個ViewHolder被緩存進(jìn)入該pool的時候,除了其自身的View Type以外,其自身與外界的綁定關(guān)系,flag標(biāo)志,與原來RecyclerView的聯(lián)系等信息都被清除了,那么理所當(dāng)然的是,對于處于pool中的ViewHolder的查詢,就應(yīng)該通過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)) {
            //標(biāo)志(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);
    }
  • 下面我們應(yīng)該順著這條線索,繼續(xù)搜索哪種情況下會將一個ViewHolder扔進(jìn)RecycledViewPool中;這里筆者找到以下幾種情況:
  1. 在View Cache(第一級緩存)中的Item被更新或者被刪除時(即從Cache中移出的ViewHolder會進(jìn)入pool中);可以看出的時,更新和刪除操作時,將ViewHolder回收進(jìn)pool中都是通過recycleCachedViewAt()方法,如下可知,其只是調(diào)用了上面的ViewHolder清除工作,同時刪除了Cache中的緩存
    //當(dāng)View Cache中Item更新時
    //但是什么時候會更新呢: 可以想像的一種情況是當(dāng)有Item緩存進(jìn)入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);
        }
    }

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

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

  1. 關(guān)于這兩者應(yīng)該看的是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). 存儲與當(dāng)前View相關(guān)的信息; (4). 進(jìn)行預(yù)布局(pre_layout); 這里很明顯,我們關(guān)注的重點應(yīng)該放在預(yù)布局上,從下面代碼中的注釋可以看出,預(yù)布局分為兩步: 第一步是找到所有沒有被remove的Item,進(jìn)行預(yù)布局準(zhǔn)備; 第二步是進(jìn)行真正的預(yù)布局,從源代碼注釋中,我們可以看出,預(yù)布局時會使用Adapter改變前的Item(包括其位置和數(shù)量)來布局,同時其使用的Layout尺寸也是改變前的尺寸(這點可以從上面onMeasure()方法中對dispatchLayoutStep2()方法的注釋可以看出(大意為: 預(yù)布局應(yīng)該發(fā)生在舊的尺寸上),這是為了和正真改變后的布局相對比,來決定Item的顯示(可能這里讀者還是不清楚pre_layout的作用,不要緊,下面會詳細(xì)解釋,這里需要了解的只是在該方法中所做的事情)
/**
 * 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): 預(yù)布局
    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()進(jìn)行的post_layout;可以看出,這里主要是對子View進(jìn)行Layout,需要注意的是,在onMeasure()中,在進(jìn)行dispatchLayoutStep2()操作之前,還進(jìn)行了mLayout.setMeasureSpecs(widthSpec, heightSpec);也就是設(shè)置改變后真正的布局尺寸;但是當(dāng)查看LayoutManager的onLayoutChildren()方法時,我們發(fā)現(xiàn)其是一個空方法,所以應(yīng)該找其實現(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的回收方向相關(guān)); (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的填充和回收相關(guān);其主要過程是通過下面while循環(huán)中不斷的填充(layoutChunk)和回收Item(recycleByLayoutState)完成;而在recycleByLayoutState()中分為兩種情況處理:即向上滾動和向下滾動,其中回收的條件是當(dāng)Item滾動出屏幕且不可見時(在recycleViewsFromEnd()和recycleViewsFromStart()中都對滾動的邊界做了判斷),而最終回收調(diào)用的是recycleViewHolderInternal()方法;在recycleViewHolderInternal()中,其首先判斷了如果第一級緩存滿了的話,先將以前存入的Item移出,并存入Pool中,之后再緩存當(dāng)前Item;這里也就是對應(yīng)了RecycledViewPool緩存的第一種情況;還需要注意的是,當(dāng)Item正在執(zhí)行動畫的時,會導(dǎo)致回收失敗,此時會在ItemAnimatorRestoreListener.onAnimationFinished()中進(jìn)行回收
  •   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); //回收進(jìn)pool中
              cachedViewSize--;
          }
          /存入第一級緩存
          mCachedViews.add(targetCacheIndex, holder);
          ...
      }
    
  1. 在我們繼續(xù)進(jìn)行下一步分析之前,筆者想先來總結(jié)一下上面我們在尋找pre_layout和post_layout區(qū)別的時候所經(jīng)過的過程: 我們主要圍繞的是RecyclerView的onMeasure()方法,經(jīng)過了dispatchLayoutStep1()和dispatchLayoutStep2()兩個主要的過程,前一個負(fù)責(zé)預(yù)布局(pre_layout),后一個負(fù)責(zé)真正的布局(post_layout);其實到這里,布局過程還沒有真正的完成,因為我們還沒有弄清楚的是Item的滾動動畫
  1. onMeasure過程之后,我們應(yīng)該將目光聚焦在layout過程,在RecyclerView的onLayout()方法中,其關(guān)鍵的是調(diào)用了dispatchLayout(),關(guān)于該方法,源碼注釋給出了明確的說明:dispatchLayout()方法中封裝了與Item(出入)動畫相關(guān)的操作,當(dāng)重新布局(可能原因比如:Adapter改變,Item滑動等)之后,Item的改變類型大概有一下幾種: (1). PERSISTENT: 即在pre_layout和post_layout中都是可見的(由animatePersistence()方法處理); (2). REMOVED: 在pre_layout中可見,但是被刪除了(對應(yīng)數(shù)據(jù)的刪除)(由animateChange()方法處理);(3). ADDED: 在pre_layout中不存在,但是被添加進(jìn)的Item(對應(yīng)數(shù)據(jù)的添加)(由animateChange()方法處理); (4). DISAPPEARING: 數(shù)據(jù)集沒有改變,但是Item由可見變?yōu)椴豢梢?即Item滑動出屏幕)(由animateDisappearance()方法處理); (5). APPEARING: 數(shù)據(jù)集沒有改變,但是Item由不可見變?yōu)榭梢?對應(yīng)Item滑動進(jìn)入屏幕)(由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ū)別應(yīng)該很清楚了;這里舉個例子來進(jìn)一步理解一下: 考慮一種情況,如果現(xiàn)在界面上有兩個Item a,b,并且占滿了屏幕,此時如果刪除b使得c需要進(jìn)入界面的話,那么我們雖然知道c的最終位置,但是我們?nèi)绾沃纁該從哪里滑入屏幕呢,很明顯,不可能默認(rèn)都從底部開始滑入,因為很明顯的是還有其他情況;所以在這里Google的解決辦法是請求兩個布局: pre_layout和post_layout; 當(dāng)Adapter改變即這里的b被刪除的時候,作為一個事件觸發(fā),此時pre_layout將加載c(但是此時c仍然是不可見的),然后在post_layout中去加載改變后的Adapter的正常布局,通過前后兩個布局對c位置的比較,我們就可以知道c該從哪里滑入;另外,還有一種情況是,如果b只是被改變了呢(并沒有被刪除),那么此時,pre_layout仍然會加載c,因為b的改變可能會引起b高度的改變而使得c有機(jī)會進(jìn)入界面;但是,當(dāng)Adapter改變完成之后,發(fā)現(xiàn)b并沒有改變高度,換句話說,就是c還是不能進(jìn)入界面的時候,此時Item c將被扔進(jìn)該pool,這種情況也就是上面說的RecycledViewPool進(jìn)行回收的第2種情況;話不多說,繼續(xù)分析(萬里長征還未過半...)
  1. 我們繼續(xù)進(jìn)入mViewInfoStore.process()方法,該方法屬于ViewInfoStore類,對于該類的描述是:對View進(jìn)行跟蹤并運行相關(guān)動畫,進(jìn)一步解釋就是執(zhí)行Item改變過程中的一些動畫;繼續(xù)看其在process()方法做了什么:其實在該方法中進(jìn)行了許多的情況的判斷,這里筆者只是抽取出了對應(yīng)當(dāng)前情況的處理,可以看出,當(dāng)similar to appear disappear but happened between different layout passes時,只是簡單的調(diào)用了ProcessCallback.unused(),而在unused()中,也只是對Item進(jìn)行了回收(如下);但是,值得注意的是,ViewInfoStore.process()方法進(jìn)行的處理,遠(yuǎn)不止如此,實際上,我們還有意外收獲,這里只需要記住該方法就好了,具體,下面還會再分析
  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. 最后筆者還想附帶提一下的是,關(guān)于Item出入屏幕動畫處理的那幾個方法(即上面的animatePersistence(),animateChange()等)都是位于ItemAnimator中,這是一個abstract的類,如果想要自定義的Item的出入動畫的話,可以繼承該類,并通過recyclerView.setItemAnimator();來進(jìn)行設(shè)置

(1-). 又見RecycledViewPool緩存

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

(3). View Cache緩存

  • View Cache也叫第一級緩存,主要指的是RecyclerView.Recycler中的mCachedViews字段,它是一個ArrayList,不區(qū)分view type,默認(rèn)容量是2,但是可以通過RecyclerView的setItemViewCacheSize()方法來設(shè)置
  • 對于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(),而該方法上面分析第三級緩存的時候,分析的是,當(dāng)Item被移出屏幕區(qū)域時,先是緩存進(jìn)了mCachedViews中,因為處于mCachedViews中的ViewHolder是希望被原樣重用的;之所以這樣說,是因為從 recycleViewHolderInternal() 的源碼中可以看出,在 mCachedViews.add() 之前并沒有像上面存入第三級緩存之前那樣進(jìn)行一系列的清理工作,也就是說ViewHolder相關(guān)的和重要的position,flag等標(biāo)志都一并被緩存了;那么,從mCachedViews中取出的ViewHolder就不需要再進(jìn)行綁定操作而可以直接使用了(實際上所以我們期望的也是在mCachedViews中的ViewHolder能夠被重用,并且還是在它原來的位置被重用,這樣就不需要再去bind了;)
  • 至于mChangedScrap和mAttachedScrap緩存的話,我們也可以從其add()方法入手(如下),可以看出,一個ViewHolder是緩存進(jìn)入mChangedScrap還是mAttachedScrap,取決于其狀態(tài),如果一個Item被移除或者非法(如:與其view type 類型不再相符等),那么就會被放進(jìn)mAttachedScrap中,反之,則進(jìn)入mChangedScrap;說的更明顯一點就是,如果如果一個Item被移除,那么就會被放進(jìn)mAttachedScrap中,如果調(diào)用了notifXXX()之類的方法,那么需要改變的ViewHolder就被放進(jìn)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的重復(fù)綁定,就可以使用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三級緩存相關(guān)的源碼分析就結(jié)束了;但是由于筆者能力有限,很多細(xì)節(jié)和理解可能不到位,更多的還是需要自己動手多看源碼:)

(6). 參考文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(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)我...
    茶點故事閱讀 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
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,621評論 2 380