Android:Handler 機制和原理?

一拧揽、Handler機制概述

Handler機制也可以說是消息機制慰照,Handler的運行時需要MessageQueue和Lopper的支持的灶挟。MessageQueue指消息隊列,用來存儲Message毒租,雖然名為隊列但其實是單向鏈表結(jié)構(gòu)稚铣;Lopper用于消息循環(huán),采用死循環(huán)的方式墅垮,如果有需要處理的消息就會處理惕医,否則就會阻塞等待。
Handler的工作原理如下:

1602490736882.jpg

Handler發(fā)送一個消息算色,調(diào)用MessageQueue的enqueueMessage方法將Message加入消息隊列抬伺;Lopper通過loop方法取出消息,最后轉(zhuǎn)發(fā)給Handler灾梦,最終在handlerMessage中處理峡钓。

二、源碼分析

2.1Handler構(gòu)造方法

Handler的構(gòu)造方法斥废,有很多椒楣,但是最后都是調(diào)用兩個方法,分別是Handler(Callback callback, boolean async)和Handler(Looper looper, Callback callback, boolean async)

    public Handler(Callback callback, boolean async) {
        //當設(shè)置為true時檢測匿名牡肉,本地或者成員類繼承Handler并且不是靜態(tài),這種類型的類會造成潛在的內(nèi)存泄漏
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }

        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

    public Handler(Looper looper, Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

2.2 創(chuàng)建以及存儲Looper

我們可以從Handler的構(gòu)造方法中發(fā)現(xiàn)Looper是創(chuàng)建的必要參數(shù)之一淆九,那么創(chuàng)建Handler之前那就要先創(chuàng)建Looper统锤,那么Looper是如何來的,我們可以看到炭庙,一種是將Looper實例化通過構(gòu)造穿進來饲窿,另一種是Looper.myLooper()直接獲取的

  • 自己創(chuàng)建一個Looper
    public static void prepare() {
        prepare(true);
    }

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

    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }

可以看到創(chuàng)建Looper有兩種方式,一種是Looper.prepare()焕蹄,一種是Looper. prepareMainLooper()逾雄,第二種是給應用主線程創(chuàng)建Looper的,所以我們自己創(chuàng)建使用第一種腻脏。我們繼續(xù)看會發(fā)現(xiàn)Looper創(chuàng)建后會存放在ThreadLocal中鸦泳,我們來看一下這個ThreadLocal是如何存儲Looper的,看源碼:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

看到這個是不是很熟悉的東西:key永品、value做鹰,key是當前線程,value就是當前線程對應的Looper鼎姐,創(chuàng)建的時候還會通過ThreadLocal.get() 獲取當前線程的Looper判斷是否已經(jīng)創(chuàng)建過Looper钾麸,所以可以知道Looper是跟線程綁定的更振,一個線程只會有一個Looper不可重復創(chuàng)建,ThreadLocal是來存儲這些Looper的饭尝,key就是Looper所在的線程肯腕。

  • Looper.myLooper()
    我們再來看看Looper.myLooper()的源碼
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

可以看到是從ThreadLocal獲取存儲的Looper,再看ThreadLocal.get()源碼:

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

就是返回當前線程所對應的Looper
所以我們可以知道钥平,在Handler創(chuàng)建前我們需要通過Looper.prepare()創(chuàng)建出當前線程自己的Looper乎芳,主線程則不需要創(chuàng)建,因為系統(tǒng)已經(jīng)創(chuàng)建了帖池,這個后面介紹奈惑。

2.3消息發(fā)送

那么Looper有了,Handler創(chuàng)建了睡汹,是如何發(fā)送消息的呢肴甸。我們可以發(fā)現(xiàn)發(fā)送消息有下面幾種方法:

  • 1.sendMessage(Message msg)
  • 2.sendEmptyMessage(int what)
  • 3.sendEmptyMessageDelayed(int what, long delayMillis)
  • 4.sendEmptyMessageAtTime(int what, long uptimeMillis)
  • 5.sendMessageDelayed(Message msg, long delayMillis)
  • 6.sendMessageAtTime(Message msg, long uptimeMillis)
  • 7.sendMessageAtFrontOfQueue(Message msg)
    這里1哆姻,2晦款,3伦籍,4撩笆,5最后調(diào)用的都是6颖榜。這里需要注意一個地方楞抡,就是系統(tǒng)是如何計算MessageDelayed的延遲時間的看铆,我們看一下源碼:
public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

可以看到是用SystemClock.uptimeMillis() + delayMillis法瑟,那么這個SystemClock.uptimeMillis()又是什么秽浇,源碼也有:

/**
 * Returns milliseconds since boot, not counting time spent in deep sleep.
 *
 * @return milliseconds of non-sleep uptime since boot.
 */
@CriticalNative
native public static long uptimeMillis();

大概意思返回自啟動以來的毫秒數(shù)浮庐,不計算深度睡眠所花費的時間。注意了這里不是是用的手機當前時間柬焕。

2.4消息加入消息隊列

消息發(fā)送了审残,這些消息是怎么管理的,那就是通過消息隊列MessageQueue斑举,把這些消息加入到消息隊列中去搅轿。這個消息隊列雖然名為隊列卻是單向鏈表。

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

接著看MessageQueue的enqueueMessage方法

    boolean enqueueMessage(Message msg, long when) {
        //handler為空拋異常
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        //消息已經(jīng)在隊列中拋異常
        if (msg.isInUse()) {
            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;
            //當前隊列的第一個msg
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                //當沒有頭消息或者加入消息的觸發(fā)事件為0或者早于頭消息的話就講加入消息放在隊頭
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // 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.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

至此富玷,入隊列完全結(jié)束璧坟。從這里可以看出,延遲低的消息會被加到延遲高的消息之前赎懦。

2.5消息處理

消息進入消息隊列后就需要Lopper對消息進行提取雀鹃,使用Looper.loop()方法不斷的從消息隊列中取出消息。這個方法是需要手動調(diào)用的铲敛,activity除外褐澎,因為activity中系統(tǒng)已經(jīng)幫我們調(diào)用了。這個我們后面再看伐蒋,下面我們看看Looper.loop()的部分源碼:

    public static void loop() {
        ......
        for (;;) {
            //拿到隊頭的消息
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            ......
            try {
                //msg.target.dispatchMessage(msg)其實就是Handler.dispatchMessage(msg);
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            ......
        }
    }

這里loop是一個無限循環(huán)的操作工三,queue.next()拿到消息然后發(fā)送給handler迁酸,那如果拿到是的是延遲消息怎么辦,我們來看看queue.next()源碼:

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

            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                //拿到當前系統(tǒng)自啟動來的未休眠時間
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                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());
                }
                if (msg != null) {
                    //當前系統(tǒng)自啟動來的未休眠時間小于消息出發(fā)時間是不會返回msg的
                    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.
                        //到了消息觸發(fā)事件俭正,返回msg
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

                // Process the quit message now that all pending messages have been handled.
                if (mQuitting) {
                    dispose();
                    return null;
                }

                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                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;
        }
    }

也就是只有到了消息觸發(fā)的時間調(diào)用next才會返回msg奸鬓。至此全部結(jié)束。這里面還有一個知識點掸读,就是IdleHandler串远,這個我們等下講。
三儿惫、現(xiàn)在來講講上面遺留的問題

  • 為什么Activity中使用Handler不需要我們創(chuàng)建Looper
  • 為什么Activity中使用Handler不需要我們自己調(diào)用Looper.loop()去循環(huán)獲取消息
  • Looper.loop()是個無限循環(huán)澡罚,且會阻塞線程,那么主線程為什么看起來沒有阻塞呢
    我們來看一下源碼:
    public static void main(String[] args) {
        ......
        Looper.prepareMainLooper();

        ......
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        ......
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

可以看到在ActivityThread中Looper.prepareMainLooper()創(chuàng)建的就是主線程中的Looper肾请,這里是系統(tǒng)自己創(chuàng)建好的留搔;隨后創(chuàng)建了MainThreadHandler,主線程中自己的Handler铛铁;再然后調(diào)用了Looper.loop()隔显。這里一、二兩個問題就知道答案了饵逐,第三個問題的關(guān)鍵就在這個MainThreadHandler括眠,系統(tǒng)就是使用這個MainThreadHandler去處理接收到的消息的,所以主線程看起來就沒有被阻塞倍权。我們來看一下MainThreadHandler是通過thread.getHandler()方法獲取的掷豺,我們看一下這個方法:

    final Handler getHandler() {
        return mH;
    }

我們再看一下mH怎么來的:

    final H mH = new H();

繼續(xù):

    class H extends Handler {
        ......
        public static final int GC_WHEN_IDLE            = 120;
        public static final int RELAUNCH_ACTIVITY = 160;
        ......

        String codeToString(int code) {
            if (DEBUG_MESSAGES) {
                switch (code) {
                    case BIND_APPLICATION: return "BIND_APPLICATION";
                    ......
                }
            }
            return Integer.toString(code);
        }
        public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
            switch (msg.what) {
                ......
                case GC_WHEN_IDLE:
                    scheduleGcIdler();
                    break;
                ......
                case RELAUNCH_ACTIVITY:
                    handleRelaunchActivityLocally((IBinder) msg.obj);
                    break;
            }
            Object obj = msg.obj;
            if (obj instanceof SomeArgs) {
                ((SomeArgs) obj).recycle();
            }
            if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + codeToString(msg.what));
        }
    }

