性能優(yōu)化之卡頓分析

????????大多數(shù)用戶感知到的卡頓等性能問(wèn)題主要原因都是因?yàn)殇秩拘阅堋?/p>

????????Android 系統(tǒng)每隔大概16.6毫秒(1000ms / 60)發(fā)出VSYNC信號(hào)耸采,觸發(fā)對(duì)UI進(jìn)行渲染,如果每次渲染成功麦备,這樣就能夠達(dá)到畫面所需要的的60fps ,要做到實(shí)現(xiàn)60fps,就意味著程序渲染工作需要在16ms之內(nèi)完成。

認(rèn)識(shí)卡頓現(xiàn)象

12fps: 類似于手動(dòng)快速翻書的幀率;

24fps:人肉眼感知的連續(xù)線的運(yùn)動(dòng)效果凛篙;通常是電影膠卷使用的幀率

60fps:是人眼與大腦之間的協(xié)作無(wú)感知的畫面更新

????????CPU: 負(fù)責(zé)measure黍匾、layout、record呛梆、execute的計(jì)算操作

????????GPU:負(fù)責(zé)將資源組件拆分到不同的像素上顯示锐涯;

????????display: 負(fù)責(zé)將Frame buffer中的數(shù)據(jù)顯示出來(lái)

????????當(dāng)系統(tǒng)每隔16ms發(fā)出VSYNC時(shí),如果CPU ,GPU都已經(jīng)完成了相關(guān)的操作填物,那么display繪制就會(huì)很順暢纹腌,但是如果CPU/GPU還在生產(chǎn)幀數(shù)據(jù),從幀緩存中讀取出來(lái)的數(shù)據(jù)就是之前的滞磺,這樣在兩個(gè)刷新周期之間顯示了同一幀的數(shù)據(jù)升薯,這就是我們通常說(shuō)的發(fā)生了<u>丟幀現(xiàn)象</u>

監(jiān)控卡頓的方法:

????????如果需要準(zhǔn)確分析卡頓發(fā)生在哪一個(gè)函數(shù),資源占用情況如何等击困,我們介紹兩種比較主流的監(jiān)控方案:

1. 利用主線程的Looper打印日志 (BlockCanary的原理)

先來(lái)看看Looper.java的源碼

//此接口的實(shí)現(xiàn)類涎劈,系統(tǒng)給我們提供了一個(gè)LogPrinter
public interface Printer {
    /**
     * Write a line of text to the output.  There is no need to terminate
     * the given string with a newline.
     */
    void println(String x);
}


。阅茶。蛛枚。。脸哀。蹦浦。。企蹭。白筹。。谅摄。。系馆。送漠。。由蘑。闽寡。。尼酿。爷狈。。裳擎。涎永。。。羡微。谷饿。。妈倔。博投。。盯蝴。毅哗。。捧挺。虑绵。。松忍。蒸殿。。鸣峭。宏所。。摊溶。爬骤。。莫换。霞玄。。拉岁。坷剧。。喊暖。馒稍。兽愤。县匠。肥缔。。巩掺。偏序。。胖替。研儒。豫缨。。殉摔。州胳。。逸月。栓撞。。
/**
     * Control logging of messages as they are processed by this Looper.  If
     * enabled, a log message will be written to <var>printer</var>
     * at the beginning and ending of each message dispatch, identifying the
     * target Handler and message contents.
     *
     * @param printer A Printer object that will receive log messages, or
     * null to disable message logging.
     */
    public void setMessageLogging(@Nullable Printer printer) {
        mLogging = printer;
    }


