RecyclerView缓存机制

正题

RecyclerView 的回收复用机制的内部实现都是由 Recycler 内部类实现
假设有个页面每行可显示5个卡位,每个卡位的 item 布局 type 一致。开始分析回收复用机制之前,先提几个问题:

1.如果向下滑动,新一行的5个卡位的显示会去复用缓存的 ViewHolder,第一行的5个卡位会移出屏幕被回收,那么在这个过程中,是先进行复用再回收?还是先回收再复用?还是边回收边复用?也就是说,新一行的5个卡位复用的 ViewHolder 有可能是第一行被回收的5个卡位吗?
2.在这个过程中,为什么当 RecyclerView 再次向上滑动重新显示第一行的5个卡位时,只有后面3个卡位触发了 onBindViewHolder() 方法,重新绑定数据呢?明明5个卡位都是复用的。

复用机制

getViewForPosition
      @NonNull
        public View getViewForPosition(int position) {
            return getViewForPosition(position, false);
        }

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

tryGetViewHolderForPositionByDeadline方法返回的是一个ViewHolder对象,然后直接返回ViewHolder的成员变量itemView,也就是当前将要布局的子view。

到这里还没有涉及到缓存的代码,我们也可以猜测,RecycleView的缓存,不只是对View缓存,是对ViewHolder的缓存。

tryGetViewHolderForPositionByDeadline这个方法是真正的缓存机制的入口,它是RecycleView.Recycler中的方法,按照我们正常的思维想想里面肯定是先从缓存中取,取不到在新建一个,那么这个缓存是啥呢,在进入tryGetViewHolderForPositionByDeadline方法之前我们先看几个Recycler中的成员变量方便看下面的代码

 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;
....
}

这几个就是用来缓存的List了

  • mAttachedScrap:不参与复用,只保存在重新布局的时候,从RecycleView中剥离的当前在显示的ViewHolder列表。比如当我们插入一条或者删除一条数据,这时候需要重新布局,怎么办呢,办法就是把当前屏幕上显示的view先拿下来保存到一个列表中,然后在重新布局上去。这个列表就是mAttachedScrap。所以它只是存储重新布局前从RecycleView上剥离出的ViewHolder,并不参与复用
  • mUnmodifiableAttachedScrap:通过Collections.unmodifiableList(mAttachedScrap),把mAttachedScrap放进去,返回一个不可更改的列表,共外部获取
  • mChangedScrap:不参与复用 从新布局的时候要修改的放到这里面,其余的放到mAttachedScrap
  • mCachedViews:这个比较重要,滑动过程中的回收和复用都是先处理的这个List,这个集合里存的ViewHolder的原本数据信息都在,所以可以直接添加到RecyclerView中显示,不需要再次重新绑定onBindViewHolder()
  • mRecyclerPool:参与缓存,并且它里面的ViewHolder的信息都会被重置,相当于一个新建的ViewHolder,所以需要重新调用onBindViewHolder来绑定数据。
    mViewCacheExtension:这个是让我们自己扩展自己的缓存策略,一般情况下我们不会自己写这东西的。
    所以,mCachedViews ,mRecyclerPool , mViewCacheExtension 这三个组成了一个三级缓存,当RecyclerView要拿一个复用的ViewHolder的时候,查找的顺序是mCachedViews->mViewCacheExtension->mRecyclerPool。因为一般情况下我们不会写mViewCacheExtension,所以一般情况就两级缓存mCachedViews->mRecyclerPool

实际上mCachedViews是不参与真正的回收的,mCachedViews的作用是保存最新被移除的ViewHolder,通过removeAndRecycleView(view, recycler)方法,它的作用是,当需要更新ViewHoder的时候,精确的匹配是不是刚才移除那个,如果是直接拿出来让RecycleView布局,如果不是,即使它中存在ViewHolder,也不会返回,而是去mRecyclerPool中找一个新的ViewHolder然后重新赋值。mAttachedScrap中也是精确匹配步骤跟mCachedViews一样。
下面我们进入tryGetViewHolderForPositionByDeadline方法中看看到底是怎么取的吧

       @Nullable
        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
      //position 如果在 item 的范围之外的话,那就抛异常吧
            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)是否处于布局前的状态 去mChangedScrap中找,布局前的状态也就是重新布局的时候
            // 0) If there is a changed scrap, try to find from there
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
//1) 先从mAttachedScrap中找,找不到在去mCachedViews中查找
            // 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
