Android硬件加速過程分析

繪制基礎(chǔ)

安卓繪制框架

繪制框架.png

應用層
1.通過與WindowManagerService通信倔丈,獲取一塊surface。
2.通過View系統(tǒng)或OpenGL、Skia在這塊surface上繪制脐往。
3.與WindowManagerService通信蕊肥,提交繪制產(chǎn)生的緩存谒获。
服務(wù)層(WindowManagerService)
1.生成蛤肌、釋放surface
2.管理surface的層級
3.獲取surface的數(shù)據(jù),將surface的數(shù)據(jù)進行混合批狱,生成新的一幀裸准。
4.將新的一幀數(shù)據(jù)寫入內(nèi)核,使得顯示器刷新數(shù)據(jù)
關(guān)鍵類:
1.Canvas:畫布赔硫,里面有一個Bitmap炒俱,用于保存繪制過程中的緩存。此類提供了繪制的基本方法爪膊,包括作圓权悟、線等。
2.Paint:畫筆推盛,提供繪制的基本參數(shù)峦阁,比如說顏色,畫筆的粗細等耘成。
3.Surface:可以獲取畫布并提交繪制結(jié)果榔昔。本質(zhì)上是一個代理類。用于Application與WindowManager通信瘪菌。
4.ViewRootImp:實現(xiàn)ViewParent撒会,包含Surface和Choreographer。用于控制Activity的繪制控嗜。

  1. Choreographer繪制調(diào)度器茧彤。用于控制各個Surface的刷新步調(diào),讓所以Surface基本上同時開始刷新疆栏。一般16ms觸發(fā)一次刷新曾掂。
  2. SurfaceFlinger:混合各個Surface的數(shù)據(jù),形成新的一幀數(shù)據(jù)壁顶。

View的重繪流程

以最基本的View的invalidate為例
基本繪制流程


繪制新流程.png

流程圖中有小小錯誤珠洗,能找到嗎?
基本流程可以分成兩塊:
一若专、觸發(fā)刷新
子view調(diào)用invalidate许蓖,然后層層遞進,調(diào)到ViewrootImp的ScheduleTraversals调衰,在這個過程中不斷確定重繪的區(qū)域膊爪,更新各個父控件的繪制標志。完成各個層級父控制的繪制標志的刷新之后嚎莉,利用ViewrootImp中的Choreographer控制刷新的時機米酬,觸發(fā)刷新。
二趋箩、刷新過程

  • 首先通過Surface的lockCanvas申請一塊新的畫布赃额,獲取一個Canvas加派。
    值得注意的是,WindowManagerService會為每一塊surface分配兩塊內(nèi)存跳芳,
    一塊作為顯示使用(讀)芍锦,一塊作為繪制使用(寫)。在完成繪制后飞盆,兩塊內(nèi)存的作用互換娄琉。lockCanvas的作用,實際上時是給待繪制的內(nèi)存上鎖吓歇,并且獲取待繪制內(nèi)存的地址车胡,把這塊內(nèi)存作為畫布的緩存。在lockCanvas時照瘾,會將顯示的內(nèi)存的數(shù)據(jù)拷貝到待繪制的內(nèi)存中,同時傳入一個區(qū)域(dirty)丧慈,作為重繪的區(qū)域析命。每次重繪實際只需要繪制需要修改的區(qū)域(dirty區(qū)域),其它區(qū)域保持不變逃默,這樣鹃愤,就大大加快了繪制效率

    繪制內(nèi)存切換.png

  • 獲取畫布之后,就需要層層調(diào)用View的draw方法完域,在這塊畫布上在做畫软吐。
    每一個View都有一塊緩存mDrawCache。View的繪制過程吟税,首先要通過繪制標志凹耙,判斷View的內(nèi)容是否發(fā)生變化,有發(fā)生變化肠仪,則調(diào)用ondraw方法肖抱,重新獲取mDrawCache。最后將mDrawCache的重繪區(qū)域繪制到畫布中异旧。
    ViewGroup包含自己本身的繪制和子View的繪制意述。自己本身的繪制和View一樣,子View的繪制需要層層調(diào)用子View的繪制方法吮蛹。

    draw過程.png

  • 完成繪制后荤崇,調(diào)用unlockCanvasAndPost接口。將繪制內(nèi)容提交給WindowManagerService潮针。
    接口unlockCanvasAndPost的實質(zhì)是將繪制的內(nèi)存解鎖术荤,并且通知WindowManagerService將兩塊內(nèi)存的作用互換,這樣然低,SurfaceFlinger就會將最新繪制后得到的內(nèi)存進行混合顯示喜每。

