Android RecyclerView 之緩存機制篇

源碼的世界及其復(fù)雜,要是每一步都去深究,很容易迷失在里面歧寺,這里將RecyclerView的緩存機制抽出來重點分析燥狰,結(jié)合圖文的方式,希望可以給您帶來幫助斜筐!

RecyclerView的緩存機制猶如一個強大的引擎龙致,為RecyclerView的暢滑運行提供了強有力的保障;Android的大部分視圖都是列表形式的顷链,那么RecyclerView的出現(xiàn)無疑大大的提升了開發(fā)效率目代;那么RecyclerView的緩存究竟是如何工作的呢,那就讓我們來揭開謎底吧嗤练!

RecyclerView的緩存機制就是依附于Recycler這個類來實現(xiàn)的榛了,讓我們先來看一下這個類的成員變量:

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

Recycler分析:

Recycler的成員變量總共有五個集合,分為兩部分煞抬,具體請看下面介紹霜大;

1,Scrap部分:

(1)mAttachedScrap:存儲的是當(dāng)前還在屏幕中的ViewHolder革答;
(2)mChangedScrap:存儲的是數(shù)據(jù)被更新的ViewHolder,比如說調(diào)用了Adapter的notifyItemChanged方法战坤;

2,Cache部分:

(1)mCachedViews:默認(rèn)大小為2残拐,通常用來存儲預(yù)取的ViewHolder途茫,同時在回收ViewHolder時,也會可能存儲一部分的ViewHolder溪食,這部分的ViewHolder通常來說囊卜,意義跟一級緩存差不多;
(2)mRecyclerPool:根據(jù)ViewType來緩存ViewHolder眠菇,每個ViewType的數(shù)組大小為5边败,可以動態(tài)的改變;
(3)mViewCacheExtension:自定義緩存捎废;

RecyclerView總共有4級緩存:
第一級緩存:Scrap部分笑窜;
第二級緩存:mCachedViews;
第三級緩存:mViewCacheExtension登疗;
第四級緩存:mRecyclerPool排截;

那么具體是怎么實現(xiàn)的呢,讓我們根據(jù)源碼來分析吧辐益;
首頁我們先看看ViewHolder的獲取流程断傲;

2,具體流程分析:

1智政,ViewHolder獲取流程:

首先先看流程圖:


RecyclerView獲取viewHolder.png

看完流程圖认罩,那么接下來具體分析一下源碼是怎么操作的;
我在上一篇博客里面分析了RecyclerView的繪制流程续捂,里面提到了獲取ViewHolder 的方法垦垂,也就是layoutChunk方法里面的next(recycler)宦搬,讓我們看一下源碼里面寫了啥?

View next(RecyclerView.Recycler recycler) {
            if (mScrapList != null) {
                return nextViewFromScrapList();
            }
            final View view = recycler.getViewForPosition(mCurrentPosition);
            mCurrentPosition += mItemDirection;
            return view;
        }

這里最終調(diào)用的是tryGetViewHolderForPositionByDeadline();
繼續(xù)分析tryGetViewHolderForPositionByDeadline()方法:

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
           
            // 第一步
            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
                        ...
                            recycleViewHolderInternal(holder);
                        }
                        holder = null;
                    } else {
                        fromScrapOrHiddenOrCache = true;
                    }
                }
            }

            if (holder == null) {
                ...
                 // 第三步
                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                   ...
                }
                // 第四步
                if (holder == null && mViewCacheExtension != null) {
                   ...
                    final View view = mViewCacheExtension
                            .getViewForPositionAndType(this, position, type);
                      if (view != null) {
                        holder = getChildViewHolder(view);
                        ..
                      }
                  ...
                }
               // 第五步
                if (holder == null) { // fallback to pool
                    ...
                    holder = getRecycledViewPool().getRecycledView(type);
                    ...
                }
              // 第六步
                if (holder == null) {
                    ...
                    holder = mAdapter.createViewHolder(RecyclerView.this, type);
                    
            }

            ...
            return holder;
        }
