深入简出RecyclerView缓存机制

更详细文档入口

1.RecyclerView基础组件

核心类 作用
LayoutManager 管理ItemView的布局显示
ViewHodler ItemView的容器
ItemDecoration ItemView边界绘制,比如分割线
ItemAnimator ItemView的添加、删除、移动动画
Recycler 管理ItemView的回收和复用

2.RecyclerView与ListView的差异

实现了可回收复用的ViewHolder
实现的Adapter返回的是ViewHolder而不是View
通过LayoutManager来统一管理布局
实现了针对Item的增删动画ItemAnimator类
实现了针对Item间隔的配置ItemDecoration类
支持局部刷新

3.RecyclerView的四级缓存

层级 参数 使用场景
一级 mAttachedScrap 添加和删除ItemView刷新界面时使用
一级 mChangedScrap 执行动画时使用
二级 mCachedViews 默认容量是2,超过时会被保存到mRecyclerPool,ItemView超出屏幕范围后先回收到mCachedViews
三级 mViewCacheExtension 开发者自己扩展使用
四级 mRecyclerPool 当mCachedViews缓存满时,默认每个type类型最多缓存5个

4.RecyclerView缓存策略分析

Recyclerview版本:

implementation "androidx.recyclerview:recyclerview:1.2.1"

接下来通过一个简单的Demo来验证分析RecyclerView的缓存策略

    private void initView(View root) {
        mRecyclerView = root.findViewById(R.id.recycler_view);
        mRecyclerView.setAdapter(new FVAdapter());
        GridLayoutManager layoutManager = new GridLayoutManager(getActivity(), 1);
        layoutManager.setItemPrefetchEnabled(false);
        mRecyclerView.setLayoutManager(layoutManager);
    }

    private void initData() {
        mData = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            mData.add(i);
        }
    }

    public class FVAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

        @NonNull
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            Button view = new Button(getActivity());
            view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 216));
            Log.d(TAG, "onCreateViewHolder");
            return new VH(view);
        }

        @SuppressLint({"SetTextI18n", "ResourceAsColor"})
        @Override
        public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
            VH vh = (VH) holder;
            Button view = (Button) vh.itemView;
            view.setText("View Position:" + position + "\nHolder Index:" + vh.vh_index);
            Log.d(TAG, "onBindViewHolder view position:" + position + " Holder Index:" + vh.vh_index);
        }

        @Override
        public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
            super.onViewRecycled(holder);
            VH vh = (VH) holder;
            Log.d(TAG, "onViewRecycled Holder Index:" + vh.vh_index);
        }

        @Override
        public int getItemCount() {
            return mData.size();
        }
    }

    public int index = 0;

    public class VH extends RecyclerView.ViewHolder {
        public int vh_index;

        public VH(@NonNull View itemView) {
            super(itemView);
            vh_index = index++;
        }
    }

Demo代码很简单,创建1000个Item,每个Item上显示位置和ViewHolder的下标。每屏最多显示5个Item:

mAttachedScrap、mChangedScrap仅在触发onLayout时影响缓存,在滚动场景下,分析流程时可先排除对缓存的影响。

onLayout.png

另外,LayoutManager有两个默认属性,会影响缓存策略:

        public final void setItemPrefetchEnabled(boolean enabled) {
            if (enabled != mItemPrefetchEnabled) {
                mItemPrefetchEnabled = enabled;
                mPrefetchMaxCountObserved = 0;
                if (mRecyclerView != null) {
                    mRecyclerView.mRecycler.updateViewCacheSize();
                }
            }
        }
    /**
     * Number of items to prefetch when first coming on screen with new data.
     */
    private int mInitialPrefetchItemCount = 2;

    public void setInitialPrefetchItemCount(int itemCount) {
        mInitialPrefetchItemCount = itemCount;
    }

从定义上看,mCacheViews的size默认为2,mRecyclerPool的每个Type对应的size默认为5个。

  int mViewCacheMax = DEFAULT_CACHE_SIZE;
  static final int DEFAULT_CACHE_SIZE = 2;
  public static class RecycledViewPool {
        private static final int DEFAULT_MAX_SCRAP = 5;
        ...
  }