碗硬。瓤湘。。恩尾。弛说。。翰意。木人。。冀偶。醒第。。进鸠。稠曼。。客年。霞幅。。量瓜。司恳。。绍傲。抵赢。。唧取。。划提。枫弟。。鹏往。淡诗。骇塘。。韩容。款违。。群凶。插爹。。请梢。赠尾。。毅弧。气嫁。。够坐。寸宵。。元咙。梯影。。蛾坯。光酣。。脉课。救军。。倘零。
/**
     * 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();

        // Allow overriding a threshold with a system prop. e.g.
        // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
        final int thresholdOverride =
                SystemProperties.getInt("log.looper."
                        + Process.myUid() + "."
                        + Thread.currentThread().getName()
                        + ".slow", 0);

        boolean slowDeliveryDetected = false;

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            //此處如果logging不為空唱遭,那么他會(huì)調(diào)用Printer的println()打印一些信息,而Printer是一個(gè)接口呈驶,就需要我們自己去實(shí)現(xiàn)這個(gè)接口拷泽,并完成我們自己的Println()函數(shù)的方法體,打印我們自己需要的信息
            // 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 traceTag = me.mTraceTag;
            long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
            long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
            if (thresholdOverride > 0) {
                slowDispatchThresholdMs = thresholdOverride;
                slowDeliveryThresholdMs = thresholdOverride;
            }
            final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
            final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

            final boolean needStartTime = logSlowDelivery || logSlowDispatch;
            final boolean needEndTime = logSlowDispatch;

            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }

            final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
            final long dispatchEnd;
            try {
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (logSlowDelivery) {
                if (slowDeliveryDetected) {
                    if ((dispatchStart - msg.when) <= 10) {
                        Slog.w(TAG, "Drained");
                        slowDeliveryDetected = false;
                    }
                } else {
                    if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                            msg)) {
                        // Once we write a slow delivery log, suppress until the queue drains.
                        slowDeliveryDetected = true;
                    }
                }
            }
            if (logSlowDispatch) {
                showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
            }

            //此處打印結(jié)束
            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();
        }
    }

????????如果我們想用主線程的Looper來(lái)打印相關(guān)的日志信息袖瞻,那么我們就需要給Looper實(shí)例提供一個(gè)Printer的實(shí)例司致,而Printer是一個(gè)接口,那么我們就來(lái)實(shí)現(xiàn)一個(gè)Printer的實(shí)現(xiàn)類:LogMonitor.java

public class LogMonitor implements Printer {

    private StackSampler mStackSampler;
    private boolean mPrintingStarted = false;
    private long mStartTimestamp;
    // 卡頓閾值
    private long mBlockThresholdMillis = 3000;
    //采樣頻率
    private long mSampleInterval = 1000;
    private Handler mLogHandler;

    public LogMonitor() {
        mStackSampler = new StackSampler(mSampleInterval);
        HandlerThread handlerThread = new HandlerThread("block-canary-io");
        handlerThread.start();
        mLogHandler = new Handler(handlerThread.getLooper());
    }

    @Override
    public void println(String x) {
        //從if到else會(huì)執(zhí)行 dispatchMessage聋迎,如果執(zhí)行耗時(shí)超過(guò)閾值脂矫,輸出卡頓信息
        if (!mPrintingStarted) {
            //記錄開始時(shí)間
            mStartTimestamp = System.currentTimeMillis();
            mPrintingStarted = true;
            mStackSampler.startDump();
        } else {
            final long endTime = System.currentTimeMillis();
            mPrintingStarted = false;
            //出現(xiàn)卡頓
            if (isBlock(endTime)) {
                notifyBlockEvent(endTime);
            }
            mStackSampler.stopDump();
        }
    }

    private void notifyBlockEvent(final long endTime) {
        mLogHandler.post(new Runnable() {
            @Override
            public void run() {
                //獲得卡頓時(shí) 主線程堆棧
                List<String> stacks = mStackSampler.getStacks(mStartTimestamp, endTime);
                for (String stack : stacks) {
                    Log.e("block-canary", stack);
                }
            }
        });
    }

    private boolean isBlock(long endTime) {
        return endTime - mStartTimestamp > mBlockThresholdMillis;
    }
}

????????再來(lái)一個(gè)堆棧信息采集器:

public class StackSampler {

    public static final String SEPARATOR = "\r\n";
    public static final SimpleDateFormat TIME_FORMATTER =
            new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
    private Handler mHandler;
    private Map<Long, String> mStackMap = new LinkedHashMap<>();
    private int mMaxCount = 100;
    private long mSampleInterval;
    //是否需要采樣
    protected AtomicBoolean mShouldSample = new AtomicBoolean(false);

    public StackSampler(long sampleInterval) {
        mSampleInterval = sampleInterval;
        HandlerThread handlerThread = new HandlerThread("block-canary-sampler");
        handlerThread.start();
        mHandler = new Handler(handlerThread.getLooper());
    }

    /**
     * 開始采樣 執(zhí)行堆棧
     */
    public void startDump() {
        //避免重復(fù)開始
        if (mShouldSample.get()) {
            return;
        }
        mShouldSample.set(true);
        mHandler.removeCallbacks(mRunnable);
        mHandler.postDelayed(mRunnable, mSampleInterval);
    }

    public void stopDump() {
        if (!mShouldSample.get()) {
            return;
        }
        mShouldSample.set(false);
        mHandler.removeCallbacks(mRunnable);
    }

    public List<String> getStacks(long startTime, long endTime) {
        ArrayList<String> result = new ArrayList<>();
        synchronized (mStackMap) {
            for (Long entryTime : mStackMap.keySet()) {
                if (startTime < entryTime && entryTime < endTime) {
                    result.add(TIME_FORMATTER.format(entryTime)
                            + SEPARATOR
                            + SEPARATOR
                            + mStackMap.get(entryTime));
                }
            }
        }
        return result;
    }

    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            StringBuilder sb = new StringBuilder();
            StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
            for (StackTraceElement s : stackTrace) {
                sb.append(s.toString()).append("\n");
            }
            synchronized (mStackMap) {
                //最多保存100條堆棧信息
                if (mStackMap.size() == mMaxCount) {
                    mStackMap.remove(mStackMap.keySet().iterator().next());
                }
                mStackMap.put(System.currentTimeMillis(), sb.toString());
            }
            if (mShouldSample.get()) {
                mHandler.postDelayed(mRunnable, mSampleInterval);
            }
        }
    };
}