(1)第一步:

首先劫拗,先判斷是否是預(yù)布局间校,也就是dispatchLayoutStep1(),這個方法在上一篇博客也已經(jīng)分析過了页慷,具體可以點擊查看憔足;
判斷如果是的話則從getChangedScrapViewForPosition()方法去獲取緩存的ViewHolder,
getChangedScrapViewForPosition()方法分析:

ViewHolder getChangedScrapViewForPosition(int position) {
            // If pre-layout, check the changed scrap for an exact match.
            final int changedScrapSize;
            if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) {
                return null;
            }
            // find by position
            for (int i = 0; i < changedScrapSize; i++) {
                final ViewHolder holder = mChangedScrap.get(i);
                if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) {
                    holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                    return holder;
                }
            }
            // find by id
            if (mAdapter.hasStableIds()) {
                final int offsetPosition = mAdapterHelper.findPositionOffset(position);
                if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) {
                    final long id = mAdapter.getItemId(offsetPosition);
                    for (int i = 0; i < changedScrapSize; i++) {
                        final ViewHolder holder = mChangedScrap.get(i);
                        if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) {
                            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
                            return holder;
                        }
                    }
                }
            }
            return null;
        }

這里做的操作就是從mChangedScrap里通過ItemID來獲取緩存的ViewHolder酒繁;
并給這個ViewHolder添加標(biāo)記位(ViewHolder.FLAG_RETURNED_FROM_SCRAP)滓彰,表示是從Scrap這個緩存里面獲取的;

(2)第二步:

第二步通過getScrapOrHiddenOrCachedHolderForPosition()方法來獲取緩存欲逃,讓我們看源碼繼續(xù)分析:

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

            // Try first for an exact, non-invalid match from scrap.
            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) {
                    // 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;
                }
            }

            // Search in our first-level recycled view cache.
            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) {
                    if (!dryRun) {
                        mCachedViews.remove(i);
                    }
                    if (DEBUG) {
                        Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                                + ") found match in cache: " + holder);
                    }
                    return holder;
                }
            }
            return null;
        }

通過上面源碼分析找蜜,這里是通過position先從mChangedScrap這個集合里面取緩存,如果取得到則給這個ViewHolder添加標(biāo)記位(ViewHolder.FLAG_RETURNED_FROM_SCRAP)稳析,表示是從Scrap這個緩存里面獲取的洗做;mChildHelper里的mHiddenViews是與動畫相關(guān)的緩存獲取,這里就不進行分析了彰居;那么如果從mChangedScrap獲取不到ViewHolder诚纸,下面就會從mCachedViews里面獲取緩存;
validateViewHolderForOffsetPosition()這個方法是用來判斷ViewHoler是否有效陈惰,如果無效了畦徘,則進行回收,具體操作在recycleViewHolderInternal(holder)這個方法里抬闯,后面會進行詳細(xì)分析井辆;

(3)第三步:
        if (mAdapter.hasStableIds()) {
              holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    if (holder != null) {
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    }
         }

這里通過判斷hasStableIds是否為true,如果為true則通過getScrapOrCachedViewForId()方法來獲取緩存溶握,這里是先從mChangedScrap里獲取緩存杯缺,如果獲取不到則從mCachedViews里面獲取緩存;和第二步類似這里就不過多分析了睡榆;

(4)第四步:

這一步通過mViewCacheExtension來獲取緩存萍肆,這個是自定義緩存,用到場景較少胀屿,也不過多分析了塘揣;

public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position,
                int type);

這里是抽象方法,具體獲取邏輯由子類實現(xiàn)宿崭;

(5)第五步:
public ViewHolder getRecycledView(int viewType) {
            final ScrapData scrapData = mScrap.get(viewType);
            if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
                final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
                return scrapHeap.remove(scrapHeap.size() - 1);
            }
            return null;
        }