为了方便分析,设置setItemPrefetchEnabled(false),排除缓存size动态变化对分析流程的额外影响:

Recycler.png
1 mCacheViews和mRecyclerPool中未命中缓存,新建一个ViewHolder
2 mCacheViews中有缓存,但是位置不匹配,未命中缓存,新建一个ViewHolder
3 mCacheViews中满缓存,但是位置不匹配,未命中缓存,新建一个ViewHolder
4 mCacheViews中满缓存,但是位置不匹配;mRecyclerPool有缓存,命中type对应的缓存,使用缓存且调研onBindViewHolder方法

按以上方式分析,理论上在滚动时最多只需要创建8个(5 + 2 + 1)ViewHolder,但事实上是这样吗?
首先看下手机端的手指滑动场景:

 14:50:20.264   onCreateViewHolder
 14:50:20.266   onBindViewHolder view position:0 Holder Index:0
 14:50:20.278   onCreateViewHolder
 14:50:20.278   onBindViewHolder view position:1 Holder Index:1
 14:50:20.283   onCreateViewHolder
 14:50:20.284   onBindViewHolder view position:2 Holder Index:2
 14:50:20.289   onCreateViewHolder
 14:50:20.290   onBindViewHolder view position:3 Holder Index:3
 14:50:20.295   onCreateViewHolder
 14:50:20.295   onBindViewHolder view position:4 Holder Index:4
 14:50:26.702   onCreateViewHolder
 14:50:26.704   onBindViewHolder view position:5 Holder Index:5
 14:50:26.749   onCreateViewHolder
 14:50:26.751   onBindViewHolder view position:6 Holder Index:6
 14:50:26.779   onCreateViewHolder
 14:50:26.780   onBindViewHolder view position:7 Holder Index:7
 14:50:26.786   onViewRecycled Holder Index:0
 14:50:26.788   onBindViewHolder view position:8 Holder Index:0
 14:50:26.801   onViewRecycled Holder Index:1
 14:50:26.803   onBindViewHolder view position:9 Holder Index:1
 14:50:26.810   onViewRecycled Holder Index:2
 14:50:26.811   onBindViewHolder view position:10 Holder Index:2
 14:50:26.823   onViewRecycled Holder Index:3
 14:50:26.825   onBindViewHolder view position:11 Holder Index:3
 14:50:26.840   onViewRecycled Holder Index:4
 14:50:26.843   onBindViewHolder view position:12 Holder Index:4
 14:50:26.874   onViewRecycled Holder Index:5
 14:50:26.876   onBindViewHolder view position:13 Holder Index:5
 14:50:26.890   onViewRecycled Holder Index:6
 14:50:26.892   onBindViewHolder view position:14 Holder Index:6
 14:50:26.907   onViewRecycled Holder Index:7
 14:50:26.909   onBindViewHolder view position:15 Holder Index:7
 14:50:26.923   onViewRecycled Holder Index:0
 14:50:26.926   onBindViewHolder view position:16 Holder Index:0

手指无论如何滚动,最多只创建8个ViewHolder,和预期一致。然而,通过实体键(比如电视端使用遥控器)却出现了不一致的情况:

1.慢速滚动

