RecyclerView添加Header和Footer

1、出现的3个异常

1.1、第一个,第二个

java.lang.IllegalArgumentException: Called attach on a child which is not detached: ViewHolder{3d34d42 position=4 id=-1, oldPos=-1, pLpos:-1} hy.sohu.com.ui_lib.hyrecyclerview.hyrecyclerView.HyRecyclerView{12f2c6 VFED..... ......ID 0,427-720,1430 #7f0903c6 app:id/hyrecyclerview_chat}, adapter:hy.sohu.com.ui_lib.hyrecyclerview.hyrecyclerView.PullToLoadAdapter@58e3153, layout:hy.sohu.com.ui_lib.hyrecyclerview.HyLinearLayoutManager@9c47a90, context:hy.sohu.com.app.chat.view.message.SingleChatMsgActivity@561df6
    at androidx.recyclerview.widget.RecyclerView$5.attachViewToParent(RecyclerView.java:931)
    at androidx.recyclerview.widget.ChildHelper.attachViewToParent(ChildHelper.java:241)
    at androidx.recyclerview.widget.RecyclerView.addAnimatingView(RecyclerView.java:1443)
    at androidx.recyclerview.widget.RecyclerView.animateDisappearance(RecyclerView.java:4371)
    at androidx.recyclerview.widget.RecyclerView$4.processDisappeared(RecyclerView.java:617)
    at androidx.recyclerview.widget.ViewInfoStore.process(ViewInfoStore.java:245)
    at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep3(RecyclerView.java:4208)
    at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3862)
    at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:4404)
    at hy.sohu.com.ui_lib.hyrecyclerview.hyrecyclerView.HyRecyclerView.onLayout(HyRecyclerView.java:1055)


java.lang.IllegalArgumentException: Called removeDetachedView with a view which"
                        + " is not flagged as tmp detached." + vh + exceptionLabel()

出现位置与原因: ChildHelper
当向RecyclerView添加一个child时,这个child已经有一个parent。

// mChildHelper = new ChildHelper

 public void attachViewToParent(View child, int index,
                    ViewGroup.LayoutParams layoutParams) {
                final ViewHolder vh = getChildViewHolderInt(child);
                if (vh != null) {
                    if (!vh.isTmpDetached() && !vh.shouldIgnore()) {
                        throw new IllegalArgumentException("Called attach on a child which is not"
                                + " detached: " + vh + exceptionLabel());
                    }
                    if (DEBUG) {
                        Log.d(TAG, "reAttach " + vh);
                    }
                    vh.clearTmpDetachFlag();
                }
                RecyclerView.this.attachViewToParent(child, index, layoutParams);
            }

1.2、第三个、Cannot call this method while RecyclerView is computing a layout or scrolling HyRecyclerView。

java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling hy.sohu.com.ui_lib.hyrecyclerview.hyrecyclerView.HyRecyclerView{62a0245 VFED..... ........ 0,131-1080,1996 #7f0903cd app:id/hyrecyclerview_chat}, adapter:hy.sohu.com.ui_lib.hyrecyclerview.hyrecyclerView.PullToLoadAdapter@2f22d9a, layout:hy.sohu.com.ui_lib.hyrecyclerview.HyLinearLayoutManager@a1e86cb, context:hy.sohu.com.app.chat.view.message.SingleChatMsgActivity@8fe39bf
    at androidx.recyclerview.widget.RecyclerView.assertNotInLayoutOrScroll(RecyclerView.java:3062)
    at androidx.recyclerview.widget.RecyclerView$RecyclerViewDataObserver.onItemRangeInserted(RecyclerView.java:5558)
    at androidx.recyclerview.widget.RecyclerView$AdapterDataObservable.notifyItemRangeInserted(RecyclerView.java:12286)
    at androidx.recyclerview.widget.RecyclerView$Adapter.notifyItemRangeInserted(RecyclerView.java:0)
    at hy.sohu.com.ui_lib.hyrecyclerview.hyrecyclerView.HeaderAndFooter.HeaderAndFooterRecyclerView$DataObserver.onItemRangeInserted(HeaderAndFooterRecyclerView.java:348)
    at androidx.recyclerview.widget.RecyclerView$AdapterDataObservable.notifyItemRangeInserted(RecyclerView.java:12286)
    at androidx.recyclerview.widget.RecyclerView$Adapter.notifyItemRangeInserted(RecyclerView.java:0)
    at hy.sohu.com.ui_lib.hyrecyclerview.hyadapter.HyBaseNormalAdapter.addData(HyBaseNormalAdapter.java:163)
    at hy.sohu.com.app.chat.view.message.ChatMsgBaseActivity.onSaveLocalSuccess(ChatMsgBaseActivity.kt:910)

出现位置与原因:
当调用notifyItemChanged,notifyItemRemove时检测mLayoutOrScrollCounter是否> 0 抛出异常。

 private class RecyclerViewDataObserver extends AdapterDataObserver {
        RecyclerViewDataObserver() {
        }
        
        @Override
        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
            assertNotInLayoutOrScroll(null);
            if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
                triggerUpdateProcessor();
            }
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            assertNotInLayoutOrScroll(null);
            if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) {
                triggerUpdateProcessor();
            }
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            assertNotInLayoutOrScroll(null);
            if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
                triggerUpdateProcessor();
            }
        }
 
    }