// 检查是不是我们要查找的viewholder,如果不是移除
                        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();
                            }
                //回收这个holder 放到mCachedViews或者mRecyclerPool中
                            recycleViewHolderInternal(holder);
                        }
                        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());
                        }
                    }
                }
                if (holder == null) { // fallback to pool
                    if (DEBUG) {
                        Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                                + position + ") fetching from shared pool");
                    }
              //去mRecyclerPool中查找  根据不同的type拿到不同的holder,
                    //type就是我们在adapter中写的getItemViewType
                    holder = getRecycledViewPool().getRecycledView(type);
                    if (holder != null) {
                        holder.resetInternal();
                        if (FORCE_INVALIDATE_DISPLAY_LIST) {
                            invalidateDisplayListInt(holder);
                        }
                    }
                }
                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 = m
  //缓存中找不到调用createViewHolder创建Adapter.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);
                    if (DEBUG) {
                        Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
                    }
                }
            }

            // This is very ugly but the only place we can grab this information
            // before the View is rebound and returned to the LayoutManager for post layout ops.
            // We don't need this in pre-layout since the VH is not updated by the LM.
            if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
                    .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
                holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                if (mState.mRunSimpleAnimations) {
                    int changeFlags = ItemAnimator
                            .buildAdapterChangeFlagsForAnimations(holder);
                    changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
                    final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
                            holder, changeFlags, holder.getUnmodifiedPayloads());
                    recordAnimationInfoIfBouncedHiddenView(holder, info);
                }
            }

            boolean bound = false;
            if (mState.isPreLayout() && holder.isBound()) {
                // do not update unless we absolutely have to.
                holder.mPreLayoutPosition = position;
            } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
                if (DEBUG && holder.isRemoved()) {
                    throw new IllegalStateException("Removed holder should be bound and it should"
                            + " come here only in pre-layout. Holder: " + holder
                            + exceptionLabel());
                }
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }
 //给holder中的itemView设置LayoutParams
            final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
            final LayoutParams rvLayoutParams;
            if (lp == null) {
                rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
                holder.itemView.setLayoutParams(rvLayoutParams);
            } else if (!checkLayoutParams(lp)) {
                rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
                holder.itemView.setLayoutParams(rvLayoutParams);
            } else {
                rvLayoutParams = (LayoutParams) lp;
            }
            rvLayoutParams.mViewHolder = holder;
            rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
            return holder;
        }

从上面源码可以总结一下的流程,先去mAttachedScrap中找。是要是看看View是不是刚刚剥离的,如果是直接返回如果不是,去mCachedViews中查找,mCachedViews中是精确查找,如果找到返回,找不到或者匹配不上就去mRecyclerPool中查找,找到了返回一个全新的ViewHolder,找不到的话只能调用onCreateViewHolder新建一个了。
mAttachedScrap和mCachedViews都是精确查找,找到的ViewHolder都是已经绑定好数据的,不会再调用onBindViewHolder重新绑定数据,mRecyclerPool中的ViewHolder都是清理干净的空白的ViewHolder,找到之后需要调用onBindViewHolder重新绑定数据

那RecycleView到底是怎么复用的呢?入口很多比如通过Recycler中的recycleView方法(recycler.recycleView)进去看看

