RecyclerView的复用(缓存)和回收机制

本篇文章从源码的角度分析RecyclerView的复用回收的原理机制

复用机制

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

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

     ViewHolder tryGetViewHolderForPositionByDeadline(int position,
              boolean dryRun, long deadlineNs) {
      }

RecyclerView的复用机制源码就在tryGetViewHolderForPositionByDeadline方法中通过复用或创建拿到ViewHolder,然后通过 ViewHolder.itemView来获取需要的View

RecyclerView缓存ViewHolder的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;
   }

mAttachedScrap:保存还在屏幕在上的Item的ViewHolder。比如在onLayout()时会先把ItemView去掉,在重新add添加进去,这个List可能就保存这种临时的ViewHolder。

mChangedScrap: 保存那些被标记跟新、有动画的ViewHolder

mCachedViews: 保存刚刚滑出屏幕上的ViewHolder,大小默认为2个(可以通过setItemViewCacheSize来设置大小)。这里的ViewHolder通过position或id可以找到,且可以直接拿来复用,不需要执行onBindViewHolder。

RecycledViewPool:根据ViewType保存被滑出屏幕的ViewHolder,每种ViewType最多保存五个ViewHodler。复用这里的ViewHolder数据已被清除,需要执行onBindViewHolder()方法。

RecyclerView大致绘制流程:

  1. RecyclerView的RequestLayout开始绘制
  2. layout开始,调用LayoutManager.fill()方法将RecyclerView填满
  3. LayoutManager.fill会循环调用layoutChunk()方法生成View并addView到RV
  4. layoutChunk调用tryGetViewHolderForPositionByDeadline()方法复用或创建一个ViewHolder。

