Android 进阶学习(八) RecyclerView 学习 (二) 关于RecyclerView 缓存机制

RecyclerView 的缓存机制还是非常值得到家参考的,先来说一下RecyclerView 关于缓存的方法, 关于RecyclerView 的缓存数据有两个级别,一个是detach ,另一个就是remove , 关于detach 就是在RecyclerView 滑动或者layout 时为了记录屏幕内条目信息而设定的,他主要的缓存的数据就是getChildCount列表所持有的数据, 至于remove 就是为了缓存我们从数据列表所删除的数据,根据这个信息我们来从代码来分析

   public final class Recycler { 
       ///这attach 相关的缓存的列表
       final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
       ///和remove 相关的缓存列表
       final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
       ///缓存池
       RecycledViewPool mRecyclerPool;
       private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
       //默认的最大缓存个数  
       int mViewCacheMax = DEFAULT_CACHE_SIZE;
       static final int DEFAULT_CACHE_SIZE = 2;
}

我们看到Recycler 中有3个和缓存的相关的列表他们分别是 mAttachedScrap mCachedViews mRecyclerPool,我们来看看都是在哪些方法对这些列表做的操作,
我们先来看看detach方法,是如何将view 暂时和Recyclerview 分离的,并看一下他是将数据缓存的哪个列表中

detachAndScrapAttachedViews

   public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
           final int childCount = getChildCount();
           for (int i = childCount - 1; i >= 0; i--) {
               final View v = getChildAt(i);
               scrapOrRecycleView(recycler, i, v);
           }
       }

我们可以看到在detachAndScrapAttachedViews方法中显示获取了getChildCount ,缓存的就是RecyclerView的所持有的展示数据,我们继续看看scrapOrRecycleView这个方法是怎么操作的

scrapOrRecycleView

       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()) {
               removeViewAt(index);
               recycler.recycleViewHolderInternal(viewHolder);
           } else {
               detachViewAt(index);
               recycler.scrapView(view);
               mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
           }
       }

我们就看到根据viewHolder的状态,一部分走的是remove,我们这里暂时不管被remove的item,我们去看看scrap的方法,即recycler.scrapView(view)

recycler.scrapView(view);

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

还是判断了一堆viewholder的状态,但是我们看到在detach时候,是将这个这个holder缓存在mAttachedScrap中,那么也就是说屏幕中的数据都放在这里,为什么要单独给展示的list单独一个缓存呢,其实他这个做是为了再下一次执行addView 的过程中从mAttachedScrap获取到数据就是已经绑定过数据的holder了而且这个holder所对应的数据还是正确的,这样可以将bindViewHolder方法所执行的时间省去了,可以加快view的绘制,

我们再来看看remove的方法

removeAndRecycleView

public void removeAndRecycleView(@NonNull View child, @NonNull Recycler recycler) {
           removeView(child);
           recycler.recycleView(child);
       }

先执行了removeView 然后又recycleView 回收,我们来看看recycleView

recycler.recycleView(child);

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

先是根据view找到这个view的holder,然后判断这个holer.isScrap ,为什么其他地方只是说一下是holder的判断,而这里却单独强调一下这个判断,我们再来说一下detach 的过程,每次我们滑动的过程中都会执行detach 将数据放入到mAttachedScrap这个list中,那么这么多的数据,这么频繁的添加数据如果不删除数据的话肯定是不行的,那么什么时候将这个数据删除呢? 对 !就是这里, holder.unScrap方法就是在remove过程中将数据从mAttachedScrap 中删除的,其他情况我们看到了再说,这里就分析一下删除的过程中的情况

holder.unScrap();

void unScrap() {
           mScrapContainer.unscrapView(this);
       }

在holder中他有调用了recycler的unscrapView方法,来处理接下来的逻辑,

####recycler.unscrapView(child);
      void unscrapView(ViewHolder holder) {
           if (holder.mInChangeScrap) {
               mChangedScrap.remove(holder);
           } else {
               mAttachedScrap.remove(holder);
           }
           holder.mScrapContainer = null;
           holder.mInChangeScrap = false;
           holder.clearReturnedFromScrapFlag();
       }

我们可以看到在unscrapView方法中,将数据移出了mAttachedScrap这个列表中的,
接下来我们继续跟踪remove过程中的recycleViewHolderInternal方法

recycleViewHolderInternal

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();
           final boolean forceRecycle = mAdapter != null
                   && transientStatePreventsRecycling
                   && mAdapter.onFailedToRecycleView(holder);
           boolean cached = false;
           boolean recycled = false;
      
           if (forceRecycle || holder.isRecyclable()) {
               if (mViewCacheMax > 0
                       && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID  | ViewHolder.FLAG_REMOVED
                       | ViewHolder.FLAG_UPDATE   | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {

                   int cachedViewSize = mCachedViews.size();
                   if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                       recycleCachedViewAt(0);
                       cachedViewSize--;
                   }
                   int targetCacheIndex = cachedViewSize;
                   if (ALLOW_THREAD_GAP_WORK&& cachedViewSize > 0  && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                       int cacheIndex = cachedViewSize - 1;
                       while (cacheIndex >= 0) {
                           int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                           if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                               break;
                           }
                           cacheIndex--;
                       }
                       targetCacheIndex = cacheIndex + 1;
                   }
                   mCachedViews.add(targetCacheIndex, holder);
                   cached = true;
               }
               if (!cached) {
                   addViewHolderToRecycledViewPool(holder, true);
                   recycled = true;
               }
           } else {

           }
           mViewInfoStore.removeViewHolder(holder);
           if (!cached && !recycled && transientStatePreventsRecycling) {
               holder.mOwnerRecyclerView = null;
           }
       }