????????再來(lái)一個(gè)靜態(tài)方法調(diào)用使用:

public class BlockCanary {
    public static void install() {
        LogMonitor logMonitor = new LogMonitor();
        Looper.getMainLooper().setMessageLogging(logMonitor);
    }
}

????????來(lái)看一下BlockCanary的工作流程圖


BlockCanary采集卡頓信息的UML圖

????????采集的信息截圖,當(dāng)然你也可以修改成保存到文件中去:


BlockCannary采集的堆棧信息.png

2. 使用Choreographer.FrameCallback監(jiān)控

????????當(dāng)每一幀被渲染時(shí)會(huì)觸發(fā)此接口的回調(diào)霉晕,并帶著底層VSYNC信息到達(dá)的時(shí)間戳庭再。

/**
     * Implement this interface to receive a callback when a new display frame is
     * being rendered.  The callback is invoked on the {@link Looper} thread to
     * which the {@link Choreographer} is attached.
     */
    public interface FrameCallback {
        /**
         * Called when a new display frame is being rendered.
         * <p>
         * This method provides the time in nanoseconds when the frame started being rendered.
         * The frame time provides a stable time base for synchronizing animations
         * and drawing.  It should be used instead of {@link SystemClock#uptimeMillis()}
         * or {@link System#nanoTime()} for animations and drawing in the UI.  Using the frame
         * time helps to reduce inter-frame jitter because the frame time is fixed at the time
         * the frame was scheduled to start, regardless of when the animations or drawing
         * callback actually runs.  All callbacks that run as part of rendering a frame will
         * observe the same frame time so using the frame time also helps to synchronize effects
         * that are performed by different callbacks.
         * </p><p>
         * Please note that the framework already takes care to process animations and
         * drawing using the frame time as a stable time base.  Most applications should
         * not need to use the frame time information directly.
         * </p>
         *
         * @param frameTimeNanos The time in nanoseconds when the frame started being rendered,
         * in the {@link System#nanoTime()} timebase.  Divide this value by {@code 1000000}
         * to convert it to the {@link SystemClock#uptimeMillis()} time base.
         */
        public void doFrame(long frameTimeNanos);
    }