void assertNotInLayoutOrScroll(String message) {
        if (isComputingLayout()) {
            if (message == null) {
                throw new IllegalStateException("Cannot call this method while RecyclerView is "
                        + "computing a layout or scrolling" + exceptionLabel());
            }
            throw new IllegalStateException(message);
        }
    }

  public boolean isComputingLayout() {
        return mLayoutOrScrollCounter > 0;
  }

2、产生原因

为什么mLayoutOrScrollCounter > 0

2.1、recyclerView的onLayout

protected void onLayout(boolean changed, int l, int t, int r, int b) {
        dispatchLayout();
  }


 void dispatchLayout() {
         mState.mIsMeasuring = false;
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                || mLayout.getHeight() != getHeight()) {
            dispatchLayoutStep2();
        } else {
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
 }

2.2、看下 dispatchLayoutStep1/2

   private void dispatchLayoutStep 1/2 () {

        onEnterLayoutOrScroll();

        mLayout.onLayoutChildren(mRecycler, mState);

        onExitLayoutOrScroll();

    }

    void onEnterLayoutOrScroll() {
        mLayoutOrScrollCounter++;
      }

    void onExitLayoutOrScroll(boolean enableChangeEvents) {
        mLayoutOrScrollCounter--;
    }

  从上面看出每次执行 dispatchLayoutStep1/2开始mLayoutOrScrollCounter++,执行完成mLayoutOrScrollCounter--就等于0,期间执行 mLayout.onLayoutChildren()操作。如果onLayoutChildren出现异常,则mLayoutOrScrollCounter就会大于0。
  而java.lang.IllegalArgumentException就是在mLayout.onLayoutChildren方法执行过程中调用的,所以由于第一个异常未处理,产生了第二个异常,实际不可能同时出现两个crash异常。
   原因 : 自定义的HyLinearLayoutManager被try——catch,导致第一个异常没有导致app闪退,当再次执行onItemRangeRemoved时,产生了第二个异常。所以只需要找到第一个问题的原因则可。

  // HyLinearLayoutManager
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        try {
            //try catch一下
            super.onLayoutChildren(recycler, state);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
3、分析mLayout.onLayoutChildren(mRecycler, mState);

以dispatchLayoutStep2中onLayoutChildren方法为例介绍。


recyclerview缓存
3.1、 notifyItemRemove(7)

正常情况下,是不会出现crash,下面是写demo可以验证。

       btn1.setOnClickListener {
            var stringList = (recyclerView.adapter as ItemAdapter).stringList as LinkedList
            stringList.removeAt(1)
            (recyclerView?.adapter as ItemAdapter).notifyItemRemoved(2)
            Log.d(TAG, "onCreate:--after-- " + recyclerView.isComputingLayout)
        }
        btn2.setOnClickListener {
            adapter.notifyDataSetChanged()
     }

当调用notifyItemRemove(7)时,dispatchLayoutStep2会从mAttachedScrap加载混存的holder。
  3.11、寻找到第0个,加入RecyclerView第0个。
  3.12、寻找到第1个,加入RecyclerView第1个。
  3.13、寻找到第2个,加入RecyclerView第2个。
  3.14、寻找到第3个,此时第三个item的type和mAdapter返回的type不一致。所以找不到,会依据type创建一个holder。

  //getScrapOrHiddenOrCachedHolderForPosition
          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;
                }
            }

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