**
         * Recycle a detached view. The specified view will be added to a pool of views
         * for later rebinding and reuse.
         *
         * <p>A view must be fully detached (removed from parent) before it may be recycled. If the
         * View is scrapped, it will be removed from scrap list.</p>
         *
         * @param view Removed view for recycling
         * @see LayoutManager#removeAndRecycleView(View, Recycler)
         */
        public void recycleView(@NonNull View view) {
            // This public recycle method tries to make view recycle-able since layout manager
            // intended to recycle this view (e.g. even if it is in scrap or change cache)
            ViewHolder holder = getChildViewHolderInt(view);
            if (holder.isTmpDetached()) {
                removeDetachedView(view, false);
            }
            if (holder.isScrap()) {
                holder.unScrap();
            } else if (holder.wasReturnedFromScrap()) {
                holder.clearReturnedFromScrapFlag();
            }
            recycleViewHolderInternal(holder);
            // In most cases we dont need call endAnimation() because when view is detached,
            // ViewPropertyAnimation will end. But if the animation is based on ObjectAnimator or
            // if the ItemAnimator uses "pending runnable" and the ViewPropertyAnimation has not
            // started yet, the ItemAnimatior on the view may not be cleared.
            // In b/73552923, the View is removed by scroll pass while it's waiting in
            // the "pending moving" list of DefaultItemAnimator and DefaultItemAnimator later in
            // a post runnable, incorrectly performs postDelayed() on the detached view.
            // To fix the issue, we issue endAnimation() here to make sure animation of this view
            // finishes.
            //
            // Note the order: we must call endAnimation() after recycleViewHolderInternal()
            // to avoid recycle twice. If ViewHolder isRecyclable is false,
            // recycleViewHolderInternal() will not recycle it, endAnimation() will reset
            // isRecyclable flag and recycle the view.
            if (mItemAnimator != null && !holder.isRecyclable()) {
                mItemAnimator.endAnimation(holder);
            }
        }

这个方法用于回收分离的视图和把指定的视图放到缓存池中用于重新绑定和复用。最后调用了recycleViewHolderInternal方法,recycleViewHolderInternal这个方法时最终的回收方法,有的入口直接调用了这个方法

 /**
         * internal implementation checks if view is scrapped or attached and throws an exception
         * if so.
         * Public version un-scraps before calling recycle.
         */
        void recycleViewHolderInternal(ViewHolder holder) {
            if (holder.isScrap() || holder.itemView.getParent() != null) {
                throw new IllegalArgumentException(
                        "Scrapped or attached views may not be recycled. isScrap:"
                                + holder.isScrap() + " isAttached:"
                                + (holder.itemView.getParent() != null) + exceptionLabel());
            }

            if (holder.isTmpDetached()) {
                throw new IllegalArgumentException("Tmp detached view should be removed "
                        + "from RecyclerView before it can be recycled: " + holder
                        + exceptionLabel());
            }

            if (holder.shouldIgnore()) {
                throw new IllegalArgumentException("Trying to recycle an ignored view holder. You"
                        + " should first call stopIgnoringView(view) before calling recycle."
                        + exceptionLabel());
            }
            final boolean transientStatePreventsRecycling = holder
                    .doesTransientStatePreventRecycling();
            @SuppressWarnings("unchecked")
            final boolean forceRecycle = mAdapter != null
                    && transientStatePreventsRecycling
                    && mAdapter.onFailedToRecycleView(holder);
            boolean cached = false;
            boolean recycled = false;
            if (DEBUG && mCachedViews.contains(holder)) {
                throw new IllegalArgumentException("cached view received recycle internal? "
                        + holder + exceptionLabel());
            }
            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();
//mViewCacheMax的值是2,所以mCachedViews中最多缓存两条数据
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
//根据先进先出原则,把最老的从mCachedViews中放到mRecyclerPool中
                        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;
                    }