这段代码看起来很长,但是逻辑非常简单,前面很长的抛异常,我们可以略过,主要从forceRecycle || holder.isRecyclable() 这个判断开始,这个判断的意思就是我们通过代码设置希望对holder回收,在最大回收个数大于0的情况下,对mCachedViews 进行操作,但是比较有意思的地方就是,如果现在的mCachedViews 满了,我们需要将mCachedViews 列表中的数据放入到RecycledViewPool 中,代码如下

if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                       recycleCachedViewAt(0);
                       cachedViewSize--;
 }

 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);
           }
           addViewHolderToRecycledViewPool(viewHolder, true);
           mCachedViews.remove(cachedViewIndex);
}
 
void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
           clearNestedRecyclerViewIfNotNested(holder);
           if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {
               holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
               ViewCompat.setAccessibilityDelegate(holder.itemView, null);
           }
           if (dispatchRecycled) {
               dispatchViewRecycled(holder);
           }
           holder.mOwnerRecyclerView = null;
           getRecycledViewPool().putRecycledView(holder);
       }

在为mCachedViews 清理出来数据后,再将我们需要remove的holder放入到mCachedViews 中,而mCachedViews 中的数据其实也是缓存的绑定完数据的holder,也就是如果这个holder是从mCachedViews 中匹配出来的,那么也不需要经过bindViewHolder 这个过程,从上面我们看到mCachedViews 他的最大缓存这个是2个,那是不是越大越好呢,肯定不是的,只要缓存的个数够用其实就是可以了,但是我们什么时候需要对他扩容来帮助我们来提高运行速度呢,我们来做一个假设下面图片这种情况,

image.png

如果我们使用的是一个GridLayoutManager 这个manager,每一行有3条数据,那么最大2个的缓存池是肯定需要重新bindViewHoder的,这种情况我们就可以适当的将这个最大值扩充一下,至于说是3还是6,这个就仁者见仁,智者见智了,

说完了数据缓存,我们再来看看我们是如何将缓存的数据取出来的,

recycler.getViewForPosition(i);

getViewForPosition就是我们获取到这个view 的入口,既然我么分析过了保存数据的过程,其实就可以猜测这个获取的过程,肯定是最开始从detach 的mAttachedScrap 这个list里面获取,因为他是缓存的屏幕上面的数据,也就是我么需要渲染的数据,其次是mCachedViews 这个list,mCachedViews 的缓存list的size比较小,所以不能缓存太多数据,然后是 RecycledViewPool 这个缓存池从这个里面获取的holder可能是没有绑定数据,也可能绑定错误的数据,所以需要重新绑定数据,最后才是new ,我么来具体看一下实现方法,由于这个里面的代码比较长,我会删掉一些不影响逻辑的方法

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

       View getViewForPosition(int position, boolean dryRun) {
           return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
       }
 @Nullable
       ViewHolder tryGetViewHolderForPositionByDeadline(int position,
               boolean dryRun, long deadlineNs) {
           ....
           boolean fromScrapOrHiddenOrCache = false;
           ViewHolder holder = null;
           if (mState.isPreLayout()) {
               holder = getChangedScrapViewForPosition(position);
               fromScrapOrHiddenOrCache = holder != null;
           }
           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
                       if (!dryRun) {
                           holder.addFlags(ViewHolder.FLAG_INVALID);
                           if (holder.isScrap()) {
                               removeDetachedView(holder.itemView, false);
                               holder.unScrap();
                           } 
                        ....
                       }
                    ....
                   } 
                 ....
               }
           }
           if (holder == null) {
               final int offsetPosition = mAdapterHelper.findPositionOffset(position);
               final int type = mAdapter.getItemViewType(offsetPosition);
               if (mAdapter.hasStableIds()) {
                   holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
                   ....
               }
               if (holder == null && mViewCacheExtension != null) {
                   final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
                  ....
               }
               if (holder == null) { 
                   holder = getRecycledViewPool().getRecycledView(type);
                   ....
               }
               if (holder == null) {     
                   holder = mAdapter.createViewHolder(RecyclerView.this, type);
                   ...
               }
           }  
           .....
           return holder;
       }

虽然我输出了一大部分代码,但是剩下的代码还是比较多,逻辑非常清晰,我们一点一点来看,

我们先来看第一次获取viewholder

if (mState.isPreLayout()) {
               holder = getChangedScrapViewForPosition(position);
               fromScrapOrHiddenOrCache = holder != null;
           }