此时按照type,倒数第二个type为Item_Type_load,会返回一个holder,这个holder的itemview为mLoadView。加入RecyclerView为第三个holder。

    @Override
    public int getItemViewType(int position) {
        if (isLoadPosition(position)) {
            return ITEM_TYPE_LOAD;
        } else if (isBottomPosition(position)) {
            return ITEM_TYPE_BOTTOM;
        }
        return super.getItemViewType(position);
    }

此时mAttachedScrap中有一个持有mLoadView的ViewHolder,RecyclerView中也加入了一个持有LoadView的ViewHolder。
  3.15、寻找第4个,此时找到的事mLoadView的holder,但是这个holder的type和mAdapter返回type不一致,此时需要把holder移除。出现如下crash。因为刚才我们已经加入到recyclerView,移除报错。

   protected void removeDetachedView(View child, boolean animate) {
        ViewHolder vh = getChildViewHolderInt(child);
        if (vh != null) {
            if (vh.isTmpDetached()) {
                vh.clearTmpDetachFlag();
            } else if (!vh.shouldIgnore()) {
                throw new IllegalArgumentException("Called removeDetachedView with a view which"
                        + " is not flagged as tmp detached." + vh + exceptionLabel());
            }
        }
    }
3.2、notifyItemRemove(3)

删除3,但是我们通知3时,当notifyItemRemove(3)。

缓存

  3.21、寻找到第0个,加入RecyclerView第0个。
  3.22、寻找到第1个,加入RecyclerView第1个。
  3.23、寻找到第2个,加入RecyclerView第2个。
  3.24、寻找到第3个,此时mAttachedScrap中有个2个位置为3个holder,一个是移除holder,一个mLoadView(位置已经做了正确的偏移),找到了holder。
  3.25、寻找到第4个,也找到已经偏移位置的line。加入到RecyclerView中,不会出现任何问题。

3.3、notifyItemRemove(4)

  3.3.1、寻找到第0个,加入RecyclerView第0个。
  3.3.2、寻找到第1个,加入RecyclerView第1个。
  3.3.3、寻找到第2个,加入RecyclerView第2个。
  3.3.4、寻找到第3个,此时mAttachedScrap中找到的holder的type和mAdapter需要的type不一致,所以会创建一个holder加入进去。我们上面知道加入的事mLoadView。
   此时mAttachedScrap和RecyclerView中各有一个持有同一个mLoadView的ViewHolder。
  3.3.5、寻找到第4个,也找到已经偏移位置的line。加入到RecyclerView中,不会出现任何问题。因为在这个位置找到了holder,不需要移除持有mLoadView的viewHolder,所以不会报第一个中的异常。
  3.3.6、执行动画。


动画

对于loadViewHolder执行消失动画。

 void animateDisappearance(@NonNull ViewHolder holder,
            @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
        addAnimatingView(holder);
        holder.setIsRecyclable(false);
        if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
            postAnimationRunner();
        }
    }

     private void addAnimatingView(ViewHolder viewHolder) {
        final View view = viewHolder.itemView;
        final boolean alreadyParented = view.getParent() == this;
        mRecycler.unscrapView(getChildViewHolder(view));
        if (viewHolder.isTmpDetached()) {
            // re-attach
            mChildHelper.attachViewToParent(view, -1, view.getLayoutParams(), true);
        } else if (!alreadyParented) {
            mChildHelper.addView(view, true);
        } else {
            mChildHelper.hide(view);
        }
    }

mChildHelper.attachViewToParent就抛出了crash。因为loadViewHolder所持有的ItemView已经被加入到了RecyclerView。