tryGetViewHolderForPositionByDeadline源码

      ViewHolder tryGetViewHolderForPositionByDeadline(int position,
              boolean dryRun, long deadlineNs) {
          if (position < 0 || position >= mState.getItemCount()) {
              throw new IndexOutOfBoundsException("Invalid item position " + position
                      + "(" + position + "). Item count:" + mState.getItemCount()
                      + exceptionLabel());
          }
      检查position的下标,越界后就抛异常

      ViewHolder tryGetViewHolderForPositionByDeadline(int position,
              boolean dryRun, long deadlineNs) {
          //...省略position越界检查代码
          boolean fromScrapOrHiddenOrCache = false;
          ViewHolder holder = null;
          // 0) If there is a changed scrap, try to find from there
          //.isPreLayout()要设置了动画才为true,所以一下代码默认不执行
          if (mState.isPreLayout()) {
              holder = getChangedScrapViewForPosition(position);
              fromScrapOrHiddenOrCache = holder != null;
          }

如果isPreLayout为true就到ChangedScrap里面找,看看isPreLayout在什么地方赋值:

  protected void onMeasure(int widthSpec, int heightSpec) {
  //...省略无关代码
  if (mLayout.mAutoMeasure) { 
      //... 
  } else { 
      // custom onMeasure 
      if (mAdapterUpdateDuringMeasure) { 
          if (mState.mRunPredictiveAnimations) { 
              mState.mInPreLayout = true; 
          } else { 
              // consume remaining updates to provide a consistent state with the layout pass. 
              mState.mInPreLayout = false; 
          } 
      }  
  } 
  //... 
  mState.mInPreLayout = false; // clear 
  }

private void dispatchLayoutStep1() {
mState.mInPreLayout = mState.mRunPredictiveAnimations
}

private void dispatchLayoutStep2() {
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
}

可以看到在执行mLayout.onLayoutChildren之前会把mInPreLayout = false。所以暂时不用管它。继续扫描代码。


  ViewHolder tryGetViewHolderForPositionByDeadline(int position,
              boolean dryRun, long deadlineNs) {            
           //...省略其他代码
          // 1) Find by position from scrap/hidden list/cache
          if (holder == null) {
              // 分别从mAttachedScrap、HiddenViews(dryRun为false)、mCachedViews获取复用的Viewholder
              holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
             
              //如果拿到ViewHolder后,
              if (holder != null) {
                  // 验证ViewHolder能否直接拿来复用
                  if (!validateViewHolderForOffsetPosition(holder)) {
                      // recycle holder (and unscrap if relevant) since it can't be used
                      if (!dryRun) {
                          // we would like to recycle this but need to make sure it is not used by
                          // animation logic etc.
                          // 如果不能复用,就放到CacheView后ViewPool里
                          holder.addFlags(ViewHolder.FLAG_INVALID);
                          if (holder.isScrap()) {
                              removeDetachedView(holder.itemView, false);
                              holder.unScrap();
                          } else if (holder.wasReturnedFromScrap()) {
                              holder.clearReturnedFromScrapFlag();
                          }
                          recycleViewHolderInternal(holder);
                      }
                      holder = null;
                  } else {
                      fromScrapOrHiddenOrCache = true;
                  }
              }
          }
  }

我们跟进去看看获取复用的代码:

      ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
          final int scrapCount = mAttachedScrap.size();

          // 遍历AttachedScrap,寻找符合位置与状态的ViewHolder
          for (int i = 0; i < scrapCount; i++) {
              final ViewHolder holder = mAttachedScrap.get(i);
              if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                      && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
                  holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                  return holder;
              }
          }

          // 如果dryRun为false,尝试从HiddenViews复用ViewHolder
          if (!dryRun) {
              View view = mChildHelper.findHiddenNonRemovedView(position);
              if (view != null) {
                  // This View is good to be used. We just need to unhide, detach and move to the
                  // scrap list.
                  final ViewHolder vh = getChildViewHolderInt(view);
                  mChildHelper.unhide(view);
                  int layoutIndex = mChildHelper.indexOfChild(view);
                  if (layoutIndex == RecyclerView.NO_POSITION) {
                      throw new IllegalStateException("layout index should not be -1 after "
                              + "unhiding a view:" + vh + exceptionLabel());
                  }
                  mChildHelper.detachViewFromParent(layoutIndex);
                  scrapView(view);
                  vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
                          | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                  return vh;
              }
          }

          // 遍历CachedViews,尝试从位置和状态符合的ViewHolder
          final int cacheSize = mCachedViews.size();
          for (int i = 0; i < cacheSize; i++) {
              final ViewHolder holder = mCachedViews.get(i);
              // invalid view holders may be in cache if adapter has stable ids as they can be
              // retrieved via getScrapOrCachedViewForId
              if (!holder.isInvalid() && holder.getLayoutPosition() == position
                      && !holder.isAttachedToTransitionOverlay()) {
                  if (!dryRun) {
                      mCachedViews.remove(i);
                  }
                  if (DEBUG) {
                      Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                              + ") found match in cache: " + holder);
                  }
                  return holder;
              }
          }
          return null;
      }

从上面的代码可以看到,先从mAttachedScrap遍历寻找position一致的,状态有效、没有更新的ViewHolder。如果dryRun==false,则从HiddenViews中寻找position一致的,状态有效、没有更新的ViewHolder。最后,从mCachedViews中寻找position一致的,状态有效、没有更新的ViewHolder。
如果从上面三个List寻找到ViewHolder,则再次判断ViewHolder是否符合条件,可以直接复用。不符合条件则置holder为null。


如果上面的List中通过position找不到ViewHolder,并且你设置了 adapter.setHasStableIds(true)那么就通过id来找。

  ViewHolder tryGetViewHolderForPositionByDeadline(int position,
            boolean dryRun, long deadlineNs) {    
          //...省略看过代码
          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()) {
                 //根据id和type去AttachView和CacheView中寻找ViewHolder
                  holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                          type, dryRun);
                  if (holder != null) {
                      // update position
                      holder.mPosition = offsetPosition;
                      fromScrapOrHiddenOrCache = true;
                  }
              }
  }

getScrapOrCachedViewForId和getScrapOrHiddenOrCachedHolderForPosition的寻找差不多,这里就不贴出代码分析。


   ViewHolder tryGetViewHolderForPositionByDeadline(int position,
             boolean dryRun, long deadlineNs) {    
                   //...省略看过代码
                   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());
                       }
                   }
               }     
   }

