RecyclerView渲染分析

上一篇介绍基本使用,这一篇我们深入分析RecyclerView内部被渲染的流程,在这里我们从三个方面来了解流程,具体三个方面如下:

  • 启动渲染:一般首次渲染的时候。
  • 滑动渲染: 手指触摸屏幕滑动直到离开的过程。
  • 通知渲染:一般调用notifyDataSetChanged() 或者 notifyItemChanged()时候。
1、启动渲染

RecyclerView继承ViewGroup,同样准守View的绘制过程,所以也会走onMeasure、onLayout、onDraw方法,我们先来看看RecyclerView的onMeasure方法。

Override
 protected void onMeasure(int widthSpec, int heightSpec) {
        if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        if (mLayout.isAutoMeasureEnabled()) {
            //...省略
            // modes in both dimensions are EXACTLY.
            mLastAutoMeasureSkippedDueToExact =
            widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY
            if (mLastAutoMeasureSkippedDueToExact || mAdapter == null) {
                    return;
            }
            if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
            }
            mLayout.setMeasureSpecs(widthSpec, heightSpec);
            mState.mIsMeasuring = true;
            dispatchLayoutStep2();
            //...省略
        }else{
            //...省略
        }
 }

我们只看核心代码,现在我们以LinearLayoutManager为例子,LinearLayoutManager重写了isAutoMeasureEnabled()方法并且返回true,这样确定了RecyclerView的绘制走了自动布局的逻辑。如果Recyclerview确定了画布大小(宽高都是MeasureSpec.EXACTLY)直接return,测量结束。重点还是dispatchLayoutStep1()、dispatchLayoutStep2()逻辑。其中dispatchLayoutStep1()的代码如下:

    /**
     * The first step of a layout where we;
     * - process adapter updates                处理adapter更新     
     * - decide which animation should run      决定哪个执行动画
     * - save information about current views   保存当前view的信息
     * - If necessary, run predictive layout and save its information   是否进行预加载并且保存预加载信息
     */
    private void dispatchLayoutStep1() {
        mState.mIsMeasuring = false;
        processAdapterUpdatesAndSetAnimationFlags();
        saveFocusInfo();
        //...省略
        if (mState.mRunSimpleAnimations) {
        //...省略  
        }
        if (mState.mRunPredictiveAnimations) {
        //...省略 
        }       
        // ..省略
       mState.mLayoutStep = State.STEP_LAYOUT;
    }

dispatchLayoutStep1()主要还是更新状态(mState)和视图(mViewInfoStore)为下一步绘制做准备。接下来看dispatchLayoutStep2()方法,代码如下:

/**
     * 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
        mState.mInPreLayout = false;
        mLayout.onLayoutChildren(mRecycler, mState);
        //..省略    
        mState.mLayoutStep = State.STEP_ANIMATIONS;
    }

这个方法更新了部分State,核心还是onLayoutChildren()方法,由LayoutManager代理,其他onLayoutChildren()实现了RecyclerView的绝大部分视图渲染逻辑。我们这边以LinearLayoutMananger为例子进行绘制分析,接下来看看onLayoutChildren()的代码如下:

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.
        // create layout state
        //以上注释已经说明了绘制算法:1、先确定锚点(anchor)2、布局方向由下往上或者由上往下绘制。
        //...省略
       if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
            || mPendingSavedState != null) {  //首次绘制确定锚点、满足次条件。
            mAnchorInfo.reset();
            //确定绘制步骤,决定由上往下还是由下往上绘制。
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            // calculate anchor position and coordinate 计算anchor的position 和 coordinate
            //计算anchor由一套规则,anchor依次由滑动mPendingSavedState、聚焦view来确定,如果未发现初始化anchor(position = 0)。
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            mAnchorInfo.mValid = true;
        } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
           >= mOrientationHelper.getEndAfterPadding()
                || mOrientationHelper.getDecoratedEnd(focused)
                <= mOrientationHelper.getStartAfterPadding())) {
                //在有focused聚焦视图情况下,满足条件更新锚点信息。
                mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
        }
        //...省略
        if (mAnchorInfo.mLayoutFromEnd) {
          // fill towards start 
          updateLayoutStateToFillStart(mAnchorInfo);
          fill(recycler, mLayoutState, state, false);
          // fill towards end
          updateLayoutStateToFillEnd(mAnchorInfo);
          fill(recycler, mLayoutState, state, false);
        } else {
          // fill towards end
          updateLayoutStateToFillEnd(mAnchorInfo);
          fill(recycler, mLayoutState, state, false);
          // fill towards start
          updateLayoutStateToFillStart(mAnchorInfo);
          fill(recycler, mLayoutState, state, false);
        }
        //...省略
 }

以上代码体现锚点的计算非常重要,我们来看一下锚点(AnchorInfo)和布局状态(LayoutState)重要属性。

    /**
     * Simple data class to keep Anchor information
     */
    static class AnchorInfo {
        OrientationHelper mOrientationHelper; //计算start,end等辅助类
        int mPosition;             //Anchor 启点的位置
        int mCoordinate;           //Anchor Y轴上起始绘制偏移量
        boolean mLayoutFromEnd;    //布局方向
        boolean mValid;            //锚点是否合法
    }

    /**
     * Helper class that keeps temporary state while {LayoutManager} is filling out the empty
     * space.
     */
    static class LayoutState {
        /**
         * Current position on the adapter to get the next item.
        * 当前位置
         */
        int mCurrentPosition;
         /**
         * Number of pixels that we should fill, in the layout direction.
         * 布局方向有多少空间需要被填充
         */
        int mAvailable;
        /**
         * Used if you want to pre-layout items that are not yet visible.
         * 提前预加载空间
         * The difference with {@link #mAvailable} is that, when recycling, distance laid out for
         * {@link #mExtraFillSpace} is not considered to avoid recycling visible children.
         */
        int mExtraFillSpace = 0;
        /**
         * Pixel offset where layout should start
         * 开始布局的偏移位置
         */
        int mOffset;
         /**
         * Defines the direction in which the layout is filled.
         * 布局方向LAYOUT_START或者LAYOUT_END
         * Should be {@link #LAYOUT_START} or {@link #LAYOUT_END}
         */
        int mLayoutDirection;
         /**
         * Used when LayoutState is constructed in a scrolling state.
         * 滚动的距离
         * It should be set the amount of scrolling we can make without creating a new view.
         * Settings this is required for efficient view recycling.
         */
        int mScrollingOffset;
    }

了解重要属性之后,下面我们通过图片来表达核心的绘制逻辑。


图一
图二

图一为首次RecyclerView绘制的时候,AnchorInfo{mPosition = 0,mCoordinate = 0} ,mLayoutDirection = LayoutState.LAYOUT_START 时候往上绘制空间为0,所以只考虑往下绘制(mAvailable + mExtraFillSpace)。图二为多数滑动或者聚焦view(比如resume界面)的时候渲染经过。在onLayoutChildren方法中调用updateLayoutStateToFillStart或者updateLayoutStateToFillEnd将计算结果更新至LayoutState,最后交付给fill进行填充绘制。

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
             RecyclerView.State state, boolean stopOnFocusable) {
        //...省略
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            /**
             * Consume the available space if:
             * * layoutChunk did not request to be ignored
             * * OR we are laying out scrap children
             * * OR we are not doing pre-layout
             */
            if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                    || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                // we keep a separate remaining space because mAvailable is important for recycling
                remainingSpace -= layoutChunkResult.mConsumed;
            }
            //...省略
       }
}
protected static class LayoutChunkResult {
        public int mConsumed; //item高度
        //...省略
}

fill代码中不断计算remainingSpace剩余高度,当remainingSpace需要填充的时候调用layoutChunk()方法进行item绘制,并且不断更新对应mConsumed,接下来我们看下layoutChunk方法。

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
                     LayoutState layoutState, LayoutChunkResult result) {
      View view = layoutState.next(recycler); //这个方法非常重要,直接包含了recyclerview的缓存机制,此处暂时没进行解释,缓存讲解的时候再来看这个方法。
      RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
      //首次绘制layoutState.mScrapList == null
      if (layoutState.mScrapList == null) {
           if (mShouldReverseLayout == (layoutState.mLayoutDirection
                   == LayoutState.LAYOUT_START)) {
               addView(view);
           } else {
               addView(view, 0);
           }
       } else {
          //此处暂时不看,这边预加载动画相关的view添加。
           if (mShouldReverseLayout == (layoutState.mLayoutDirection
                   == LayoutState.LAYOUT_START)) {
               addDisappearingView(view);
           } else {
               addDisappearingView(view, 0);
           }
       }
       measureChildWithMargins(view, 0, 0);
       result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
       // We calculate everything with View's bounding box (which includes decor and margins)
       // To calculate correct layout position, we subtract margins.
       layoutDecoratedWithMargins(view, left, top, right, bottom);
       //...省略
}