17:20:56.628  onCreateViewHolder
17:20:56.628  onBindViewHolder view position:0 Holder Index:0
17:20:56.631  onCreateViewHolder
17:20:56.631  onBindViewHolder view position:1 Holder Index:1
17:20:56.634  onCreateViewHolder
17:20:56.635  onBindViewHolder view position:2 Holder Index:2
17:20:56.639  onCreateViewHolder
17:20:56.639  onBindViewHolder view position:3 Holder Index:3
17:20:56.644  onCreateViewHolder
17:20:56.644  onBindViewHolder view position:4 Holder Index:4
17:20:59.294  onCreateViewHolder
17:20:59.294  onBindViewHolder view position:5 Holder Index:5
17:21:00.194  onCreateViewHolder
17:21:00.195  onBindViewHolder view position:6 Holder Index:6
17:21:00.886  onCreateViewHolder
17:21:00.886  onBindViewHolder view position:7 Holder Index:7
17:21:01.157  onViewRecycled Holder Index:0
17:21:01.814  onBindViewHolder view position:8 Holder Index:0
17:21:02.073  onViewRecycled Holder Index:1
17:21:02.624  onBindViewHolder view position:9 Holder Index:1
17:21:02.890  onViewRecycled Holder Index:2
17:21:04.544  onBindViewHolder view position:10 Holder Index:2
17:21:04.808  onViewRecycled Holder Index:3
17:21:06.044  onBindViewHolder view position:11 Holder Index:3
17:21:06.307  onViewRecycled Holder Index:4
17:21:06.973  onBindViewHolder view position:12 Holder Index:4
17:21:07.240  onViewRecycled Holder Index:5
17:21:07.814  onBindViewHolder view position:13 Holder Index:5
17:21:08.073  onViewRecycled Holder Index:6
17:21:08.714  onBindViewHolder view position:14 Holder Index:6
17:21:08.974  onViewRecycled Holder Index:7
17:21:09.583  onBindViewHolder view position:15 Holder Index:7
17:21:09.857  onViewRecycled Holder Index:0
17:21:10.484  onBindViewHolder view position:16 Holder Index:0
17:21:10.758  onViewRecycled Holder Index:1

在实体键慢速滚动场景下,结果与预期一致。

2.快速滚动

17:22:48.759 onCreateViewHolder
17:22:48.760 onBindViewHolder view position:0 Holder Index:0
17:22:48.762 onCreateViewHolder
17:22:48.763 onBindViewHolder view position:1 Holder Index:1
17:22:48.765 onCreateViewHolder
17:22:48.766 onBindViewHolder view position:2 Holder Index:2
17:22:48.769 onCreateViewHolder
17:22:48.770 onBindViewHolder view position:3 Holder Index:3
17:22:48.773 onCreateViewHolder
17:22:48.773 onBindViewHolder view position:4 Holder Index:4
17:22:51.242 onCreateViewHolder
17:22:51.243 onBindViewHolder view position:5 Holder Index:5
17:22:51.298 onCreateViewHolder
17:22:51.298 onBindViewHolder view position:6 Holder Index:6
17:22:51.347 onCreateViewHolder
17:22:51.348 onBindViewHolder view position:7 Holder Index:7
17:22:51.399 onCreateViewHolder
17:22:51.400 onBindViewHolder view position:8 Holder Index:8
17:22:51.455 onCreateViewHolder
17:22:51.455 onBindViewHolder view position:9 Holder Index:9
17:22:51.510 onCreateViewHolder
17:22:51.510 onBindViewHolder view position:10 Holder Index:10
17:22:51.519 onViewRecycled Holder Index:0
17:22:51.551 onBindViewHolder view position:11 Holder Index:0
17:22:51.577 onViewRecycled Holder Index:1
17:22:51.596 onBindViewHolder view position:12 Holder Index:1
17:22:51.627 onViewRecycled Holder Index:2
17:22:51.647 onBindViewHolder view position:13 Holder Index:2
17:22:51.660 onViewRecycled Holder Index:3
17:22:51.699 onBindViewHolder view position:14 Holder Index:3
17:22:51.708 onViewRecycled Holder Index:4
17:22:51.748 onBindViewHolder view position:15 Holder Index:4
17:22:51.758 onViewRecycled Holder Index:5

一共创建了11个ViewHolder,和预期不符,这是为什么呢?

首先,先看看按键事件是如何实现RecyclerView滚动的:

图一

图一中标记1,会根据按键方向找到合适的焦点:

图二