????????所以我們就自定義一個(gè)該接口的實(shí)現(xiàn):

public class ChoreographerHelper {

    public static void start() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
                long lastFrameTimeNanos = 0;

                @Override
                public void doFrame(long frameTimeNanos) {
                    //上次回調(diào)時(shí)間
                    if (lastFrameTimeNanos == 0) {
                        lastFrameTimeNanos = frameTimeNanos;
                        Choreographer.getInstance().postFrameCallback(this);
                        return;
                    }
                    long diff = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000;
                    if (diff > 16.6f) {
                        //掉幀數(shù)
                        int droppedCount = (int) (diff / 16.6);
                    }
                    lastFrameTimeNanos = frameTimeNanos;
                    Choreographer.getInstance().postFrameCallback(this);
                }
            });
        }
    }
}

????????上面的實(shí)現(xiàn)可以幫助我們獲取實(shí)時(shí)的幀數(shù)和掉幀數(shù)捞奕,如果掉的比較多時(shí)(有可能卡頓),我們可以用第一種監(jiān)控方式里面的堆棧信息采集器來(lái)進(jìn)行采集拄轻,同時(shí)也可以輸出采集的信息颅围。

???????? 以上兩種監(jiān)控卡頓的方式僅供參考,感謝騰訊-享學(xué)課堂的老師的資料恨搓,

常見(jiàn)卡頓的處理:

  1. 嵌套太深院促,過(guò)于復(fù)雜的布局

????????系統(tǒng)對(duì)于視圖的繪制過(guò)程包括measure ,layout ,draw三個(gè)過(guò)程,如果嵌套的太深奶卓,自然對(duì)每個(gè)視圖都進(jìn)行三步測(cè)繪過(guò)程就會(huì)需要更多的時(shí)間來(lái)完成一疯,這樣就會(huì)造成卡頓等現(xiàn)象。

解決方案:

????????參考《Android性能優(yōu)化之布局優(yōu)化》

  1. 過(guò)度的繪制(OverDraw)

????????如果在屏幕上的某一個(gè)像素點(diǎn)有多次繪制夺姑,就是過(guò)度繪制了墩邀,較為常見(jiàn)的就是重復(fù)的繪制背景繪制一些不可見(jiàn)的UI元素。

????????我們可以在我們?cè)O(shè)備的“系統(tǒng)設(shè)置”->"開發(fā)者選項(xiàng)"->"調(diào)試GPU過(guò)度繪制"中開啟調(diào)試盏浙,此時(shí)你的設(shè)備界面可能出現(xiàn)五種顏色標(biāo)識(shí):

????????????????原色: 沒(méi)有過(guò)度繪制

????????????????藍(lán)色: 1次過(guò)度繪制

????????????????綠色: 2次過(guò)度繪制

????????????????粉色: 3次過(guò)度繪制

????????????????紅色: 大于等于4次繪制

???????? 解決方案:

????????1)移除一些不需要的背景

????????如果有些子視圖有背景眉睹,而且會(huì)覆蓋父視圖時(shí),那么主視圖的背景就不必要設(shè)置废膘,系統(tǒng)對(duì)于沒(méi)有背景的是不會(huì)直接渲染內(nèi)容竹海,這樣就可以提高渲染的性能。

????????2)使視圖層級(jí)結(jié)構(gòu)扁平化

????????優(yōu)化布局的層次層次來(lái)減少重疊的視圖

????????3) 降低透明度