硬件加速

為什么使用硬件加速

CPU和GPU架構(gòu)

CPU : Central Processing Unit 务唐, 中央處理器,是計算機設(shè)備核心器件带兜,用于執(zhí)行程序代碼枫笛。
GPU : Graphic Processing Unit , 圖形處理器刚照,主要用于處理圖形運算枪眉,通常所說“顯卡”的核心部件就是GPU接癌。
下面是CPU和GPU的結(jié)構(gòu)對比圖。其中:

  • 黃色的Control為控制器,用于協(xié)調(diào)控制整個CPU的運行雨涛,包括取出指令、控制其他模塊的運行等拾积;

  • 綠色的ALU(Arithmetic Logic Unit)是算術(shù)邏輯單元抹蚀,用于進行數(shù)學、邏輯運算郭变;
    橙色的Cache和DRAM分別為緩存和RAM颜价,用于存儲信息。


    cpu和gpu架構(gòu)圖.png
  • 從結(jié)構(gòu)圖可以看出诉濒,CPU的控制器較為復雜周伦,而ALU數(shù)量較少。因此CPU擅長各種復雜的邏輯運算未荒,但不擅長數(shù)學尤其是浮點運算专挪。

  • 和CPU不同的是,GPU就是為實現(xiàn)大量數(shù)學運算設(shè)計的片排。從結(jié)構(gòu)圖中可以看到寨腔,GPU的控制器比較簡單,但包含了大量ALU率寡。GPU中的ALU使用了并行設(shè)計脆侮,且具有較多浮點運算單元。

硬件加速的主要原理勇劣,就是通過底層軟件代碼靖避,將CPU不擅長的圖形計算轉(zhuǎn)換成GPU專用指令,由GPU完成比默。

硬件加速的開啟

Android 系統(tǒng)的 UI 從 繪制 到 顯示在屏幕上 是分兩個步驟的
第一步:在Android 應用程序這一側(cè)進行的幻捏。(將 UI 構(gòu)建到一個圖形緩沖區(qū) Buffer 中,交給SurfaceFlinger )
第二步:在SurfaceFlinger進程這一側(cè)進行的命咐。(獲取Buffer 并合成以及顯示到屏幕中篡九。)
其中,第二步在 SurfaceFlinger 的操作一直是以硬件加速方式完成的醋奠,所以我們說的硬件加速一般指的是在 應用程序 圖形通過GPU加速渲染 到 Buffer 的過程榛臼。

在Android中伊佃,可以四給不同層次上開啟硬件加速:

  • 應用:
    <application android:hardwareAccelerated="true">
  • Activity
    <activity android:hardwareAccelerated="true">
  • Window
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
  • View
    view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    在這四個層次中,應用和Activity是可以選擇的沛善,Window只能打開航揉,View只能關(guān)閉。

硬件加速的流程分析

模式模型

構(gòu)建階段

所謂構(gòu)建就是遞歸遍歷所有視圖金刁,將需要的操作緩存下來帅涂,之后再交給單獨的Render線程利用OpenGL渲染。在Android硬件加速框架中尤蛮,View視圖被抽象成RenderNode節(jié)點媳友。


構(gòu)建Rendor緩存.png

View中的繪制都會被抽象成一個個DrawOp(DisplayListOp),比如View中drawLine产捞,構(gòu)建中就會被抽象成一個DrawLintOp醇锚,drawBitmap操作會被抽象成DrawBitmapOp,每個子View的繪制被抽象成DrawRenderNodeOp坯临,每個DrawOp有對應的OpenGL繪制命令搂抒,同時內(nèi)部也握著繪圖所需要的數(shù)據(jù)。

DrawOp.png