// 将最新的ViewHolder放入mCachedViews中
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
 //如果不设置往mCachedViews中放,就放入mRecyclerPool
                if (!cached) {
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            } else {
                // NOTE: A view can fail to be recycled when it is scrolled off while an animation
                // runs. In this case, the item is eventually recycled by
                // ItemAnimatorRestoreListener#onAnimationFinished.

                // TODO: consider cancelling an animation when an item is removed scrollBy,
                // to return it to the pool faster
                if (DEBUG) {
                    Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
                            + "re-visit here. We are still removing it from animation lists"
                            + exceptionLabel());
                }
            }
            // even if the holder is not removed, we still call this method so that it is removed
            // from view holder lists.
            mViewInfoStore.removeViewHolder(holder);
            if (!cached && !recycled && transientStatePreventsRecycling) {
                holder.mOwnerRecyclerView = null;
            }
        }

回收的逻辑比较简单,由 LayoutManager 来遍历移出屏幕的卡位,然后对每个卡位进行回收操作,回收时,都是把 ViewHolder 放在 mCachedViews 里面,如果 mCachedViews 满了,那就在 mCachedViews 里拿一个 ViewHolder 扔到 ViewPool 缓存里,然后 mCachedViews 就可以空出位置来放新回收的 ViewHolder 了。

总结

RecyclerView 滑动场景下的回收复用涉及到的结构体两个:
mCachedViews 和 RecyclerViewPool

mCachedViews 优先级高于 RecyclerViewPool,回收时,最新的 ViewHolder 都是往 mCachedViews 里放,如果它满了,那就移出一个扔到 ViewPool 里好空出位置来缓存最新的 ViewHolder。

复用时,也是先到 mCachedViews 里找 ViewHolder,但需要各种匹配条件,概括一下就是只有原来位置的卡位可以复用存在 mCachedViews 里的 ViewHolder,如果 mCachedViews 里没有,那么才去 ViewPool 里找。

在 ViewPool 里的 ViewHolder 都是跟全新的 ViewHolder 一样,只要 type 一样,有找到,就可以拿出来复用,重新绑定下数据即可。

问题解答

1.如果向下滑动,新一行的5个卡位的显示会去复用缓存的 ViewHolder,第一行的5个卡位会移出屏幕被回收,那么在这个过程中,是先进行复用再回收?还是先回收再复用?还是边回收边复用?

答:先复用再回收,新一行的5个卡位先去目前的 mCachedViews 和 ViewPool 的缓存中寻找复用,没有就重新创建,然后移出屏幕的那行的5个卡位再回收缓存到 mCachedViews 和 ViewPool 里面,所以新一行5个卡位和复用不可能会用到刚移出屏幕的5个卡位

2.在这个过程中,为什么当 RecyclerView 再次向上滑动重新显示第一行的5个卡位时,只有后面3个卡位触发了 onBindViewHolder() 方法,重新绑定数据呢?明明5个卡位都是复用的。

答:滑动场景下涉及到的回收和复用的结构体是 mCachedViews 和 ViewPool,前者默认大小为2,后者为5。所以,当第三行显示出来后,第一行的5个卡位被回收,回收时先缓存在 mCachedViews,满了再移出旧的到 ViewPool 里,所有5个卡位有2个缓存在 mCachedViews 里,3个缓存在 ViewPool,至于是哪2个缓存在 mCachedViews,这是由 LayoutManager 控制。

上面讲解的例子使用的是 GridLayoutManager,滑动时的回收逻辑则是在父类 LinearLayoutManager 里实现,回收第一行卡位时是从后往前回收,所以最新的两个卡位是0、1,会放在 mCachedViews 里,而2、3、4的卡位则放在 ViewPool 里。

所以,当再次向上滑动时,第一行5个卡位会去两个结构体里找复用,之前说过,mCachedViews 里存放的 ViewHolder 只有原本位置的卡位才能复用,所以0、1两个卡位都可以直接去 mCachedViews 里拿 ViewHolder 复用,而且这里的 ViewHolder 是不用重新绑定数据的,至于2、3、4卡位则去 ViewPool 里找,刚好 ViewPool 里缓存着3个 ViewHolder,所以第一行的5个卡位都是用的复用的,而从 ViewPool 里拿的复用需要重新绑定数据,才会这样只有三个卡位需要重新绑定数据。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342