主線程會使用這個mH來處理消息。
我們在這里也可以看到之前在Loop.loop()方法中看到的IdleHandler账锹,這里舉例用scheduleGcIdler()方法
四萌业、IdleHandler是什么
我們來跟一下scheduleGcIdler()源碼:

    void scheduleGcIdler() {
        if (!mGcIdlerScheduled) {
            mGcIdlerScheduled = true;
            Looper.myQueue().addIdleHandler(mGcIdler);
        }
        mH.removeMessages(H.GC_WHEN_IDLE);
    }

我們可以看到Looper.myQueue().addIdleHandler加入了一個mGcIdler,我們來看一下這個mGcIdler:

    final class GcIdler implements MessageQueue.IdleHandler {
        @Override
        public final boolean queueIdle() {
            doGcIfNeeded();
            return false;
        }
    }

我們發(fā)現(xiàn)IdleHandler原來是一個接口:

    public static interface IdleHandler {
        /**
         * Called when the message queue has run out of messages and will now
         * wait for more.  Return true to keep your idle handler active, false
         * to have it removed.  This may be called if there are still messages
         * pending in the queue, but they are all scheduled to be dispatched
         * after the current time.
         */
        boolean queueIdle();
    }

這個queueIdle()方法表示在消息隊列中消息用完后立即調(diào)用奸柬,如果返回true則會保持在系統(tǒng)空閑時不斷調(diào)用,false則只調(diào)用一次就被刪除婴程。如果消息隊列中有消息廓奕,就會被掛起等待消息處理。我們現(xiàn)在在來看看MessageQueue.next()方法:

    Message next() {
        ....
        for (;;) {
                ......
                if (msg != null) {
                    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.
                        ......
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
                ......
            }

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

可以在這里看到IdleHandler執(zhí)行過程確實如此档叔。

五桌粉、補充知識點

5.1HandlerThread是什么

我們通過觀察源碼可以發(fā)現(xiàn)HandlerThread繼承自Thread所以他擁有Thread能力,與之不同的是HandlerThread內(nèi)部自建創(chuàng)建了一個Handler衙四,提供了Looper铃肯。

5.2Message.obtain()

    public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }

從消息池中返回新的消息,且不會分配新對象传蹈,避免了消息對象的重復創(chuàng)建跟銷毀押逼。

5.3怎么維護消息池

消息池也是鏈表結(jié)構(gòu)步藕,在消息被消費后消息池會執(zhí)行回收操作,將該消息內(nèi)部數(shù)據(jù)清空然后添加消息鏈表最前面挑格。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末咙冗,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子漂彤,更是在濱河造成了極大的恐慌雾消,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挫望,死亡現(xiàn)場離奇詭異立润,居然都是意外死亡,警方通過查閱死者的電腦和手機媳板,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門桑腮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人拷肌,你說我怎么就攤上這事到旦。” “怎么了巨缘?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵添忘,是天一觀的道長。 經(jīng)常有香客問我若锁,道長搁骑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任又固,我火速辦了婚禮仲器,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘仰冠。我一直安慰自己乏冀,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布洋只。 她就那樣靜靜地躺著辆沦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪识虚。 梳的紋絲不亂的頭發(fā)上肢扯,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音担锤,去河邊找鬼蔚晨。 笑死,一個胖子當著我的面吹牛肛循,可吹牛的內(nèi)容都是我干的铭腕。 我是一名探鬼主播银择,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼谨履!你這毒婦竟也來了欢摄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤笋粟,失蹤者是張志新(化名)和其女友劉穎怀挠,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體害捕,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡绿淋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了尝盼。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吞滞。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖盾沫,靈堂內(nèi)的尸體忽然破棺而出裁赠,到底是詐尸還是另有隱情,我是刑警寧澤赴精,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布佩捞,位于F島的核電站,受9級特大地震影響蕾哟,放射性物質(zhì)發(fā)生泄漏一忱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一谭确、第九天 我趴在偏房一處隱蔽的房頂上張望帘营。 院中可真熱鬧,春花似錦逐哈、人聲如沸芬迄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽薯鼠。三九已至,卻和暖如春械蹋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背羞芍。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工哗戈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人荷科。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓唯咬,卻偏偏與公主長得像纱注,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子胆胰,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355