性能優(yōu)化-界面卡頓和丟幀(Choreographer 代碼檢測(cè))

標(biāo)簽: Choreographer UI卡頓 UI丟幀


作者公眾號(hào):



本文將介紹3個(gè)知識(shí)點(diǎn):

  1. 獲取系統(tǒng)UI刷新頻率
  2. 檢測(cè)UI丟幀和卡頓
  3. 輸出UI丟幀和卡頓堆棧信息
    Choreographer.jpg

系統(tǒng)UI刷新頻率

Android系統(tǒng)每隔16ms重繪UI界面,16ms是因?yàn)锳ndroid系統(tǒng)規(guī)定UI繪圖的刷新頻率60FPS归露。Android系統(tǒng)每隔16ms靶擦,發(fā)送一個(gè)系統(tǒng)級(jí)別信號(hào)VSYNC喚起重繪操作雇毫。1秒內(nèi)繪制UI界面60次棚放。每16ms為一個(gè)UI界面繪制周期。
現(xiàn)在有些手機(jī)廠商的手機(jī)屏幕刷新頻率已經(jīng)是120FPS馍迄,每隔8.3毫秒重繪UI界面攀圈;
獲取系統(tǒng)UI刷新頻率

    private float getRefreshRate() { //獲取屏幕主頻頻率
        Display display = getWindowManager().getDefaultDisplay();
        float refreshRate = display.getRefreshRate();
        Log.d(TAG, "屏幕主頻頻率 =" + refreshRate);
        return refreshRate;
    }

log打印如下:

D/MainActivity: 屏幕主頻頻率 =60.0

UI丟幀和卡頓檢查-Choreographer

平常所說(shuō)的“丟幀”情況峦甩,并不是真的把繪圖的幀給“丟失”了,也而是UI繪圖的操作沒有和系統(tǒng)16ms的繪圖更新頻率步調(diào)一致犬辰,開發(fā)者代碼在繪圖中繪制操作太多,導(dǎo)致操作的時(shí)間超過(guò)16ms灸促,在Android系統(tǒng)需要在16ms時(shí)需要重繪的時(shí)刻由于UI線程被阻塞而繪制失敗浴栽。如果丟的幀數(shù)量是一兩幀,用戶在視覺上沒有明顯感覺吃度,但是如果超過(guò)3幀椿每,用戶就有視覺上的感知英遭。丟幀數(shù)如果再持續(xù)增多挖诸,在視覺上就是所謂的“卡頓”多律。

丟幀是引起卡頓的重要原因。在Android中可以通過(guò)Choreographer檢測(cè)Android系統(tǒng)的丟幀情況辽装。

public class MainActivity extends Activity {
    ...
    private MyFrameCallback mFrameCallback = new MyFrameCallback();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Choreographer.getInstance().postFrameCallback(mFrameCallback);
        