以上代码将item绘制出来并且addview到recyclerview中,并且通过layoutDecoratedWithMargins调整到最终的位置。到此处我们来看一下item的包装类,补充下OrientationHelper,这个类协助remainingSpace空间的计算。


图三
2、滑动渲染

滑动分为两种,一种是正常滑动,一种是快速滑动(Flinger),以下分析这两种滑动代码。

@Override
public boolean onTouchEvent(MotionEvent e) {
      //...省略
     switch (action) {
            case MotionEvent.ACTION_DOWN: {
                  mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
                  mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
            }
            //...省略
            case MotionEvent.ACTION_MOVE: {
              if (mScrollState != SCROLL_STATE_DRAGGING) {
                   if (canScrollHorizontally) {
                          if (dx > 0) {
                              dx = Math.max(0, dx - mTouchSlop);
                          } else {
                              dx = Math.min(0, dx + mTouchSlop);
                          }
                          if (dx != 0) {
                              startScroll = true;
                          }
                    }
                    if (canScrollVertically) {
                        if (dy > 0) {
                            dy = Math.max(0, dy - mTouchSlop);
                        } else {
                            dy = Math.min(0, dy + mTouchSlop);
                        }
                        if (dy != 0) {
                            startScroll = true;
                        }
                    }
                }
            }
            //...省略
           if (mScrollState == SCROLL_STATE_DRAGGING) {
                mLastTouchX = x - mScrollOffset[0];
                mLastTouchY = y - mScrollOffset[1];

                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        vtev)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
            }
            //...省略
          case MotionEvent.ACTION_UP: {
            //...省略
            final float yvel = canScrollVertically ?
                    -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0;
            if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                setScrollState(SCROLL_STATE_IDLE);
            }
            resetTouch();
          }
          //...省略
}

以上代码为手指触摸到屏幕后,记录mLastTouchX、mLastTouchY,经过ACTION_MOVE,计算dy或者dx,并且mTouchSlop为滑动阀值(这个值可初始化时候决定,能决定滑动偏移值),dy或者dx大于这个值触发scrollByInternal方法,我们来看一下scrollByInternal方法。

boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
    //...省略
    if (mAdapter != null) {
        //...省略
       scrollStep(x, y, mReusableIntPair);
        //...省略
    }
    //...省略
}

在onTouchEvent中,当ACTION_UP手指抬起来的时候,经过VelocityTrackerCompat根据初速度来计算是否触发fling()事件,我们先来看看fling的调用过程。

public boolean fling(int velocityX, int velocityY) {
    //...省略
    velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
    velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
    mViewFlinger.fling(velocityX, velocityY);
   //...省略
}

接着看ViewFlinger中的fling、postOnAnimation、internalPostOnAnimation方法

public void fling(int velocityX, int velocityY) {
     //...省略
    postOnAnimation();
    //...省略
}

void postOnAnimation() {
     if (mEatRunOnAnimationRequest) {
          mReSchedulePostAnimationCallback = true;
      } else {
           internalPostOnAnimation();
      }
}

private void internalPostOnAnimation() {
      removeCallbacks(this);
      ViewCompat.postOnAnimation(RecyclerView.this, this);
 }

最终调用的是ViewFlinger方法中的run方法。

@Override
public void run() {
     //...省略
     if (mAdapter != null) {
         //...省略
        scrollStep(unconsumedX, unconsumedY, mReusableIntPair);
         //...省略
     }
     //...省略
}

接下来分析scrollStep方法

void scrollStep(int dx, int dy, @Nullable int[] consumed) {
     //...省略
     if (dx != 0) {
            consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
     }
    if (dy != 0) {
          consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
    }
     //...省略
}

最终调用了LayoutMananger中的scrollHorizontallyBy和scrollVerticallyBy方法,我们以LinearLayoutMananger为例子。以下分析LinearLayoutMananger中scrollHorizontallyBy为例子。

/**
* {@inheritDoc}
*/
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
                                RecyclerView.State state) {
    if (mOrientation == VERTICAL) {
          return 0;
    }
     return scrollBy(dx, recycler, state);
}

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);
     //...省略    
}