mViewCacheExtension是给我们自定义复用ViewHolder的,一般没用,继续往下。


   ViewHolder tryGetViewHolderForPositionByDeadline(int position,
             boolean dryRun, long deadlineNs) {    
               //...省略看过代码
                final int type = mAdapter.getItemViewType(offsetPosition);
               if (holder == null) { // fallback to pool
                   if (DEBUG) {
                       Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                               + position + ") fetching from shared pool");
                   }
                   // 从ViewPool寻找复用
                   holder = getRecycledViewPool().getRecycledView(type);
                   if (holder != null) {
                       // 如果从ViewPool中找到holder,则置空里面的数据
                       holder.resetInternal();
                       if (FORCE_INVALIDATE_DISPLAY_LIST) {
                           invalidateDisplayListInt(holder);
                       }
                   }
               }   
   }

从ViewPool中根据ViewType中获取一个ViewHolder,并且在得到ViewHolder后,将ViewHolder的相关数据重置,这也就是为什么要重新执行onBindViewHolder()。看看如何获取ViewHolder.

      public ViewHolder getRecycledView(int viewType) {
          final ScrapData scrapData = mScrap.get(viewType);
          if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
              final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
               // 把List的最后一个返回并remove
              for (int i = scrapHeap.size() - 1; i >= 0; i--) {
                  if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
                      return scrapHeap.remove(i);
                  }
              }
          }
          return null;
      }

ViewPool的存储结构为<type, ScrapData>,先根据type拿到对应的ScrapData类,ScrapData类中存储一个ViewHolder集合变量,然后从这个集合中获取最后下标的数据返回并移除。


如果在ViewPool中都没有找到ViewHolder,那就继续往下看。

   ViewHolder tryGetViewHolderForPositionByDeadline(int position,
             boolean dryRun, long deadlineNs) {    
               //...省略看过代码
               if (holder == null) {
                  //...省略无关代码
                   holder = mAdapter.createViewHolder(RecyclerView.this, type);
                  //...省略无关代码
               bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
               }
               }
   }

这里就开始调用mAdapter.createViewHolder来创建ViewHolder了。这里就是我们的adapter的流程了,创建一个VH,而不是复用VH。

到这里,方法tryGetViewHolderForPositionByDeadline()的复用的ViewHolder机制就分析完成了。
复用机制方法总结:

  • RecyclerView可分为四级缓存:
  1. mChangeScrap和mAttachScrap为一级缓存:主要复用还在屏幕上的ViewHolder。mChangeScrap通过position进行判断,mAttachScrap可以通过position或id进行判断ViewHolder。
  2. mCacheView为二级缓存:默认大小为2,主要存储刚刚滑出屏幕的ViewHolder。 第一级和第二级缓存都是通过postion或id进行判断ViewHolder,而且不需要执行onCreateViewHolder和onBindViewHolder两个方法
  3. mViewCacheExtension为三级缓存:可以自己定义如何复用,但一般不使用。
  4. mRecylerPool为四级缓存:这里是最后找复用的地方,每个ViewType可以有装5个ViewHolder,根据ViewType来查找是否有复用的ViewHolder。如果有可复用的ViewHolder,则将ViewHolder的数据重置,并执行onBindViewHolder方法。
  • 源码角度分析复用:
  1. 如果mInPreLayout == true时,才根据position查询mChangeScrap是否有可复用的ViewHolder。mInPreLayout一般在有动画时才为true。否则不复用mChangeView。
  2. 根据position分别查询mAttachScrap、hiddenViews、mCacheView是否有可复用的ViewHolder。
  3. 如果设置了mHasStableIds==true,则根据id查询mAttachScrap、mCacheView是否有可复用的ViewHolder。
  4. 根据typemRecycerPool查询可复用的ViewHolder,并且情况数据,重新bind。
  5. 复用流程都没有找到ViewHolder,则执行正常的创建ViewHolder流程。

