Java層 MessageQueue 消息隊列分析

14201610540457_.pic.jpg

Java層MessageQueue分析

Native 層的MessageQueue簡析

帶著問題找答案:

本文主要解答,handler.sendMessageDelayed()延時消息是如何實現(xiàn)的纯出?

1蚯妇、Thread、Looper暂筝、MessageQueue箩言、Handler 鐵三角關(guān)系

1.1、Looper是依附在Thread之上的焕襟。一個Thread 對一個Looper

public final class Looper {
  static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
   private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
  }
}

調(diào)用Looper.prepare()創(chuàng)建Looper后是保存在sThreadLocal中陨收。

1.2、建立起消息循環(huán),需要線程中調(diào)用Looper.loop()

[Thread]

public class HandlerThread extends Thread{
 @Override
    public void run() {
        Looper.prepare();
        Looper.loop();
    }
}

1.3鸵赖、Handler 持有run起來的Looper,是MessageQueue的入口和出口务漩。

實例化Handler 需要傳入一個Looper對象

 HandlerThread handerThread = new HandlerThread("demo");
 Handler handler = new Handler(handerThread.getLooper()){
    @Override
    public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
    }
};

1.3.1、Message 的核心屬性

Message 的核心屬性

public final class Message implements Parcelable {

          public long when; //(1)消費該消息的預(yù)期時間點
         
          Runnable callback; //(2)Handler.post()方法傳入的Runnable對象

          Handler target; // (3)發(fā)送該消息的handler它褪,最終也是該Message消息的消費者饵骨;配合what執(zhí)行具體的業(yè)務(wù)操作
          public int what; // what 標識具體的業(yè)務(wù)
     }
  • when 標識消費該消息的預(yù)期時間點
  • callback 記錄Handler.post()方法傳入的Runnable對象
  • target 發(fā)送該消息的handler,最終也是該Message消息的消費者
  • what 區(qū)分具體的業(yè)務(wù)

1.3.2茫打、向Handler發(fā)送Message

- handler.sendMessageDelayed(Message.obtain(),1000)
- handler.sendMessage(Message.obtain())
- handler.post(Runnable {  })
- handler.postDelayed(Runnable {  },100)

向handler中添加消息 有多種形式:,無論是sendMessage() 還是post(),最終都會封裝一個Message對象居触,然后調(diào)用sendMessageAtTime()方法。

public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }
sendMessageDelayed()
 public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }
postDelayed()

Runnable會封裝成一個Message對象(Runnable保存Message.callBack屬性上)
然后執(zhí)行sendMessageDelayed()


 private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }
    
 public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {
    return sendMessageDelayed(getPostMessage(r), delayMillis);
}

1.3.3老赤、消費Message

Message從MessageQueue取出之后,會交給target Handler來處理,最終調(diào)用Handler.dispatchMessage()

