RecyclerView之三级缓存源码解析

序言

  1. RecyclerView有三大典型的功能,一个是Recycler的缓存机制,一个LayoutManager的布局管理,一个ItemDecoration的分割线绘制;本文将结合源码讲解其缓存机制
  2. 更多相关的源码解析见RecyclerView之ItemDecoration

正文

缓存机制

(1). RecycledViewPool的缓存

  • RecycledViewPool也叫第三级缓存
  • 文档中说的是: 为多个RecyclerView提供的一个共用缓存池,如果想要通过RecyclerView缓存View的话,可以自己提供一个RecycledViewPool实例,并通过RecyclerView的setRecycledViewPool()方法来设置,如果不主动提供的话,RecyclerView会为自己主动创建一个
  • 首先来看其缓存方式: 其中有一个 SparseArray<ScrapData> 类型的mScrap来缓存ViewHolder,每一个View Type 类型的Item都会有一个该缓存(源码如下),默认最大容量为5,但是可以通过recyclerView.getRecycledViewPool().setMaxRecycledViews(int viewType, int max);来设置;(作者推荐的是:如果屏幕上有很多相同类型的ItemView同时改变,那么推荐将该容量设置大一些,但是如果有一种类型的ItemView很少出现,并且不超过一个,那么推荐将该容量设置为1,否则其迟早会被填满而造成内存浪费)
  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内存限制,所以产生了这样一个比HashMap轻量的类(具体可以参考博客)

  • 接下来看一下RecycledViewPool的存取方法;从这两个方法中,我们可以看出,在RecycledViewPool中缓存的ViewHolder之间是依靠 View Type 来区分的,也就是说,同一个View Type之间的ViewHolder缓存在RecycledViewPool中是没有区别的;如果我们没有重写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关联的操作了(因为不清除的话,在多个RecyclerView之间共享就会出现问题);那么到这里我们对于RecycledViewPool中的ViewHolder就有了大致的了解了,总结一下就是: 当一个ViewHolder被缓存进入该pool的时候,除了其自身的View Type以外,其自身与外界的绑定关系,flag标志,与原来RecyclerView的联系等信息都被清除了,那么理所当然的是,对于处于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的联系清除
        holder.mOwnerRecyclerView = null;
        //缓存入pool
        getRecycledViewPool().putRecycledView(holder);
    }
  • 下面我们应该顺着这条线索,继续搜索哪种情况下会将一个ViewHolder扔进RecycledViewPool中;这里笔者找到以下几种情况:
  1. 在View Cache(第一级缓存)中的Item被更新或者被删除时(即从Cache中移出的ViewHolder会进入pool中);可以看出的时,更新和删除操作时,将ViewHolder回收进pool中都是通过recycleCachedViewAt()方法,如下可知,其只是调用了上面的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();
        ...
    }
    
    //该方法中调用了上面所说的回收进pool中的清除工作,同时将Cache中的缓存删除
    void recycleCachedViewAt(int cachedViewIndex) {
        ....
        addViewHolderToRecycledViewPool(viewHolder, true);
        mCachedViews.remove(cachedViewIndex);
    }
  1. LayoutManager在pre_layout过程中添加View,但是在post_layout过程中没有添加该View;当然,在寻找该过程对应的源码的时候,我们首先应该弄清楚的是pre_layout和post_layout是什么(所以在继续讲解之前,笔者打算先讲一个小插曲)