找不到焦点时,会先去查找mCachesView中是否有对应Position的缓存,如果没有再从mRecyclerPool中查找是否对应Type的缓存,如果没有命中缓存,则会新建一个ViewHolder,然后bindViewHolder,这个ViewHolder就是即将获取焦点的ItemView。

查找到可获取焦点的ItemView后,根据图一中的流程,因为要requestFocus,最终会通过标记2流程实现RecyclerView的实现。

再看下postOnAnimation实现:

    public void postOnAnimation(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.mChoreographer.postCallback(
                    Choreographer.CALLBACK_ANIMATION, action, null);
        } else {
            // Postpone the runnable until we know
            // on which thread it needs to run.
            getRunQueue().post(action);
        }
    }

简述下Choreographer绘制流程:

图三.Choreographer绘制流程

因此图一标记2位置实现RecyclerView的滚动是在doFrame队列中CALLBACK_ANIMATION阶段时才得到了处理。

在此滚动阶段才可能完成ViewHolder的回收,流程如下:

图四

以下是一张比较抽象的流程图,主要说明在按键事件下Recycler的大概流程:

图五

有一个关键函数fill,在图二、图四中都会调用到:

    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        // max offset we should set is mFastScroll + available
        final int start = layoutState.mAvailable;
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            // TODO ugly bug fix. should not happen
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutState(recycler, layoutState);
        }
        ...
    }

图二流程调用如下,在未找到可获取焦点子控件失败时尝试填充新的子控件,其中滚动的最大距离maxScroll是RecyclerView最高高度的MAX_SCROLL_FACTOR(三分之一):

    private static final float MAX_SCROLL_FACTOR = 1 / 3f;
    ...
   @Override
    public View onFocusSearchFailed(View focused, int focusDirection,
            RecyclerView.Recycler recycler, RecyclerView.State state) {
        ...
        final int maxScroll = (int) (MAX_SCROLL_FACTOR * mOrientationHelper.getTotalSpace());
        updateLayoutState(layoutDir, maxScroll, false, state);
        // 在调用fill前把mScrollingOffset置为无效,此次不会回收ViewHolder
        mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
        mLayoutState.mRecycle = false;
        fill(recycler, mLayoutState, state, true);
        ...
    }

图四流程调用如下,在滚动时尝试填充新的子控件:

    int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
        ...
        updateLayoutState(layoutDirection, absDelta, true, state);
        final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);
        ...
        final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
        // 真正实现视觉上的滚动效果
        mOrientationHelper.offsetChildren(-scrolled); 
        ...
    }

    private void updateLayoutState(int layoutDirection, int requiredSpace,
            boolean canUseExistingSpace, RecyclerView.State state) {
        ...
        mLayoutState.mScrollingOffset = scrollingOffset;
    }

从上面的源码可以看到只有在完成滚动后才可能进行ViewHolder的回收。
可知快速滚动场景下,由于按键事件不断触发,若不能命中缓存,就会不断新建ViewHolder;而只有RecyclerView的滚动需要等到下一次doFrame时才会执行,此后才会决定是否回收ViewHolder。

那么来分析下RecycleView的滚动的具体实现。
实际上RecycleView是在doFrame中一点一点像素滚动的,滚动的距离是通过时间和距离计算出来的:

        //计算此次滚动所需要的时间
        private int computeScrollDuration(int dx, int dy) {
            final int absDx = Math.abs(dx);
            final int absDy = Math.abs(dy);
            final boolean horizontal = absDx > absDy;
            final int containerSize = horizontal ? getWidth() : getHeight();

            float absDelta = (float) (horizontal ? absDx : absDy);
            final int duration = (int) (((absDelta / containerSize) + 1) * 300);

            return Math.min(duration, MAX_SCROLL_DURATION);
        }