回收机制

      private void scrapOrRecycleView(Recycler recycler, int index, View view) {
          final ViewHolder viewHolder = getChildViewHolderInt(view);
          if (viewHolder.shouldIgnore()) {
              if (DEBUG) {
                  Log.d(TAG, "ignoring view " + viewHolder);
              }
              return;
          }
          if (viewHolder.isInvalid() && !viewHolder.isRemoved()
                  && !mRecyclerView.mAdapter.hasStableIds()) {
              // 移除相关位置的View
              removeViewAt(index);
              // 拿去CacheView、RecyclerPool中回收
              recycler.recycleViewHolderInternal(viewHolder);
          } else {
              //增加相关位置的view 
              detachViewAt(index);
              //将ViewHolder回收到mAttachScrap或mChangeScrap中
              recycler.scrapView(view);
              mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
          }
      }

scrapOrRecycleView方法中,会根据ViewHolder的状态进行不同的回收操作。如果ViewHolder的状态是失效的、没有被移除、没有设置StableIdRemove相应位置的View,并回收到缓存池中。否则,detach相应位置的View,并回收到临时缓存中
remove是真正将子View从RecyclerView中移除,而detach子View还未和RecycelerView分离

所以,如果设置了StableIds(),将回收到临时缓存中。在复用的时候,会先根据position在临时缓存中查找,如果还没找到则根据id来临时缓存中查找。


先来看看移除View时,recycleViewHolderInternal怎么回收?

      void recycleViewHolderInternal(ViewHolder holder) {
           // ...省略代码

          // 如果CachedViews已经包含这个VH就抛异常
          if (DEBUG && mCachedViews.contains(holder)) {
              throw new IllegalArgumentException("cached view received recycle internal? "
                      + holder + exceptionLabel());
          }
          if (forceRecycle || holder.isRecyclable()) {
               // mViewCacheMax默认为2,并且VH为已经更新或移除
              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();
                  // 移调最前面的0个去RecyclerPool缓存
                  if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                      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增加到CachedViews
                  mCachedViews.add(targetCacheIndex, holder);
                  cached = true;
              }
             // 否则增加到RecycledViewPool中
              if (!cached) {
                  addViewHolderToRecycledViewPool(holder, true);
                  recycled = true;
              }
          } 
        //...省略代码
      }
      void recycleCachedViewAt(int cachedViewIndex) {
          if (DEBUG) {
              Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
          }
          ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
          if (DEBUG) {
              Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);
          }
          // 添加到RecycledViewPool中
          addViewHolderToRecycledViewPool(viewHolder, true);
          //从CacheViews中移除
          mCachedViews.remove(cachedViewIndex);
      }

      void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
         //...省略代码
          if (dispatchRecycled) {
              dispatchViewRecycled(holder);
          }
          holder.mOwnerRecyclerView = null;
          getRecycledViewPool().putRecycledView(holder);
      }

      void dispatchViewRecycled(@NonNull ViewHolder holder) {
          if (mRecyclerListener != null) {
              mRecyclerListener.onViewRecycled(holder);
          }
          if (mAdapter != null) {
              mAdapter.onViewRecycled(holder);
          }
          if (mState != null) {
              mViewInfoStore.removeViewHolder(holder);
          }
          if (DEBUG) Log.d(TAG, "dispatchViewRecycled: " + holder);
      }

代码中很简单,就是mCacheView如果满了,就将mCacheView的第0个移动到RecyclerViewPool中。然后增加到mCacheView中。


增加View时,scrapView怎么回收?

      void scrapView(View view) {
          final ViewHolder holder = getChildViewHolderInt(view);
          if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                  || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
              if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
                  throw new IllegalArgumentException("Called scrap view with an invalid view."
                          + " Invalid views cannot be reused from scrap, they should rebound from"
                          + " recycler pool." + exceptionLabel());
              }
              holder.setScrapContainer(this, false);
              mAttachedScrap.add(holder);
          } else {
              if (mChangedScrap == null) {
                  mChangedScrap = new ArrayList<ViewHolder>();
              }
              holder.setScrapContainer(this, true);
              mChangedScrap.add(holder);
          }
      }

  // 有动画
  boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) {
      return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder,
              viewHolder.getUnmodifiedPayloads());
  }

大致是没有动画时ViewHolder存在mAttachedScrap中,否则存在mChangedScrap

RecyclerView的复用和回收机制到这里就分析完了。

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

推荐阅读更多精彩内容