如此以來尿扯,每個View不僅僅握有自己DrawOp List,同時還拿著子View的繪制入口焰雕,如此遞歸衷笋,便能夠統(tǒng)計到所有的繪制Op,很多分析都稱為Display List矩屁,源碼中也是這么來命名類的辟宗,不過這里其實更像是一個樹,而不僅僅是List吝秕,示意如下:


displayList結(jié)構(gòu)圖-2.png

構(gòu)建完成后泊脐,就可以將這個繪圖Op樹交給Render線程進行繪制,這里是同軟件繪制很不同的地方烁峭,軟件繪制時容客,View一般都在主線程中完成繪制,而硬件加速约郁,除非特殊要求缩挑,一般都是在單獨線程中完成繪制,如此以來就分擔了主線程很多壓力鬓梅,提高了UI線程的響應速度供置。


CPU和GPU分工合作圖.jpg

構(gòu)建過程源碼分析

構(gòu)建階段流程圖.png

從流程圖可以看出,HardwareRenderer是整個硬件加速繪制的入口绽快。整個繪制的過程通過不斷dispatchGetDisplayList或者dispathDraw遍歷View芥丧,刷新displayList紧阔。
從名字能看出,ThreadedRenderer應該跟一個Render線程息息相關(guān)续担,不過ThreadedRenderer是在UI線程中創(chuàng)建的擅耽,那么與UI線程也必定相關(guān),其主要作用:

1赤拒、在UI線程中完成DrawOp集構(gòu)建
2秫筏、負責跟渲染線程通信

可見ThreadRenderer的作用是很重要的,簡單看一下實現(xiàn):

ThreadedRenderer(Context context, boolean translucent) {
    ...
    <!--新建native node-->
    long rootNodePtr = nCreateRootRenderNode();
    mRootNode = RenderNode.adopt(rootNodePtr);
    mRootNode.setClipToBounds(false);
    <!--新建NativeProxy-->
    mNativeProxy = nCreateProxy(translucent, rootNodePtr);
    ProcessInitializer.sInstance.init(context, mNativeProxy);
    loadSystemProperties();
}
  • RootNode用來標識整個DrawOp樹的根節(jié)點挎挖,有個這個根節(jié)點就可以訪問所有的繪制Op.
  • RenderProxy對象这敬,這個對象就是用來跟渲染線程進行通信的句柄
    再看一下RenderProxy的源碼
RenderProxy::RenderProxy(bool translucent, RenderNode* rootRenderNode, IContextFactory* contextFactory)
        : mRenderThread(RenderThread::getInstance())
        , mContext(nullptr) {
    SETUP_TASK(createContext);
    args->translucent = translucent;
    args->rootRenderNode = rootRenderNode;
    args->thread = &mRenderThread;
    args->contextFactory = contextFactory;
    mContext = (CanvasContext*) postAndWait(task);
    mDrawFrameTask.setContext(&mRenderThread, mContext);  
   }
  • RenderThread是一個單例線程,也就是說蕉朵,每個進程最多只有一個硬件渲染線程崔涂,這樣就不會存在多線程并發(fā)訪問沖突問題,到這里其實環(huán)境硬件渲染環(huán)境已經(jīng)搭建好好了始衅。
    ThreadRender的繪制入口
@Override
void draw(View view, AttachInfo attachInfo, HardwareDrawCallbacks callbacks) {
    attachInfo.mIgnoreDirtyState = true;

    final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer;
    choreographer.mFrameInfo.markDrawStart();
    <!--關(guān)鍵點1:構(gòu)建View的DrawOp樹-->
    updateRootDisplayList(view, callbacks);

    <!--關(guān)鍵點2:通知RenderThread線程繪制-->
    int syncResult = nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length);
    ...
}
  • updateRootDisplayList冷蚂,構(gòu)建RootDisplayList,其實就是構(gòu)建View的DrawOp樹汛闸,updateRootDisplayList會進而調(diào)用根View的updateDisplayListIfDirty蝙茶,讓其遞歸子View的updateDisplayListIfDirty,從而完成DrawOp樹的創(chuàng)建
  • 通知RenderThread線程繪制
    構(gòu)建樹的流程