(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(包括其位置和数量)来布局,同时其使用的Layout尺寸也是改变前的尺寸(这点可以从上面onMeasure()方法中对dispatchLayoutStep2()方法的注释可以看出(大意为: 预布局应该发生在旧的尺寸上),这是为了和正真改变后的布局相对比,来决定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. 接下来是实现真正的布局,即dispatchLayoutStep2()进行的post_layout;可以看出,这里主要是对子View进行Layout,需要注意的是,在onMeasure()中,在进行dispatchLayoutStep2()操作之前,还进行了mLayout.setMeasureSpecs(widthSpec, heightSpec);也就是设置改变后真正的布局尺寸;但是当查看LayoutManager的onLayoutChildren()方法时,我们发现其是一个空方法,所以应该找其实现类(这里以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()方法: 从其参数可以猜测的是,该方法与Item的填充和回收相关;其主要过程是通过下面while循环中不断的填充(layoutChunk)和回收Item(recycleByLayoutState)完成;而在recycleByLayoutState()中分为两种情况处理:即向上滚动和向下滚动,其中回收的条件是当Item滚动出屏幕且不可见时(在recycleViewsFromEnd()和recycleViewsFromStart()中都对滚动的边界做了判断),而最终回收调用的是recycleViewHolderInternal()方法;在recycleViewHolderInternal()中,其首先判断了如果第一级缓存满了的话,先将以前存入的Item移出,并存入Pool中,之后再缓存当前Item;这里也就是对应了RecycledViewPool缓存的第一种情况;还需要注意的是,当Item正在执行动画的时,会导致回收失败,此时会在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. 在我们继续进行下一步分析之前,笔者想先来总结一下上面我们在寻找pre_layout和post_layout区别的时候所经过的过程: 我们主要围绕的是RecyclerView的onMeasure()方法,经过了dispatchLayoutStep1()和dispatchLayoutStep2()两个主要的过程,前一个负责预布局(pre_layout),后一个负责真正的布局(post_layout);其实到这里,布局过程还没有真正的完成,因为我们还没有弄清楚的是Item的滚动动画
  1. onMeasure过程之后,我们应该将目光聚焦在layout过程,在RecyclerView的onLayout()方法中,其关键的是调用了dispatchLayout(),关于该方法,源码注释给出了明确的说明:dispatchLayout()方法中封装了与Item(出入)动画相关的操作,当重新布局(可能原因比如:Adapter改变,Item滑动等)之后,Item的改变类型大概有一下几种: (1). PERSISTENT: 即在pre_layout和post_layout中都是可见的(由animatePersistence()方法处理); (2). REMOVED: 在pre_layout中可见,但是被删除了(对应数据的删除)(由animateChange()方法处理);(3). ADDED: 在pre_layout中不存在,但是被添加进的Item(对应数据的添加)(由animateChange()方法处理); (4). DISAPPEARING: 数据集没有改变,但是Item由可见变为不可见(即Item滑动出屏幕)(由animateDisappearance()方法处理); (5). APPEARING: 数据集没有改变,但是Item由不可见变为可见(对应Item滑动进入屏幕)(由animateAppearance()方法处理);
  1. 但是我们最终追寻下去,可以看出的是在dispatchLayout()中,又将一系列处理完全交给了dispatchLayoutStep3()方法来处理;从下面代码中可以看出,其最终通过回调ViewInfoStore.ProcessCallback来处理上面的四种动画
  private void dispatchLayoutStep3() {
      ...
      // Step 4: Process view info lists and trigger animations
      mViewInfoStore.process(mViewInfoProcessCallback);
  }
  1. 到这里为止,我们对于pre_layout和post_layout的区别应该很清楚了;这里举个例子来进一步理解一下: 考虑一种情况,如果现在界面上有两个Item a,b,并且占满了屏幕,此时如果删除b使得c需要进入界面的话,那么我们虽然知道c的最终位置,但是我们如何知道c该从哪里滑入屏幕呢,很明显,不可能默认都从底部开始滑入,因为很明显的是还有其他情况;所以在这里Google的解决办法是请求两个布局: pre_layout和post_layout; 当Adapter改变即这里的b被删除的时候,作为一个事件触发,此时pre_layout将加载c(但是此时c仍然是不可见的),然后在post_layout中去加载改变后的Adapter的正常布局,通过前后两个布局对c位置的比较,我们就可以知道c该从哪里滑入;另外,还有一种情况是,如果b只是被改变了呢(并没有被删除),那么此时,pre_layout仍然会加载c,因为b的改变可能会引起b高度的改变而使得c有机会进入界面;但是,当Adapter改变完成之后,发现b并没有改变高度,换句话说,就是c还是不能进入界面的时候,此时Item c将被扔进该pool,这种情况也就是上面说的RecycledViewPool进行回收的第2种情况;话不多说,继续分析(万里长征还未过半...)
  1. 我们继续进入mViewInfoStore.process()方法,该方法属于ViewInfoStore类,对于该类的描述是:对View进行跟踪并运行相关动画,进一步解释就是执行Item改变过程中的一些动画;继续看其在process()方法做了什么:其实在该方法中进行了许多的情况的判断,这里笔者只是抽取出了对应当前情况的处理,可以看出,当similar to appear disappear but happened between different layout passes时,只是简单的调用了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缓存

  • 这里插曲可能稍微长了一点,但是,笔者感觉这是值得的;现在,让我们继续最初的话题: 什么情况下一个ViewHolder会被扔进Pool中呢?这里笔者再次回顾一下:
  1. 在View Cache中的Item被更新或者被删除时(存满溢出时)
  2. LayoutManager在pre_layout过程中添加View,但是在post_layout过程中没有添加该View(数据集改变,如删除)
  • 到这里RecyclerView的第三级缓存差不多就分析完了,接下来,我们再看一下与其紧密相关的第一级缓存

(3). View Cache缓存

  • View Cache也叫第一级缓存,主要指的是RecyclerView.Recycler中的mCachedViews字段,它是一个ArrayList,不区分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>();
}
  • 现在,我们需要看的是什么时候,mCachedViews会缓存ViewHolder,通过追踪,可以发现,只有在recycleViewHolderInternal()中调用了mCachedViews.add(),而该方法上面分析第三级缓存的时候,分析的是,当Item被移出屏幕区域时,先是缓存进了mCachedViews中,因为处于mCachedViews中的ViewHolder是希望被原样重用的;之所以这样说,是因为从 recycleViewHolderInternal() 的源码中可以看出,在 mCachedViews.add() 之前并没有像上面存入第三级缓存之前那样进行一系列的清理工作,也就是说ViewHolder相关的和重要的position,flag等标志都一并被缓存了;那么,从mCachedViews中取出的ViewHolder就不需要再进行绑定操作而可以直接使用了(实际上所以我们期望的也是在mCachedViews中的ViewHolder能够被重用,并且还是在它原来的位置被重用,这样就不需要再去bind了;)
  • 至于mChangedScrap和mAttachedScrap缓存的话,我们也可以从其add()方法入手(如下),可以看出,一个ViewHolder是缓存进入mChangedScrap还是mAttachedScrap,取决于其状态,如果一个Item被移除或者非法(如:与其view type 类型不再相符等),那么就会被放进mAttachedScrap中,反之,则进入mChangedScrap;说的更明显一点就是,如果如果一个Item被移除,那么就会被放进mAttachedScrap中,如果调用了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的类,使用的时候,只需要实现一个View getViewForPositionAndType(Recycler recycler, int position, int type);方法
  • 下面,我们通过一个例子来看一下什么时候可以使用该缓存:(: 下面的例子来源于文末的参考文章)考虑现在有这样的一些Item
  1. 其position固定(比如广告之类)
  2. 不会改变(view type等)
  3. 数量合理,以便可以保存在内存中
    现在,为了避免这些Item的重复绑定,就可以使用ViewCacheExtension(需要注意的是,这里不能使用RecycledViewPool,因为其缓存的ViewHolder需要重新绑定,同时也能使用View Cache,因为其中的ViewHolder是不区分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). 小结

  • 到这里为止,RecyclerView三级缓存相关的源码分析就结束了;但是由于笔者能力有限,很多细节和理解可能不到位,更多的还是需要自己动手多看源码:)

(6). 参考文章

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