Message msg = queue.next(); // might block
msg.target.dispatchMessage(msg);
dispatchMessage
public void dispatchMessage(@NonNull Message msg) {
        //(1)Runnable != null 優(yōu)先執(zhí)行Runnable()
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
    
  • 通過handler.post()發(fā)送的消息,Message.callBack 不為空轮洋,優(yōu)先執(zhí)行Runnable.run()
 private static void handleCallback(Message message) {
        message.callback.run();
    }
  • Handler 構(gòu)建時 若指定了mCallback 回調(diào),則由mCallback 來處理消息诗越。一般情況mCallback都為null,所以此種情況可以不考慮
  • 然后Message 交由Handler.handleMessage()來處理
class Handler{
   public void handleMessage(@NonNull Message msg) {}   
}

通過handler.sendMessage()發(fā)送的消息砖瞧,需要復(fù)寫Handler.handleMessage()來具體處理消息

二、延時消息是如何實現(xiàn)的嚷狞。

- handler.sendMessageDelayed(Message.obtain(),1000)
- handler.postDelayed(Runnable {  },100)

這就要看MessageQueue對象了块促。

2.1荣堰、入隊列

[Handler]

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

Handler.enqueueMessage()方法中

  • 將handler本身 賦給了msg.target
  • 調(diào)用MessageQueue.queueMessage()

MessageQueue中最重要的就是兩個方法:
1.enqueueMessage向隊列中插入消息
2.next 從隊列中取出消息

我們先分析enqueueMessage:

boolean enqueueMessage(Message msg, long when) {


        if (msg.target == null) { //排除msg.target == null 的case
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) { //排除 正在使用的msg
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            msg.markInUse();
            msg.when = when;
            Message p = mMessages; //MesssageQueue中的消息隊列 以單向鏈表的形式存在;mMessages 指向鏈表中的第一個幻速
            boolean needWake;

            //(1)新消息插入鏈表表頭:a、當前隊列沒有待處理的消息 或者 b竭翠、新消息msg.when = 0(這種情況一般不存在)或者 c振坚、新消息的觸發(fā)時間 早于mMessages表頭消息的觸發(fā)時間
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else { //(2)將新消息按照msg.when時間的先后順序,插入到mMessages鏈表的中間位置.使整個鏈表以時間排序斋扰。
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            //(3)若當前是阻塞狀態(tài),則喚醒next()操作
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

MessageQueue中的消息 是一個單向鏈表的形式保存的,mMessages 變量 指向鏈表的表頭渡八。Message鏈表中的元素是以觸發(fā)時間(when)為基準,從小道大排列的,when小的排在鏈表前面,優(yōu)先被處理;when大的排在鏈表的后面,延后處理传货。

2.1.1屎鳍、 enqueueMessage 主要做了兩件事情:

(1)將新來的Message消息 插入到鏈表中合適的位置。

滿足以下條件,新message會被插入到鏈表頭部

  • a、當前隊列沒有待處理的消息
  • b挺智、新消息msg.when = 0(這種情況一般不存在)
  • c钦奋、新消息的觸發(fā)時間 早于mMessages表頭消息的觸發(fā)時間
    其他情況,新message會被插入到鏈表中部合適位置。
(2)滿足條件時,喚醒隊列
 if (needWake) {
    nativeWake(mPtr);
 }

什么時候需要喚醒隊列:

  • 新消息插入鏈表頭部時,需要立即喚醒隊列
  • 新消息插入鏈表中部時,一般不需要立即喚醒;但是當鏈表表頭是一個消息屏障忧饭,且先消息是一個異步消息時,才需要喚醒隊列筷畦。

2.1.2词裤、next() 從隊列中消費Message,進行分發(fā)處理

    Message next() {

        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        //自旋
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            //(1)nativePollOnce 嘗試阻塞
            //如果nextPollTimeoutMillis=-1汁咏,一直阻塞不會超時亚斋。
            //如果nextPollTimeoutMillis=0,不會阻塞攘滩,立即返回帅刊。
            //如果nextPollTimeoutMillis>0,最長阻塞nextPollTimeoutMillis毫秒(超時)漂问,如果期間有程序喚醒會立即返回赖瞒。
            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;

                //(2)如果鏈表表頭是一個同步屏障消息,則跳過眾多同步消息,找到鏈表中第一個異步消息,進行分發(fā)處理
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }

                //(3) 返回取到的Message 
                if (msg != null) {
                    //msg尚未到達觸發(fā)時間,則計算新的阻塞超時時間nextPollTimeoutMillis,下次循環(huán)觸發(fā)隊列阻塞
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        //從鏈表中移除該消息后蚤假,直接返回
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        msg.markInUse();
                        return msg;
                    }
                } else { //沒有找到異步消息,設(shè)置nextPollTimeoutMillis=-1,隊列阻塞
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

                // Process the quit message now that all pending messages have been handled.
                //(4)MessageQueue執(zhí)行了quit()栏饮,此處釋放MessageQueue
                if (mQuitting) {
                    dispose();
                    return null;
                }


                //(5) 對已注冊的HandlerIdle回調(diào)的處理
        
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }

next()方法大概可以描述為:從Message鏈表中取出一條Message返回;當鏈表中無消息,或者鏈表中第一個Message尚未到達觸發(fā)時間時,則阻塞next()方法磷仰。

代碼比較長袍嬉,我們分幾部分來講:

(1) nativePollOnce()

通過Native層的epoll來阻塞住當前線程

 nativePollOnce(ptr, nextPollTimeoutMillis);
 private native void nativePollOnce(long ptr, int timeoutMillis)

nativePollOnce根據(jù)傳入的參數(shù)nextPollTimeoutMillis 會有不同的阻塞行為

  • 如果nextPollTimeoutMillis=-1,一直阻塞不會超時。
  • 如果nextPollTimeoutMillis=0伺通,不會阻塞箍土,立即返回。
  • 如果nextPollTimeoutMillis>0罐监,最長阻塞nextPollTimeoutMillis毫秒(超時)吴藻,如果期間有程序喚醒會立即返回。
(2)處理同步屏障消息

如果鏈表表頭是一個同步屏障消息,則跳過眾多同步消息,找到鏈表中第一個異步消息,進行分發(fā)處理

Message 分為同步消息和異步消息弓柱。

class Message{
    public boolean isAsynchronous() {
        return (flags & FLAG_ASYNCHRONOUS) != 0;
    }
    public void setAsynchronous(boolean async) {
        if (async) {
            flags |= FLAG_ASYNCHRONOUS;
        } else {
            flags &= ~FLAG_ASYNCHRONOUS;
        }
    }
}

通常我們使用Handler發(fā)消息都是同步消息沟堡,發(fā)出去之后就會在消息隊列里面排隊處理。我們都知道矢空,Android系統(tǒng)16ms會刷新一次屏幕航罗,如果主線程的消息過多,在16ms之內(nèi)沒有執(zhí)行完妇多,必然會造成卡頓或者掉幀伤哺。那怎么才能不排隊,沒有延時的處理呢者祖?這個時候就需要異步消息。在處理異步消息的時候绢彤,我們就需要同步屏障七问,讓異步消息不用排隊等候處理。

可以理解為同步屏障是一堵墻茫舶,把同步消息隊列攔住械巡,先處理異步消息,等異步消息處理完了饶氏,這堵墻就會取消讥耗,然后繼續(xù)處理同步消息。

MessageQueue里面有postSyncBarrier()可以發(fā)送同步屏障消息

public int postSyncBarrier() {
        return postSyncBarrier(SystemClock.uptimeMillis());
    }

    private int postSyncBarrier(long when) {
        // Enqueue a new sync barrier token.
        // We don't need to wake the queue because the purpose of a barrier is to stall it.
        synchronized (this) {
            final int token = mNextBarrierToken++;
            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            msg.arg1 = token;

            Message prev = null;
            Message p = mMessages;
            if (when != 0) {
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            if (prev != null) { // invariant: p == prev.next
                msg.next = p;
                prev.next = msg;
            } else {
                msg.next = p;
                mMessages = msg;
            }
            return token;
        }
    }

值得注意的是疹启,同步屏障消息沒有target古程,普通的消息的必須有target的。

再回到MessageQueue.next() 方法

//(2)如果鏈表表頭是一個同步屏障消息,則跳過眾多同步消息,找到鏈表中第一個異步消息,進行分發(fā)處理
        if (msg != null && msg.target == null) {
            // Stalled by a barrier.  Find the next asynchronous message in the queue.
            do {
                prevMsg = msg;
                msg = msg.next;
            } while (msg != null && !msg.isAsynchronous());
        }

我們看到,如果鏈表表頭是一個同步屏障消息,就會遍歷鏈表,返回第一個待處理異步消息喊崖。這樣就跳過了前面眾多的同步消息挣磨。

(3) 取到的Message返回還是阻塞?
//(3) 返回取到的Message 
        if (msg != null) {
            //msg尚未到達觸發(fā)時間,則計算新的阻塞超時時間nextPollTimeoutMillis,下次循環(huán)觸發(fā)隊列阻塞
            if (now < msg.when) {
                // Next message is not ready.  Set a timeout to wake up when it is ready.
                nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
            } else {
                //從鏈表中移除該消息后荤懂,直接返回
                // Got a message.
                mBlocked = false;
                if (prevMsg != null) {
                    prevMsg.next = msg.next;
                } else {
                    mMessages = msg.next;
                }
                msg.next = null;
                msg.markInUse();
                return msg;
            }
        } else { //沒有找到異步消息,設(shè)置nextPollTimeoutMillis=-1,隊列阻塞
            // No more messages.
            nextPollTimeoutMillis = -1;
        }

回到next()中的代碼塊,取到待處理的message后做了如下處理

  • 如果msg == null,說明鏈表中沒有消息,則nextPollTimeoutMillis = -1茁裙,下次循環(huán) 會無限阻塞。
  • msg !=null,并且msg.when 符合觸發(fā)條件,則直接返回
  • msg !=null,但是msg.when 尚未到達預(yù)期的觸發(fā)時間點,則重新計算nextPollTimeoutMillis节仿,下次循環(huán)進行固定時長的阻塞晤锥。
(4)mQuitting 的處理

如果調(diào)用了MessageQueue.quit() ,mQuitting = true,隊列中所有的消息都會處理后,會調(diào)用dispose 釋放MessageQueue

if (mQuitting) {
    dispose();
    return null;
}
(5)對已注冊的HandlerIdle回調(diào)的處理

MessageQueue可以注冊HandlerIdle監(jiān)聽,此處對注冊的HandlerIdle做回調(diào)處理。

 //(5) 對已注冊的HandlerIdle回調(diào)的處理
        
        if (pendingIdleHandlerCount < 0
                && (mMessages == null || now < mMessages.when)) {
            pendingIdleHandlerCount = mIdleHandlers.size();
        }
        if (pendingIdleHandlerCount <= 0) {
            // No idle handlers to run.  Loop and wait some more.
            mBlocked = true;
            continue;
        }

        if (mPendingIdleHandlers == null) {
            mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
        }
        mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
                
                
                
                
         for (int i = 0; i < pendingIdleHandlerCount; i++) {
        final IdleHandler idler = mPendingIdleHandlers[i];
        mPendingIdleHandlers[i] = null; // release the reference to the handler

        boolean keep = false;
        try {
            keep = idler.queueIdle();
        } catch (Throwable t) {
            Log.wtf(TAG, "IdleHandler threw exception", t);
        }

        if (!keep) {
            synchronized (this) {
                mIdleHandlers.remove(idler);
            }
        }
    }

三廊宪、參考文章

https://zhuanlan.zhihu.com/p/265859150

http://www.reibang.com/p/8c829dc15950

http://www.reibang.com/p/48cf21ad637b

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末矾瘾,一起剝皮案震驚了整個濱河市女轿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌霜威,老刑警劉巖谈喳,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異戈泼,居然都是意外死亡婿禽,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進店門大猛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來扭倾,“玉大人,你說我怎么就攤上這事挽绩√乓迹” “怎么了?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵唉堪,是天一觀的道長模聋。 經(jīng)常有香客問我,道長唠亚,這世上最難降的妖魔是什么链方? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮灶搜,結(jié)果婚禮上祟蚀,老公的妹妹穿的比我還像新娘。我一直安慰自己割卖,他們只是感情好前酿,可當我...
    茶點故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鹏溯,像睡著了一般罢维。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上剿涮,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天言津,我揣著相機與錄音,去河邊找鬼取试。 笑死悬槽,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的瞬浓。 我是一名探鬼主播初婆,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了磅叛?” 一聲冷哼從身側(cè)響起屑咳,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎弊琴,沒想到半個月后兆龙,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡敲董,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年紫皇,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片腋寨。...
    茶點故事閱讀 39,773評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡聪铺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出萄窜,到底是詐尸還是另有隱情铃剔,我是刑警寧澤,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布查刻,位于F島的核電站键兜,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏穗泵。R本人自食惡果不足惜蝶押,卻給世界環(huán)境...
    茶點故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望火欧。 院中可真熱鬧,春花似錦茎截、人聲如沸苇侵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽榆浓。三九已至,卻和暖如春撕攒,著一層夾襖步出監(jiān)牢的瞬間陡鹃,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工抖坪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留萍鲸,地道東北人。 一個月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓擦俐,卻偏偏與公主長得像脊阴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,689評論 2 354

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