private void updateRootDisplayList(View view, HardwareDrawCallbacks callbacks) {
    <!--更新-->
    updateViewTreeDisplayList(view);
   if (mRootNodeNeedsUpdate || !mRootNode.isValid()) {
      <!--獲取DisplayListCanvas-->
        DisplayListCanvas canvas = mRootNode.start(mSurfaceWidth, mSurfaceHeight);
        try {
        <!--利用canvas緩存Op-->
            final int saveCount = canvas.save();
            canvas.translate(mInsetLeft, mInsetTop);
            callbacks.onHardwarePreDraw(canvas);

            canvas.insertReorderBarrier();
            canvas.drawRenderNode(view.updateDisplayListIfDirty());
            canvas.insertInorderBarrier();

            callbacks.onHardwarePostDraw(canvas);
            canvas.restoreToCount(saveCount);
            mRootNodeNeedsUpdate = false;
        } finally {
        <!--將所有Op填充到RootRenderNode-->
            mRootNode.end(canvas);
        }
    }
}
  • 利用View的RenderNode獲取一個DisplayListCanvas
  • 利用DisplayListCanvas構(gòu)建并緩存所有的DrawOp
  • 將DisplayListCanvas緩存的DrawOp填充到RenderNode
  • 將根View的緩存DrawOp設(shè)置到RootRenderNode中诸老,完成構(gòu)建
    DisplayList的更新過程
  // Don't need to recreate the display list, just need to tell our
            // children to restore/recreate theirs
            if (renderNode.isValid()
                    && !mRecreateDisplayList) {
                mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                dispatchGetDisplayList();

                return renderNode; // no work needed
            }

            // If we got here, we're recreating it. Mark it as such to ensure that
            // we copy in child display lists into ours in drawChild()
            mRecreateDisplayList = true;

            int width = mRight - mLeft;
            int height = mBottom - mTop;
            int layerType = getLayerType();

            final DisplayListCanvas canvas = renderNode.start(width, height);

            try {
                if (layerType == LAYER_TYPE_SOFTWARE) {
                    buildDrawingCache(true);
                    Bitmap cache = getDrawingCache(true);
                    if (cache != null) {
                        canvas.drawBitmap(cache, 0, 0, mLayerPaint);
                    }
                } else {
                    computeScroll();

                    canvas.translate(-mScrollX, -mScrollY);
                    mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;

                    // Fast path for layouts with no backgrounds
                    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                        dispatchDraw(canvas);
                        drawAutofilledHighlight(canvas);
                        if (mOverlay != null && !mOverlay.isEmpty()) {
                            mOverlay.getOverlayView().draw(canvas);
                        }
                        if (debugDraw()) {
                            debugDrawFocus(canvas);
                        }
                    } else {
                        draw(canvas);
                    }
                }
            } finally {
                renderNode.end(canvas);
                setDisplayListProperties(renderNode);
            }
  • 如果本View不需要更新RendorNode隆夯,則調(diào)用dispathcGetDisplayList,讓其子view更新RendorNode别伏。最終會導致步驟2蹄衷。
  • 如果本View需要更新RendorNode,則調(diào)用draw(Canvas)方法厘肮。
    繼續(xù)跟蹤draw(Canvas)方法
 if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

我們知道每個view的DrawRenderNodeOp緩存主要包括displayListOps和其子View的DrawRenderNodeOp

  • drawBackground愧口、onDraw 、onDrawForeground类茂、drawDefaultFocusHighlight是更新自身相關(guān)的displayListOps
  • dispatchDraw向下觸發(fā)便利耍属,更新子View相關(guān)的DrawRenderNodeOp。


    drawOps更新.png

    dispatchDraw最終會走到draw(Canvas canvas, ViewGroup parent, long drawingTime) ,看一下里面的代碼