這里是通過RecycledViewPool里的getRecycledView方法來獲取緩存亲铡,這里的mScrap是Android自定義的集合SparseArray,和map一樣,只是效率會更高效一些奖蔓;這里通過mScrap獲取scrapHeap的集合琅摩,然后獲取該集合的最后一個元素;

(6)第六步:
public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
            try {
                TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
                final VH holder = onCreateViewHolder(parent, viewType);
                if (holder.itemView.getParent() != null) {
                    throw new IllegalStateException("ViewHolder views must not be attached when"
                            + " created. Ensure that you are not passing 'true' to the attachToRoot"
                            + " parameter of LayoutInflater.inflate(..., boolean attachToRoot)");
                }
                holder.mItemViewType = viewType;
                return holder;
            } finally {
                TraceCompat.endSection();
            }
        }

當(dāng)上面的幾步都獲取不到ViewHolder時锭硼,則通過調(diào)用Adapter的onCreateViewHolder()方法來創(chuàng)建一個ViewHolder并返回給RecyclerView;
那么到這里ViewHolder的獲取就分析完畢了蜕劝;

2檀头,ViewHolder的回收流程:

先來看一張詳細(xì)的流程圖:


ViewHolder回收.png

這里把復(fù)雜的源碼通過流程圖展示出來,源碼的細(xì)節(jié)就不過多的描述了岖沛;
從上面的流程圖可以看出暑始,RecyclerView在滑動時候就會進行ViewHolder的回收,而具體的回收邏輯是在recycleViewHolderInternal()這個方法里婴削,我們重點分析這個方法廊镜;

先來看一下源碼:

void recycleViewHolderInternal(ViewHolder holder) {
           ...
            if (forceRecycle || holder.isRecyclable()) {
                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();
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                      //第一步
                        recycleCachedViewAt(0);
                        cachedViewSize--;
                    }

                    int targetCacheIndex = cachedViewSize;
                    if (ALLOW_THREAD_GAP_WORK
                            && cachedViewSize > 0
                            && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                     ...
                  //第二步
                    mCachedViews.add(targetCacheIndex, holder);
                    cached = true;
                }
                if (!cached) {
                    //第三步
                    addViewHolderToRecycledViewPool(holder, true);
                    recycled = true;
                }
            } else {
                ...
            }
            ...
        }

這里主要做了三步操作:

1,第一步:

這里通過判斷mCachedViews的大小是否已經(jīng)超過最大唉俗,是的話則移除mCachedViews的第一個元素嗤朴,并添加到RecycledViewPool里面去;
具體請看下面源碼:

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);
        }
2虫溜,第二步:

這里做的操作就是將ViewHolder緩存到mCachedViews集合里面去雹姊;

3,第三步:

這里通過判斷前面如果沒有將ViewHolder緩存到mCachedViews時衡楞,則把該mCachedViews緩存到RecycledViewPool里去吱雏,最終走的是下面這個方法;

public void putRecycledView(ViewHolder scrap) {
            final int viewType = scrap.getItemViewType();
            final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
            if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
                return;
            }
            if (DEBUG && scrapHeap.contains(scrap)) {
                throw new IllegalArgumentException("this scrap item already exists");
            }
            scrap.resetInternal();
            scrapHeap.add(scrap);
        }

需要注意的是瘾境,RecycledViewPool的viewType歧杏,一個viewType默認(rèn)對應(yīng)可以存5個ViewHolder的緩存;

static class ScrapData {
            final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
            int mMaxScrap = DEFAULT_MAX_SCRAP; // 默認(rèn)5個緩存的大忻允亍犬绒;
            long mCreateRunningAverageNs = 0;
            long mBindRunningAverageNs = 0;
        }

當(dāng)然這個值是可以修改的,通過setMaxRecycledViews(int viewType, int max)這個方法來進行設(shè)置盒犹;