        MYTest();
        button = findViewById(R.id.bottom);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                uiLongTimeWork();
                Log.d(MainActivity.class.getSimpleName(), "button click");
            }
        });
    }

    private void MYTest() {
        setContentView(R.layout.activity_main);
        Log.d(MainActivity.class.getSimpleName(), "MYTest");
    }

    private float getRefreshRate() { //獲取屏幕主頻頻率
        Display display = getWindowManager().getDefaultDisplay();
        float refreshRate = display.getRefreshRate();
//        Log.d(TAG, "屏幕主頻頻率 =" + refreshRate);
        return refreshRate;
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public class MyFrameCallback implements Choreographer.FrameCallback {
        private String TAG = "性能檢測(cè)";
        private long lastTime = 0;
        
        @Override
        public void doFrame(long frameTimeNanos) {
            if (lastTime == 0) {
                //代碼第一次初始化。不做檢測(cè)統(tǒng)計(jì)丰涉。
                lastTime = frameTimeNanos;
            } else {
                long times = (frameTimeNanos - lastTime) / 1000000;
                int frames = (int) (times / (1000/getRefreshRate()));
                if (times > 16) {
                    Log.w(TAG, "UI線程超時(shí)(超過(guò)16ms):" + times + "ms" + " , 丟幀:" + frames);
                }
                lastTime = frameTimeNanos;
            }
            Choreographer.getInstance().postFrameCallback(mFrameCallback);
        }
    }

    private void uiLongTimeWork() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

Choreographer周期性的在UI重繪時(shí)候觸發(fā)一死,在代碼中記錄上一次和下一次繪制的時(shí)間間隔投慈,如果超過(guò)16ms,就意味著一次UI線程重繪的“丟幀”瘩绒。丟幀的數(shù)量為間隔時(shí)間除以16,如果超過(guò)3锁荔,就開始有卡頓的感知蟀给。
Log如下

W/性能檢測(cè): UI線程超時(shí)(超過(guò)16ms):33ms , 丟幀:1
W/性能檢測(cè): UI線程超時(shí)(超過(guò)16ms):19ms , 丟幀:1
W/性能檢測(cè): UI線程超時(shí)(超過(guò)16ms):1016ms , 丟幀:60
W/性能檢測(cè): UI線程超時(shí)(超過(guò)16ms):24ms , 丟幀:1
W/性能檢測(cè): UI線程超時(shí)(超過(guò)16ms):21ms , 丟幀:1
W/性能檢測(cè): UI線程超時(shí)(超過(guò)16ms):1016ms , 丟幀:60
W/性能檢測(cè): UI線程超時(shí)(超過(guò)16ms):23ms , 丟幀:1
W/性能檢測(cè): UI線程超時(shí)(超過(guò)16ms):33ms , 丟幀:1

如果手動(dòng)點(diǎn)擊按鈕故意阻塞1秒,丟棄的幀數(shù)更多阳堕。丟幀:60跋理,就是點(diǎn)擊button按鈕,執(zhí)行uiLongTimeWork產(chǎn)生的恬总;

UI丟幀和卡頓堆棧信息輸出

以上是“UI丟幀和卡頓檢查-Choreographer”使用Android的Choreographer監(jiān)測(cè)App發(fā)生的UI卡頓丟幀問題前普。Choreographer本身依賴于Android主線程的Looper消息機(jī)制。
發(fā)生在Android主線程的每(1000/UI刷新頻率)ms重繪操作依賴于Main Looper中消息的發(fā)送和獲取壹堰。如果App一切運(yùn)行正常拭卿,無(wú)卡頓無(wú)丟幀現(xiàn)象發(fā)生,那么開發(fā)者的代碼在主線程Looper消息隊(duì)列中發(fā)送和接收消息的時(shí)間會(huì)很短峻厚,理想情況是(1000/UI刷新頻率)ms惠桃,這是也是Android系統(tǒng)規(guī)定的時(shí)間。但是呐馆,如果一些發(fā)生在主線程的代碼寫的太重摹恰,執(zhí)行任務(wù)花費(fèi)時(shí)間太久,就會(huì)在主線程延遲Main Looper的消息在(1000/UI刷新頻率)ms尺度范圍內(nèi)的讀和寫遣耍。

先看下Android官方實(shí)現(xiàn)的Looper中l(wèi)oop()函數(shù)代碼官方實(shí)現(xiàn):

/**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;
 
        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();
 
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
 
            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
 
            final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
 
            final long traceTag = me.mTraceTag;
            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
            final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            final long end;
            try {
                msg.target.dispatchMessage(msg);
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (slowDispatchThresholdMs > 0) {
                final long time = end - start;
                if (time > slowDispatchThresholdMs) {
                    Slog.w(TAG, "Dispatch took " + time + "ms on "
                            + Thread.currentThread().getName() + ", h=" +
                            msg.target + " cb=" + msg.callback + " msg=" + msg.what);
                }
            }
 
            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
 
            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }
 
            msg.recycleUnchecked();
        }
    }

在loop()函數(shù)中赊豌,Android完成了Looper消息隊(duì)列的分發(fā)碘饼,在分發(fā)消息開始,會(huì)打印一串log日志:

   logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);

同時(shí)在消息處理結(jié)束后也會(huì)打印一串消息日志:

logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);

正常的情況下钠绍,分發(fā)消息開始到消息結(jié)束柳爽,理想的情況下應(yīng)該在(1000/UI刷新頻率)ms以內(nèi)。但是分發(fā)處理的消息到上層争拐,由開發(fā)者代碼接管并處理架曹,如果耗時(shí)太久,就很可能超出(1000/UI刷新頻率)ms万牺,也即發(fā)生了丟幀脚粟,超時(shí)太多核无,由于Android系統(tǒng)依賴主線程Looper重繪UI的消息遲遲得不到處理噪沙,那么就導(dǎo)致繪圖動(dòng)作停滯正歼,用戶視覺上就會(huì)感受到卡頓朋腋。
利用這一特性和情景,可以使用主線程的Looper監(jiān)測(cè)系統(tǒng)發(fā)生的卡頓和丟幀穷绵。具體是這樣的:
首先給App的主線程Looper注冊(cè)一個(gè)自己的消息日志輸出打印器仲墨,正常情況下,該日志打印器將輸出全部的Android Looper上的日志癌蚁,但是在這里,技巧性的過(guò)濾兩個(gè)特殊日志:

>>>>> Dispatching to

表示Looper開始分發(fā)主線程上的消息咬摇。

<<<<< Finished to

表示Looper分發(fā)主線程上的消失結(jié)束逸邦。
從>>>>> Dispatching to 到 <<<<< Finished to 之間這段操作,就是留給開發(fā)者所寫的代碼發(fā)生在上層主線程操作的動(dòng)作烛卧,通常所說(shuō)的卡頓也就發(fā)生這一段。

正確情況下局雄,從消息分發(fā)(>>>>> Dispatching to)開始炬搭,到消息處理結(jié)束(<<<<< Finished to),這段操作理想情況應(yīng)在(1000/UI刷新頻率)ms以內(nèi)完成灼芭,如果超過(guò)這一時(shí)間彼绷,也即意味著卡頓和丟幀。

現(xiàn)在設(shè)計(jì)一種技巧性的編程方案:在(>>>>> Dispatching to)開始時(shí)候猜旬,延時(shí)一定時(shí)間(THREAD_HOLD)執(zhí)行一個(gè)線程,延時(shí)時(shí)間為THREAD_HOLD秘遏,該線程只完成打印當(dāng)前Android堆棧的信息。THREAD_HOLD即為開發(fā)者意圖捕捉到的超時(shí)時(shí)間倦蚪。如果沒什么意外,該線程在THREAD_HOLD后慕购,就打印出當(dāng)前Android的堆棧信息获洲。巧就巧妙在利用這一點(diǎn)兒,因?yàn)檠訒r(shí)THREAD_HOLD執(zhí)行的線程和主線程Looper中的線程是并行執(zhí)行的门岔,當(dāng)在>>>>> Dispatching to時(shí)刻把延時(shí)線程任務(wù)構(gòu)建完拋出去等待THREAD_HOLD后執(zhí)行,而當(dāng)前的Looper線程中的消息分發(fā)也在執(zhí)行牢裳,這兩個(gè)是并發(fā)執(zhí)行的不同線程蒲讯。
設(shè)想如果Looper線程中的操作代碼很快就執(zhí)行完畢,不到16ms就到了<<<<< Finished to晦墙,那么毫無(wú)疑問當(dāng)前的主線程無(wú)卡頓和丟幀發(fā)生。如果特意把THREAD_HOLD設(shè)置成大于16ms的延時(shí)時(shí)間抗楔,比如1000ms,如果線程運(yùn)行順暢不卡頓無(wú)丟幀,那么從>>>>> Dispatching to到達(dá)<<<<< Finished to后勺良,把延時(shí)THREAD_HOLD執(zhí)行的線程刪除掉腰池,那么線程就不會(huì)輸出任何堆棧信息讳侨。若不行主線程發(fā)生阻塞,當(dāng)從>>>>> Dispatching to到達(dá)<<<<< Finished to花費(fèi)1000ms甚至更長(zhǎng)時(shí)間后勇婴,而由于到達(dá)<<<<< Finished to的時(shí)候沒來(lái)得及把堆棧打印線程刪除掉,因此就輸出了當(dāng)前堆棧信息,此堆棧信息剛好即為發(fā)生卡頓和丟幀的代碼堆棧添诉,正好就是所需的卡頓和丟幀檢測(cè)代碼。

public class MainActivity extends Activity {
    ...
    private CheckTask mCheckTask = new CheckTask();
    
        @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        check();
        ...
        button = findViewById(R.id.bottom);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                uiLongTimeWork();
                Log.d(MainActivity.class.getSimpleName(), "button click");
            }
        });
    }
    
    private void check() {
        Looper.getMainLooper().setMessageLogging(new Printer() {
            private final String START = ">>>>> Dispatching to";
            private final String END = "<<<<< Finished to";

            @Override
            public void println(String s) {
                if (s.startsWith(START)) {
                    mCheckTask.start();
                } else if (s.startsWith(END)) {
                    mCheckTask.end();
                }
            }
        });
    }
    
    private void uiLongTimeWork() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private class CheckTask {
        private HandlerThread mHandlerThread = new HandlerThread("卡頓檢測(cè)");
        private Handler mHandler;

        private final int THREAD_HOLD = 1000;

        public CheckTask() {
            mHandlerThread.start();
            mHandler = new Handler(mHandlerThread.getLooper());
        }

        private Runnable mRunnable = new Runnable() {
            @Override
            public void run() {
                log();
            }
        };

        public void start() {
            mHandler.postDelayed(mRunnable, THREAD_HOLD);
        }

        public void end() {
            mHandler.removeCallbacks(mRunnable);
        }
    }



    /**
     * 輸出當(dāng)前異常或及錯(cuò)誤堆棧信息捎稚。
     */
    private void log() {
        StringBuilder sb = new StringBuilder();
        StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
        for (StackTraceElement s : stackTrace) {
            sb.append(s + "\n");
        }

        Log.w(TAG, sb.toString());
    }

運(yùn)行輸出:

1970-02-14 17:35:06.367 11590-11590/com.yanbing.aop_project D/MainActivity: button click
1970-02-14 17:35:06.367 11590-11611/com.yanbing.aop_project W/MainActivity: java.lang.String.indexOf(String.java:1658)
    java.lang.String.indexOf(String.java:1638)
    java.lang.String.contains(String.java:2126)
    java.lang.Class.classNameImpliesTopLevel(Class.java:1169)
    java.lang.Class.getEnclosingConstructor(Class.java:1159)
    java.lang.Class.isLocalClass(Class.java:1312)
    java.lang.Class.getSimpleName(Class.java:1219)
    com.yanbing.aop_project.MainActivity$2.onClick(MainActivity.java:71)
    android.view.View.performClick(View.java:6294)
    android.view.View$PerformClick.run(View.java:24770)
    android.os.Handler.handleCallback(Handler.java:790)
    android.os.Handler.dispatchMessage(Handler.java:99)
    android.os.Looper.loop(Looper.java:164)
    android.app.ActivityThread.main(ActivityThread.java:6494)
    java.lang.reflect.Method.invoke(Native Method)
    com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
    com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)