我只知道isPreLayout和执行动画有关,但是他们之间到底什么是什么关系就不是很清楚了,这个过程还没有分析,我们就先越过这个过程

再看第二个判断,

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
                       if (!dryRun) {
                           holder.addFlags(ViewHolder.FLAG_INVALID);
                           if (holder.isScrap()) {
                               removeDetachedView(holder.itemView, false);
                               holder.unScrap();
                           } 
                        ....
                       }
                    ....
                   } 
                 ....
               }
           }

我们可以看到这个holder是通过getScrapOrHiddenOrCachedHolderForPosition 这个方法来找到,我们进去看看他具体的实现逻辑,
在这之前来说一下我们在detach的时候会将数据添加到mAttachedScrap这个list,我们知道了在remove的时候我们会将这个holder移出mAttachedScrap,从上面这个方法中我们看到同样进行了holder.unScrap();,也就是在如果在mAttachedScrap找到这个holder也会将它在list移除

getScrapOrHiddenOrCachedHolderForPosition

 ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
           final int scrapCount = mAttachedScrap.size();
           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;
               }
           }
           if (!dryRun) {
               View view = mChildHelper.findHiddenNonRemovedView(position);
               if (view != null) {
                   final ViewHolder vh = getChildViewHolderInt(view);
                   mChildHelper.unhide(view);
                   int layoutIndex = mChildHelper.indexOfChild(view);
                   mChildHelper.detachViewFromParent(layoutIndex);
                   scrapView(view);
                   vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
                           | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                   return vh;
               }
           }

           final int cacheSize = mCachedViews.size();
           for (int i = 0; i < cacheSize; i++) {
               final ViewHolder holder = mCachedViews.get(i);
               if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
                   if (!dryRun) {
                       mCachedViews.remove(i);
                   }          
                   return holder;
               }
           }
           return null;
       }

我们看到在这个getScrapOrHiddenOrCachedHolderForPosition 方法中首先遍历mAttachedScrap,如果mAttachedScrap这个position 和我们需要查找的position相同则返回,如果没有找到则根据我们根据dryRun来判断,我们看到在View getViewForPosition(int position)方法中这个dryRun向下传递的是false,那么这里就是调用的从mChildHelper.findHiddenNonRemovedView 这个方法,

 View findHiddenNonRemovedView(int position) {
       final int count = mHiddenViews.size();
       for (int i = 0; i < count; i++) {
           final View view = mHiddenViews.get(i);
           RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
           if (holder.getLayoutPosition() == position
                   && !holder.isInvalid()
                   && !holder.isRemoved()) {
               return view;
           }
       }
       return null;
   }

这就是第一次查找,如果第一次查找我们没有找到,那么就应该去下一个地方继续查找

       final int offsetPosition = mAdapterHelper.findPositionOffset(position);
       final int type = mAdapter.getItemViewType(offsetPosition);
        if (mAdapter.hasStableIds()) {
                   holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                           type, dryRun);
                   if (holder != null) {
                       // update position
                       holder.mPosition = offsetPosition;
                       fromScrapOrHiddenOrCache = true;
                   }
        }

先找到offsetPosition ,根绝offsetPosition 查找type,最后根据itemdid 和 type 去getScrapOrCachedViewForId方法查找

getScrapOrCachedViewForId

ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) {
           final int count = mAttachedScrap.size();
           for (int i = count - 1; i >= 0; i--) {
               final ViewHolder holder = mAttachedScrap.get(i);
               if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
                   if (type == holder.getItemViewType()) {
                       holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                       if (holder.isRemoved()) {
                           if (!mState.isPreLayout()) {
                               holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE
                                       | ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
                           }
                       }
                       return holder;
                   } else if (!dryRun) {
                       mAttachedScrap.remove(i);
                       removeDetachedView(holder.itemView, false);
                       quickRecycleScrapView(holder.itemView);
                   }
               }
           }
           final int cacheSize = mCachedViews.size();
           for (int i = cacheSize - 1; i >= 0; i--) {
               final ViewHolder holder = mCachedViews.get(i);
               if (holder.getItemId() == id) {
                   if (type == holder.getItemViewType()) {
                       if (!dryRun) {
                           mCachedViews.remove(i);
                       }
                       return holder;
                   } else if (!dryRun) {
                       recycleCachedViewAt(i);
                       return null;
                   }
               }
           }
           return null;
       }

从上面的方法我们看到同样的还是遍历mAttachedScrap 这个list,只不过这次判断的逻辑是 itemid 和type,同时相同才可以,如果itemid相同,type不同那么则将这个holder 放入到mCachedViews 中,接下来就是遍历mCachedViews,继续寻找,
若果上面两种情况都没找到,就会继续去mViewCacheExtension 中寻找,但是mViewCacheExtension这个需要我们自己去实现,这里就不多说了,继续看下面

holder = getRecycledViewPool().getRecycledView(type);

这个就很简单了,从缓存池根据type获取,
最后一个就是new

holder = mAdapter.createViewHolder(RecyclerView.this, type);

整个过程我们就分析完成了,一个非常巧妙的设计,

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

推荐阅读更多精彩内容