然后到這里你會發(fā)現(xiàn)懂更,這里只用了mCachedViews和RecycledViewPool來做緩存,上面提到的Scrap部分和ViewCacheExtension部分呢急膀?別急沮协,后面我們繼續(xù)來分析這兩者是什么時候用到的;

1卓嫂,Scrap部分

先來看一下Scrap部分慷暂,Scrap集合添加ViewHolder的方法主要是在scrapView()這個方法里面,而這個方法被getScrapOrHiddenOrCachedHolderForPosition()和scrapOrRecycleView()這個方法所調(diào)用;

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

1行瑞,先來看一下這個getScrapOrHiddenOrCachedHolderForPosition()方法奸腺,這個方法的調(diào)用時機上面已經(jīng)提到過了,就是在獲取ViewHolder的時候血久,這里就不重復(fù)了突照;
那么我們再來看一下這個方法里面的這個scrap部分做了什么?

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
            ...
            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;
                }
            }
            ...
            return null;
        }

這里通過mChildHelper的findHiddenNonRemovedView()方法來獲取一個ViewHolder氧吐,是從mHiddenViews這個集合里面獲取讹蘑,而這個mHiddenViews集合里面是存儲的和動畫相關(guān)的ViewHolder;
這里獲取了ViewHolder之后就通過scrapView()方法存儲到scrap里面去筑舅;

2座慰,接下來分析這個scrapOrRecycleView()方法的調(diào)用時機;
這個方法是由detachAndScrapAttachedViews()這個方法來調(diào)用的翠拣,而調(diào)用detachAndScrapAttachedViews()這個方法的地方是LayoutManager里的onLayoutChildren()方法版仔,也就是說,這里的回收是通過觸發(fā)LayoutManager的布局來調(diào)用的误墓;
這里最終回收的是通過mChildHelper.getChildAt(index)獲取的ViewHolder;

到這里蛮粮,scrap部分的回收就將完了;

2谜慌,ViewCacheExtension部分

接下來分析一下ViewCacheExtension部分的回收蝉揍,ViewCacheExtension這個自定義緩存的部分,在源碼里面只有取ViewHolder的邏輯畦娄,但是沒有存ViewHolder的邏輯又沾,看來谷歌是把ViewCacheExtension回收的邏輯交給開發(fā)者自己去實現(xiàn)了,那么這里就不過多的分析了熙卡;

那么到這里RecyclerView的緩存機制就分析完了杖刷;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市驳癌,隨后出現(xiàn)的幾起案子滑燃,更是在濱河造成了極大的恐慌,老刑警劉巖颓鲜,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件表窘,死亡現(xiàn)場離奇詭異,居然都是意外死亡甜滨,警方通過查閱死者的電腦和手機乐严,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來衣摩,“玉大人昂验,你說我怎么就攤上這事。” “怎么了既琴?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵占婉,是天一觀的道長。 經(jīng)常有香客問我甫恩,道長逆济,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任磺箕,我火速辦了婚禮啄骇,結(jié)果婚禮上脆粥,老公的妹妹穿的比我還像新娘瓦侮。我一直安慰自己涵卵,他們只是感情好莱褒,可當(dāng)我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布击困。 她就那樣靜靜地躺著,像睡著了一般广凸。 火紅的嫁衣襯著肌膚如雪阅茶。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天谅海,我揣著相機與錄音脸哀,去河邊找鬼。 笑死扭吁,一個胖子當(dāng)著我的面吹牛撞蜂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播侥袜,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蝌诡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了枫吧?” 一聲冷哼從身側(cè)響起浦旱,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎九杂,沒想到半個月后颁湖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡例隆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年甥捺,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片镀层。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡涎永,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情羡微,我是刑警寧澤谷饿,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站妈倔,受9級特大地震影響博投,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜盯蝴,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一毅哗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧捧挺,春花似錦虑绵、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至黑竞,卻和暖如春捕发,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背很魂。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工扎酷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人遏匆。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓法挨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親幅聘。 傳聞我的和親對象是個殘疾皇子坷剧,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,779評論 2 354