可以看到當(dāng)點(diǎn)擊按鈕故意制造一個(gè)卡頓后,卡頓被檢測(cè)到蒲凶,并且輸出和定位到了卡頓的具體代碼位置。
總結(jié):利用主線程的Looper檢測(cè)卡頓和丟幀灵巧,從成對(duì)的消息分發(fā)(>>>>> Dispatching to)融欧,到消息處理結(jié)束(<<<<< Finished to)权她,正常的理想時(shí)間應(yīng)該在16ms以內(nèi),若當(dāng)前代碼耗時(shí)太多,這一段時(shí)間就會(huì)超過(guò)16ms廓啊。假設(shè)現(xiàn)在要檢測(cè)耗時(shí)超過(guò)1秒(1000ms)的耗時(shí)操作,那就在>>>>> Dispatching to時(shí)刻疮装,拋出一個(gè)延時(shí)執(zhí)行的線程,該線程打印當(dāng)前堆棧的信息,延時(shí)的時(shí)間特意設(shè)置成閾值1000专缠。此種情況下藤肢,正常順暢執(zhí)行無(wú)卡頓無(wú)丟幀的代碼從>>>>> Dispatching to到<<<<< Finished to之間不會(huì)超過(guò)設(shè)置的閾值1000ms蟀淮,因此當(dāng)Looper中的代碼到達(dá)<<<<< Finished to就把之前拋出來(lái)延時(shí)執(zhí)行的線程刪除掉,也就不會(huì)輸出任何堆棧信息策治。但是只有當(dāng)耗時(shí)代碼從>>>>> Dispatching to到<<<<< Finished to超過(guò)了1000ms混蔼,由于Looper中由于耗時(shí)操作很晚(超過(guò)我們?cè)O(shè)定的閾值)才到達(dá)<<<<< Finished to遵湖,沒趕上刪掉堆棧打印線程,于是堆棧線程得以有機(jī)會(huì)打印當(dāng)前堆棧信息烹卒,這就是卡頓和丟幀的發(fā)生場(chǎng)景檢測(cè)機(jī)制。
事實(shí)上可以靈活設(shè)置延時(shí)閾值THREAD_HOLD,從而檢測(cè)到任何大于或等于該時(shí)間的耗時(shí)操作谣辞。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末躯嫉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子杨拐,更是在濱河造成了極大的恐慌帆阳,老刑警劉巖蜒谤,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件旬盯,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)鹃两,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了极景?”我有些...
    開封第一講書人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵译秦,是天一觀的道長(zhǎng)筑悴。 經(jīng)常有香客問我,道長(zhǎng)稍途,這世上最難降的妖魔是什么阁吝? 我笑而不...
    開封第一講書人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮械拍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘定躏。我一直安慰自己绑谣,他們只是感情好壤玫,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開白布她渴。 她就那樣靜靜地躺著牍帚,像睡著了一般肃叶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上十嘿,一...
    開封第一講書人閱讀 51,287評(píng)論 1 301
  • 那天因惭,我揣著相機(jī)與錄音,去河邊找鬼绩衷。 笑死蹦魔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的咳燕。 我是一名探鬼主播勿决,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼招盲!你這毒婦竟也來(lái)了低缩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤曹货,失蹤者是張志新(化名)和其女友劉穎咆繁,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體顶籽,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡玩般,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了礼饱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坏为。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖慨仿,靈堂內(nèi)的尸體忽然破棺而出久脯,到底是詐尸還是另有隱情,我是刑警寧澤镰吆,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站跑慕,受9級(jí)特大地震影響万皿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜核行,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一牢硅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧芝雪,春花似錦减余、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)如筛。三九已至,卻和暖如春抒抬,著一層夾襖步出監(jiān)牢的瞬間杨刨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工擦剑, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留妖胀,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓惠勒,卻偏偏與公主長(zhǎng)得像赚抡,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子纠屋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354