可以看到动画的时间是通过滚动距离计算出来的,并且一定大于300ms。

   //计算此次滚动时刻可以滚动的距离
   public boolean computeScrollOffset() {
        if (isFinished()) {
            return false;
        }

        switch (mMode) {
            case SCROLL_MODE:
                long time = AnimationUtils.currentAnimationTimeMillis();
                // Any scroller can be used for time, since they were started
                // together in scroll mode. We use X here.
                final long elapsedTime = time - mScrollerX.mStartTime;

                final int duration = mScrollerX.mDuration;
                if (elapsedTime < duration) {
                    final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration);
                    mScrollerX.updateScroll(q);
                    mScrollerY.updateScroll(q);
                } else {
                    abortAnimation();
                }
                break;
                ...
        }

        return true;
    }

根据图四的流程,再来看看回收ViewHodler的条件, 以列表向上滚动为例:

    private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,
                                       int noRecycleSpace) {
        ...
        // ignore padding, ViewGroup may not clip children.
        final int limit = scrollingOffset - noRecycleSpace;            ...
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                if (mOrientationHelper.getDecoratedEnd(child) > limit
                        || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                    // stop here
                    recycleChildren(recycler, 0, i);
                    return;
                }
            }
        }
    }
    ...
    private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
        ...
        if (endIndex > startIndex) {
            for (int i = endIndex - 1; i >= startIndex; i--) {
                removeAndRecycleViewAt(i, recycler);
            }
        } else {
            for (int i = startIndex; i > endIndex; i--) {
                removeAndRecycleViewAt(i, recycler);
            }
        }
    }

被回收的ViewHolder根据type类型放在List的尾部

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

复用ViewHolder时根据type类型取List尾部的第一个

        public ViewHolder getRecycledView(int viewType) {
            final ScrapData scrapData = mScrap.get(viewType);
            if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
                final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
                for (int i = scrapHeap.size() - 1; i >= 0; i--) {
                    if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
                        return scrapHeap.remove(i);
                    }
                }
            }
            return null;
        }

回收的关键在于

mOrientationHelper.getDecoratedEnd(child) > limit || mOrientationHelper.getTransformedEndWithDecoration(child) > limit

在这篇文章的Demo中,以上函数是获取child的最底部(包括bottomMargin)位置,当找到第一个底部大于limit的View时,可以回收所有在这个视图以上的ItemView。

其中limit = scrollingOffset - noRecycleSpace,并且noRecycleSpace在本文中为0,从updateLayoutState可以看出scrollingOffset就是在不添加新的View前可滚动的距离。

FastScroll

上图右侧是用户长按方向键下,每响应一个KeyEvent.KEYCODE_DPAD_DOWN事件,会在RecyclerView中addView一次(有可使用缓存则使用缓存,未命中缓存则新建)。左侧是触发滚动流程,在滚动发生前检查是否有子View的底部高度大于limit,从图中可见即是否大于目标滚动距离,只有大于目标滚动距离时才开始回收ViewHolder。因为在滚动动画执行期间(不小于300ms),如果有按键事件再次触发focusSearch,就会因为addView刷新mScrollingOffset,本应该在动画结束时触发回收的条件将不满足,所以就出现了缓存Size超过预期的现象。

为什么在手指滑动场景时没有这样的现象?

FastFling

从这里看到相比按键事件,在事件触发后并不需要立即就填充子View,而是在doFrame期间填充的,这样就不会出现limit滚动期间被外界修改的问题,因此ViewHolder的Size总是符合预期的。

5.多行多列

试想一下,长按方向键下快速滚动场景下,ViewHolder的创建和回收情况是怎样的?

优化思路

理论上可以优化成Fling滚动,等到滚动结束后再计算焦点所在的位置。

        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            boolean mScrolled = false;

            @Override
            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
                    mScrolled = false;
                    View item = recyclerView.findChildViewUnder(0, rowHeight);
                    if (item != null) {
                        item.requestFocus();
                    }
                }
            }

            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                if (dx != 0 || dy != 0) {
                    mScrolled = true;
                }
            }
        });
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (event.getRepeatCount() != 0) {
            if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
                mRecyclerView.fling(0, rowHeight);
            } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
                mRecyclerView.fling(0, -rowHeight);
            }
            return true;
        } else {
            return false;
        }
    }
优化前
优化后
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335