????????系統(tǒng)對(duì)于不透明的view ,之需要渲染一次就可以顯示出來(lái)丐黄,而如果設(shè)置了透明度斋配,則至少需要渲染兩次(因?yàn)橄到y(tǒng)要先知道他的下層元素是什么,然后再結(jié)合上層的view進(jìn)行混色處理)灌闺。

????????透明的動(dòng)畫艰争,淡入淡出和陰影效果等都是和透明度相關(guān)的,這樣就會(huì)造成過(guò)度繪制桂对。

  1. 異步加載布局

????????LayoutInflater 在加載XML布局的過(guò)程中會(huì)在主線程使用IO讀取XML文件進(jìn)行解析甩卓,再跟進(jìn)解析的結(jié)果利用反射創(chuàng)建布局中的View/ViewGroup對(duì)象。這個(gè)過(guò)程會(huì)雖然布局的復(fù)雜度上升蕉斜,耗時(shí)自然也會(huì)隨之增大逾柿。

????????AsyncLayoutInflater 內(nèi)部是一個(gè)線程來(lái)進(jìn)行遞歸遍歷xml文件的節(jié)點(diǎn),然后全部解析完成后將結(jié)果通過(guò)callback回調(diào)到主線程宅此。

implementation "androidx.asynclayoutinflater:asynclayoutinflater:1.0.0"

public class MainActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new AsyncLayoutInflater(this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
            @Override
            public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {
                setContentView(view);
            }
        });
        
    }

????????AsyncLayoutInflater的使用局限性:

????????????????1) 不支持包含F(xiàn)ragment的layout

????????????????2) 不支持設(shè)置LayoutInflater.Factory 或LayoutInflator.Factory2

????????????????3) 需要一個(gè)線程安全的generateLayoutParams 的parent

????????????????4) 如果無(wú)法異步構(gòu)造的布局机错,則會(huì)自動(dòng)退回到UI主線程上

????????????????5) 需要構(gòu)建的view中不能直接使用Handler或者調(diào)用Looper.myLooper(),因?yàn)楫惒骄€程默認(rèn)情況下是沒(méi)有調(diào)用Looper.prepare();

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市父腕,隨后出現(xiàn)的幾起案子毡熏,更是在濱河造成了極大的恐慌,老刑警劉巖侣诵,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痢法,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡杜顺,警方通過(guò)查閱死者的電腦和手機(jī)财搁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)躬络,“玉大人尖奔,你說(shuō)我怎么就攤上這事∏畹保” “怎么了提茁?”我有些...
    開封第一講書人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)馁菜。 經(jīng)常有香客問(wèn)我茴扁,道長(zhǎng),這世上最難降的妖魔是什么汪疮? 我笑而不...
    開封第一講書人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任峭火,我火速辦了婚禮,結(jié)果婚禮上智嚷,老公的妹妹穿的比我還像新娘卖丸。我一直安慰自己,他們只是感情好盏道,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開白布稍浆。 她就那樣靜靜地躺著,像睡著了一般猜嘱。 火紅的嫁衣襯著肌膚如雪衅枫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,488評(píng)論 1 302
  • 那天泉坐,我揣著相機(jī)與錄音为鳄,去河邊找鬼。 笑死腕让,一個(gè)胖子當(dāng)著我的面吹牛孤钦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播纯丸,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼偏形,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了觉鼻?” 一聲冷哼從身側(cè)響起俊扭,我...
    開封第一講書人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎坠陈,沒(méi)想到半個(gè)月后萨惑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捐康,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年庸蔼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了解总。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡姐仅,死狀恐怖花枫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情掏膏,我是刑警寧澤劳翰,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站馒疹,受9級(jí)特大地震影響佳簸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜行冰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一溺蕉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧悼做,春花似錦疯特、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至朽色,卻和暖如春邻吞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背葫男。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工抱冷, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人梢褐。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓旺遮,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親盈咳。 傳聞我的和親對(duì)象是個(gè)殘疾皇子耿眉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

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