4、解决方案
私信
   public void removeData(String msgId) {
        int index = -1;
        for (Iterator iterable = getDatas().iterator(); iterable.hasNext(); ) {
            ChatMsgBean chatMsgBaseBean = (ChatMsgBean) iterable.next();
            index++;
            if (!TextUtils.isEmpty(msgId) && msgId.equals(chatMsgBaseBean.msgId)) {
                try {
                    iterable.remove();
                    notifyItemRemoved(index);
                    if (index > 0) {
                        notifyItemChanged(index - 1);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
        }
    }

上述代码执行
  notifyItemRemoved(0) 执行完只剩1条数据。
  notifyItemRemoved(1);
当执行notifyItemRemoved(1)时,此时list只有一条数据,就会导致上面notifyItemRemoved(4)的场景。
这里只列举删除,当然增删改查都可能导致这个问题。

 public void removeData(String msgId) {
        int index = -1;
        List<ChatMsgBean> datas = getDatas();

        for (int i = 0; i < datas.size(); i++) {
            ChatMsgBean chatMsgBean = datas.get(i);
            if (!TextUtils.isEmpty(msgId) && msgId.equals(chatMsgBean.msgId)) {
                index = i;
                break;
            }
        }

        if (index != -1) {
            datas.remove(index);
            notifyItemRemoved(index);
        }
    }

  4.1、避免每次创建的viewholder和缓存中的ViewHolder持有同一个View.


   //PullToLoadAdapter
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == ITEM_TYPE_LOAD) {
            mRealLoadView = createWrapView(mLoadView);
            return new RecyclerView.ViewHolder(mRealLoadView) {
            };
        } else if (viewType == ITEM_TYPE_BOTTOM) {
            mRealBottomView = createWrapView(mBottomView);
            return new RecyclerView.ViewHolder(mRealBottomView) {
            };
        }

        return super.onCreateViewHolder(parent, viewType);
    }

 // HeaderAndFooterAdapter
@Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//        如果是头部
        if (isHeaderType(viewType)) {
            int headerPosition = mHeaderViews.indexOfKey(viewType);
            View headerView = mHeaderViews.valueAt(headerPosition);
//            if (mOnCreateHeaderViewHolderListener != null && headerView != null
//                    && headerView.getTag() != null && headerView.getTag() instanceof Integer) {
//                if ((int) headerView.getTag() > 0) {
//                    return mOnCreateHeaderViewHolderListener.onCreateHeaderViewHolder(parent, viewType, (int) headerView
//                    .getTag());
//                }
//            }
            return createHeaderAndFooterViewHolder(createWrapView(headerView));
        }
//        如果是placeHolder
        if (isPlaceHolderType(viewType)) {
            View view = LayoutInflater.from(mContext.getApplicationContext()).inflate(R.layout.placeholer_recyclerview, null,
                    false);
            return new HyPlaceHolderView(view);
        }
//        如果是尾部
        if (isFooterType(viewType)) {
            int footerPosition = mFooterViews.indexOfKey(viewType);
            View footerView = mFooterViews.valueAt(footerPosition);
            return createHeaderAndFooterViewHolder(createWrapView(footerView));
        }
        return mRealAdapter.onCreateViewHolder(parent, viewType);
    }


 protected FrameLayout createWrapView(View view) {
        FrameLayout frameLayout = new FrameLayout(mContext);
        ViewParent parent = view.getParent();
        LogUtil.d("HyRecyclerView",
                "onCreateViewHolder_0: " + parent + "  " + view.getTag() + " params.h =" + view.getLayoutParams() + "  " +
                        "visiable=  " + view.getVisibility());
        if (parent != null && parent instanceof ViewGroup) {
            ViewGroup viewGroup = (ViewGroup) parent;
            viewGroup.removeView(view);
        }
        frameLayout.setVisibility(view.getVisibility());
        if (view.getLayoutParams() != null) {
            ViewGroup.LayoutParams frlayoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    view.getLayoutParams().height);
            frameLayout.setLayoutParams(frlayoutParams);
        } else {
            ViewGroup.LayoutParams frlayoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT);
            frameLayout.setLayoutParams(frlayoutParams);
        }
        frameLayout.addView(view);
        return frameLayout;
    }

因为topView和mRefreshView用于刷新,bottomView和loadView用于加载,因此
添加了一层,在PullToRefreshRecyclerView和HyRecyclerView里面都需要替换控件,并且将bottomView等属性赋值包裹的View。
  4.2、从上面可知,因为在对应position没有获取到holder,所以新创建了一个。想办法从mAttachscrap中获取缓存。

 ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
            boolean fromScrapOrHiddenOrCache = false;
            ViewHolder holder = null;
            // 0) If there is a changed scrap, try to find from there
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
            // 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
                        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();
                            }
                            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;
                    }
                }
}

处理给adapter的item设置唯一的ID,重写geiItemID方法。

   mAdapter.setHasStableIds(true);

    @Override
    public long getItemId(int position) {
        return super.getItemId(position);
    }
总结

  添加header和footer时,由于缓存复用,避免创建的Viewholder持有同一个View。

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

推荐阅读更多精彩内容