到此处就可以看到updateLayoutState和fill方法,通过这两个方法不断的测量可绘空间并且填充,可绘制的空间加上了滑动的偏移量。综述完成了滑动绘制的讲述。

3、通知渲染

通知渲染也有两种方式:notifyDataSetChanged全局刷新和notifyItemChanged局部刷新。

  • 先来看看notifyDataSetChanged()
public final void notifyDataSetChanged() {
       mObservable.notifyChanged();
}
public void notifyChanged() {
          // since onChanged() is implemented by the app, it could do anything, including
          // removing itself from {@link mObservers} - and that could cause problems if
          // an iterator is used on the ArrayList {@link mObservers}.
          // to avoid such problems, just march thru the list in the reverse order.
          for (int i = mObservers.size() - 1; i >= 0; i--) {
              mObservers.get(i).onChanged();
          }
}

这个mObservers的注册在setAdapter方法中,调用的顺序依次setAdapter -> setAdapterInternal -> registerAdapterDataObserver -> registerObserver,接下来代码如下:

  public void registerObserver(T observer) {
        if (observer == null) {
            throw new IllegalArgumentException("The observer is null.");
        }
        synchronized(mObservers) {
            if (mObservers.contains(observer)) {
                throw new IllegalStateException("Observer " + observer + " is already registered.");
            }
            mObservers.add(observer);
        }
    }

最终可以看出来调用的是RecyclerView内部的成员RecyclerViewDataObserver,RecyclerViewDataObserver中的onChanged是最后调用的目的,代码如下:

   @Override
   public void onChanged() {
        assertNotInLayoutOrScroll(null);
        mState.mStructureChanged = true;
        processDataSetCompletelyChanged(true);
        if (!mAdapterHelper.hasPendingUpdates()) {
              requestLayout();
        }
   }

最终调用requestLayout方法,进行重新绘制。其中processDataSetCompletelyChanged(true)会将所有状态进行更新,保证不进行动画执行。

@Override
public void requestLayout() {
       if (mEatRequestLayout == 0 && !mLayoutFrozen) {
           super.requestLayout();
       } else {
           mLayoutRequestEaten = true;
       }
}

通过super.requestLayout()的调用会触发onLayout()方法,最后调用dispatchLayout进行绘制。

  void dispatchLayout() {
        if (mAdapter == null) {
            Log.e(TAG, "No adapter attached; skipping layout");
            // leave the state in START
            return;
        }
        if (mLayout == null) {
            Log.e(TAG, "No layout manager attached; skipping layout");
            // leave the state in START
            return;
        }
        mState.mIsMeasuring = false;
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                || mLayout.getHeight() != getHeight()) {
            // First 2 steps are done in onMeasure but looks like we have to run again due to
            // changed size.
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            // always make sure we sync them (to ensure mode is exact)
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
    }

以上方法由进行dispatchLayoutStep1()、dispatchLayoutStep2()、dispatchLayoutStep3()方法。其中dispatchLayoutStep3()和动画绘制和执行相关。
其中dispatchLayoutStep由mState.mLayoutStep的状态决定。其中mState.mLayoutStep的状态对应State.STEP_START、State.STEP_LAYOUT、State.STEP_ANIMATIONS。

  • 接着看notifyItemChanged局部刷新
 public final void notifyItemChanged(int position, @Nullable Object payload) {
            mObservable.notifyItemRangeChanged(position, 1, payload);
 }
 public void notifyItemRangeChanged(int positionStart, int itemCount,
                                           @Nullable Object payload) {
            // since onItemRangeChanged() is implemented by the app, it could do anything, including
            // removing itself from {@link mObservers} - and that could cause problems if
            // an iterator is used on the ArrayList {@link mObservers}.
            // to avoid such problems, just march thru the list in the reverse order.
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload);
            }
}

最终调用了RecyclerViewDataObserver方法中的onItemRangeChanged方法。

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

void triggerUpdateProcessor() {
         if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
               ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
         } else {
              mAdapterUpdateDuringMeasure = true;
              requestLayout();
          }
}

mAdapterHelper.onItemRangeChanged方法将需要执行的item加入到AdapterHelper.UpdateO中,需要执行的item的动作(ADD、REMOVE、UPDATE、MOVE)通过AdapterHelper.UpdateOp进行记录,放入到一个等待队列中去,并且触发requestLayout,最终经过dispatchLayoutStep3()方法执行动画操作。

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

推荐阅读更多精彩内容