....
   if (drawingWithRenderNode) {
            // Delay getting the display list until animation-driven alpha values are
            // set up and possibly passed on to the view
            renderNode = updateDisplayListIfDirty();
            if (!renderNode.isValid()) {
                // Uncommon, but possible. If a view is removed from the hierarchy during the call
                // to getDisplayList(), the display list will be marked invalid and we should not
                // try to use it again.
                renderNode = null;
                drawingWithRenderNode = false;
            }
        }
 if (!drawingWithDrawingCache) {
            if (drawingWithRenderNode) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
            } else {
                // Fast path for layouts with no backgrounds
                if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                    dispatchDraw(canvas);
                } else {
                    draw(canvas);
                }
            }
        } 
....
  • 子view通過updateDisplayListIfDirty刷新自己的RendNode巩检。如果子View本身恬涧,以及子View的子控件沒有變化,RenderNode不會發(fā)生實質(zhì)的變化碴巾。
  • 最后通過DisplayListCanvas的drawRenderNode將繪制緩存保存到父View 中

DisplayListCanvas類分析溯捆,以drawLine為例

void DisplayListCanvas::drawLines(const float* points, int count, const SkPaint& paint) {
    points = refBuffer<float>(points, count);

    addDrawOp(new (alloc()) DrawLinesOp(points, count, refPaint(&paint)));
}

RenderThread渲染過程

DrawOp樹構(gòu)建完畢后,UI線程利用RenderProxy向RenderThread線程發(fā)送一個DrawFrameTask任務(wù)請求,RenderThread被喚醒提揍,開始渲染啤月。

  • 首先進行DrawOp的合并
  • 接著繪制特殊的Layer
  • 最后繪制其余所有的DrawOpList
  • 調(diào)用swapBuffers將前面已經(jīng)繪制好的圖形緩沖區(qū)提交給Surface Flinger合成和顯示。

開啟硬件加速的優(yōu)缺點

優(yōu)點

  • 硬件加速使用雙線程工作劳跃,主線程只負責構(gòu)建繪制樹谎仲,渲染線程負責渲染。主線程基本上不會因為繪制超時而卡頓刨仑。
  • GPU分擔了CPU的渲染任務(wù)郑诺,CPU有多余的時間做其他重要的事情,這將使得手機整體表現(xiàn)將更加流暢杉武。
  • GPU擅長做渲染工作辙诞,硬件加速允許應用執(zhí)行繁重的渲染任務(wù)。如果沒有GPU或者不采用硬件加速轻抱,播放視頻將非撤赏浚卡頓
    缺點
  • 硬件加速屬于雙緩沖機制,使用顯存進行頁面渲染(使用較少的物理內(nèi)存)祈搜,導致更頻繁的顯存操作较店,可能引起以下現(xiàn)象:
    花屏、閃屏
  • 硬件加速的準備工作較長容燕,可能在應用剛啟動時梁呈,存在掉幀現(xiàn)象。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蘸秘,一起剝皮案震驚了整個濱河市官卡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌秘血,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件评甜,死亡現(xiàn)場離奇詭異灰粮,居然都是意外死亡,警方通過查閱死者的電腦和手機忍坷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門粘舟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人佩研,你說我怎么就攤上這事柑肴。” “怎么了旬薯?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵晰骑,是天一觀的道長。 經(jīng)常有香客問我绊序,道長硕舆,這世上最難降的妖魔是什么秽荞? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮抚官,結(jié)果婚禮上扬跋,老公的妹妹穿的比我還像新娘。我一直安慰自己凌节,他們只是感情好钦听,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著倍奢,像睡著了一般朴上。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上娱挨,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天余指,我揣著相機與錄音,去河邊找鬼跷坝。 笑死酵镜,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的柴钻。 我是一名探鬼主播淮韭,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼贴届!你這毒婦竟也來了靠粪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤毫蚓,失蹤者是張志新(化名)和其女友劉穎占键,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體元潘,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡畔乙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了翩概。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片牲距。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖钥庇,靈堂內(nèi)的尸體忽然破棺而出牍鞠,到底是詐尸還是另有隱情,我是刑警寧澤评姨,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布难述,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏龄广。R本人自食惡果不足惜硫眯,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望择同。 院中可真熱鬧两入,春花似錦、人聲如沸敲才。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽紧武。三九已至剃氧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間阻星,已是汗流浹背朋鞍。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留妥箕,地道東北人滥酥。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像畦幢,于是被迫代替她去往敵國和親坎吻。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容