AsyncTask到底是什么

最近找實習面試中經(jīng)常會被問到關于AsyncTask的一些內(nèi)部機制的問題,之前也早有學習,但是還不夠系統(tǒng),沒有形成一個體系,現(xiàn)在我們來完整的徹底的梳理一下吧,掃除一些知識盲點。
相信大多數(shù)Android開發(fā)者都接觸過AsyncTask這個類,AsyncTask主要是用來處理一些后臺的任務,說到底就是一個異步處理工具類,隨著Android開發(fā)配套的開源項目越來越完善越來越好,比如網(wǎng)絡請求時有了Volley配套OKHttp等非常好用的庫,但是在一些例如和UI做交互的情況下還是需要AsyncTask來幫忙的。

那么AsyncTask到底是什么呢,閱讀源碼我們可以發(fā)現(xiàn),AsyncTask就是一個Handler和線程池的封裝,線程池用來異步處理后臺任務,handler用來發(fā)送消息進行UI方面的交互。好,既然說到了源碼,那么我們先來看看AsyncTask的源碼。(基于API 23)

先看看AsyncTask的構造方法:

private final WorkerRunnable<Params, Result> mWorker;
private final FutureTask<Result> mFuture;
/**
* 創(chuàng)建一個新的異步任務,這個構造方法只能在主線程調(diào)用。
*/
public AsyncTask() {
    mWorker = new WorkerRunnable<Params, Result>() {
        public Result call() throws Exception {
            mTaskInvoked.set(true);

            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
            //noinspection unchecked
            Result result = doInBackground(mParams);
            Binder.flushPendingCommands();
            return postResult(result);
        }
    };
    mFuture = new FutureTask<Result>(mWorker) {
        @Override
        protected void done() {
            try {
                postResultIfNotInvoked(get());
            } catch (InterruptedException e) {
                android.util.Log.w(LOG_TAG, e);
            } catch (ExecutionException e) {
                throw new RuntimeException("An error occurred while executing doInBackground()",
                        e.getCause());
            } catch (CancellationException e) {
                postResultIfNotInvoked(null);
            }
        }
    };
}

首先new一個mWorker,這是一個WorkerRunnable<Params,Result>,抽象類,實現(xiàn)了Callable的接口,本質(zhì)上就是一個Callable。mFuture是一個FutureTask對象。
可以看到在WorkerRunnable中最主要是做了三步,將代表任務是否調(diào)用的原子boolean標記設為true,將這個線程的優(yōu)先級設為后臺線程優(yōu)先級,并且丟出doInBackground處理的結(jié)果。
這個結(jié)果被丟到了postResult方法中,我們可以來看看postResult方法部分的代碼。

private Result postResult(Result result) {
    @SuppressWarnings("unchecked")
    Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
            new AsyncTaskResult<Result>(this, result));
    message.sendToTarget();
    return result;
}

可以看到其實這個方法并沒有對丟過來的result做什么事,丟進來的result依然原封不動的丟了出去,只是在這個過程中捎帶實例化了一下AsyncTaskResult,將丟進來的這個result以及當前
這個AsyncTask一起又拋給了新初始化的AsyncTaskResult, 并且將向handler發(fā)送了一個Message。注意這里message的 what是MESSAGE_POST_RESULT, 這里是有用到的,我們后面再講。

好,我們繼續(xù)往下走,看看result丟進去的AsyncTaskResult。

@SuppressWarnings({"RawUseOfParameterizedType"})
private static class AsyncTaskResult<Data> {
    final AsyncTask mTask;
    final Data[] mData;

    AsyncTaskResult(AsyncTask task, Data... data) {
        mTask = task;
        mData = data;
    }
}

可以看到AsyncTaskResult只是一個static的內(nèi)部類,只是起了一個保存當前的AsyncTask對象和后臺處理的result data的作用。我們再繼續(xù)往下看看這里保存的result和task在哪里使用到了。之前
已經(jīng)提到了AsyncTask本質(zhì)上就是一個線程池和Handler的封裝,現(xiàn)在我們都已經(jīng)獲得了后臺處理的結(jié)果了,消息也發(fā)送了,那么我們跟著result來看看這個重要組成部分吧。

private static class InternalHandler extends Handler {
    public InternalHandler() {
        super(Looper.getMainLooper());
    }

    @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
    @Override
    public void handleMessage(Message msg) {
        AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
        switch (msg.what) {
            case MESSAGE_POST_RESULT:
                // There is only one result
                result.mTask.finish(result.mData[0]);
                break;
            case MESSAGE_POST_PROGRESS:
                result.mTask.onProgressUpdate(result.mData);
                break;
        }
    }
}

這里就非常清楚了,之前也講過在post中發(fā)送的消息的what內(nèi)容是MESSAGE_POST_RESULT, 我們看看handler中,result也是消息中傳遞過來的AsyncTaskResult,里面保存了當前的task和處理的結(jié)果,
進入我們預想的case,通過對應的task,處理AsyncTaskResult中的data, 這里是走到了AsyncTask的finish方法。走到這兒可能有點遠了,但是不怕,我們繼續(xù)往下看,其實這份代碼寫的還是很有條理的。

 private void finish(Result result) {
    if (isCancelled()) {
        onCancelled(result);
    } else {
        onPostExecute(result);
    }
    mStatus = Status.FINISHED;
}

注意,這里的finish方法是一個private,只有當前的task能調(diào)用,一定程度上保證了安全性。它通過對isCancelled的判斷可以進入不同的結(jié)果處理流程,實際上onCancelled和onPostExecute就是我們平常使用AsyncTask經(jīng)常使用的兩個方法,到這一步就沒什么好說了,task攜帶著之前一步步傳過來的doInBackground的結(jié)果,來到了我們最熟悉的幾個方法了。只是我們需要留意一下Status這個環(huán)節(jié)。
mStatus是一個volatile的Status對象,而Status是一個枚舉類,只包括了

  1. PENDING, 等待狀態(tài),表明當前這個task還沒有執(zhí)行。
  2. RUNNING, 運行狀態(tài),當前task正在執(zhí)行中。
  3. FINISHED, 結(jié)束狀態(tài),當前task已經(jīng)運行結(jié)束了。

而mStatus在這個task初始時就直接設為了PENDING, 并且只有在finish方法中改變?yōu)镕INISHED, 這已經(jīng)是最后的環(huán)節(jié)了。那么是在哪里變?yōu)镽UNNING呢,我們現(xiàn)在就來看看整個AsyncTask最核心的部分,線程池這一塊。

看完了從task構造方法一路跟隨到最后finish,我們現(xiàn)在再跟著我們平時的用法來一起學習線程池一部分。事實上我們在使用時,每次初始化后,會使用execute這個方法來讓task跑起來。我們看看這里是發(fā)生了什么事。

@MainThread
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
    return executeOnExecutor(sDefaultExecutor, params);
}

代碼很簡單,主要就是攜帶參數(shù),在主線程中啟動起來。注意,這里只能是在UI線程也就是主線程中完成這一步,這也是AsyncTask一個比較大的軟肋。
事實上task是通過一個隊列完成的。別急,好飯不怕晚,我們先看看這里return 的executeOnExecutor方法干了什么。

@MainThread
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
            Params... params) {
    if (mStatus != Status.PENDING) {
        switch (mStatus) {
            case RUNNING:
                throw new IllegalStateException("Cannot execute task:"
                            + " the task is already running.");
            case FINISHED:
                throw new IllegalStateException("Cannot execute task:"
                        + " the task has already been executed "
                        + "(a task can be executed only once)");
        }
    }

    mStatus = Status.RUNNING;

    onPreExecute();

    mWorker.mParams = params;
    exec.execute(mFuture);

    return this;
}

這里是不是就和之前一直到finish的分析連接上了呢。這里執(zhí)行task時,講道理的話到這一步時當前的task一定還是PENDING狀態(tài),所以這里會先檢查mStatus,如果不是PENDING就會拋出異常,否則就正常執(zhí)行,把mStatus改為RUNNING狀態(tài)。然后就到了我們也非常熟悉的方法了,onPreExecute,這里一般做一些后臺任務之前的事情。

終于到了重點了,完成了準備工作后,注意exec.execute(mFuture)這一步,這個exec是什么? 一個Executor,Executor只是一個接口,定義了一個execute的方法,這個exec是在上一步execute時傳入的全局sDefaultExecutor。sDefaultExecutor是一個默認線程池。

public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;

可以看到,sDefaultExecutor實際上是一個SerialExecutor. 但是這里為什么要分開寫呢。可以發(fā)現(xiàn)SERIAL_EXECUTOR是一個final對象,也就是說sDefaultExecutor默認是使用系統(tǒng)初始化好的SerialExecutor,但是我們也可以手動給這個Task設置一個Executor。

/** @hide */
public static void setDefaultExecutor(Executor exec) {
    sDefaultExecutor = exec;
}

這樣就清楚了,其實留給開發(fā)者的余地還是很大的。在這里還是只研究默認的Executor吧。一樣,我們看看源碼。

private static class SerialExecutor implements Executor {
    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    Runnable mActive;

    public synchronized void execute(final Runnable r) {
        mTasks.offer(new Runnable() {
            public void run() {
                try {
                    r.run();
                } finally {
                    scheduleNext();
                }
            }
        });
        if (mActive == null) {
            scheduleNext();
        }
    }

    protected synchronized void scheduleNext() {
        if ((mActive = mTasks.poll()) != null) {
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}

哈,可以看到這里的excute方法就是我們前面分析每次提到的excute,到最后就都到了這里。首先我們可以看到在SerialExecutor中維護了一個ArrayDeque, mTasks。還記得之前在executeOnExecutor中看到的exec.execute(mFuture)方法嗎?mFuture是一個FutureTask,實現(xiàn)了Future和Runnable,這里就將丟過來的mFutuer封裝成了一個Runnable對象,然后在把這個Runnable添加到隊列。

接著包含一個scheduleNext() 方法。scheduleNext是先從mTasks隊列中取出隊首的一個Runnable任務叫做mActive,如果這個Runnable不為空,就將其添加到真正的線程池THREAD_POOL_EXECUTOR之中執(zhí)行。

需要指出的是,由于第一次execute時,在將Runnable加進隊列后,mActive初始化為null,所以會默認走進scheduleNext,這樣也保證了一開始的自動啟動。
以后的任務就是在try{}finally{}中可以看到,每個Runnable運行完后就進入finally執(zhí)行隊列中的下一個任務。

我們已經(jīng)發(fā)現(xiàn)了這里的每個Runnable都是在THREAD_POOL_EXECUTOR中完成的。這是什么?一個ThreadPoolExecutor. 我們先看看一個ThreadPoolExecutor的構造方法:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}

我們看看依次需要的參數(shù):

  1. corePoolSize 核心線程池大小,也就是線程池中可以維持的線程數(shù)目,即使這些線城是空閑的,也不會終止(除非設置了allowCoreThreadTimeOut),因此可以理解為常駐線程池的線程數(shù)目。
  2. maximumPoolSize 線程池中允許的最大線程數(shù)目。因為一般來說線程數(shù)目越多,調(diào)度所用的花銷越大,所以需要設置一個數(shù)目上限。
  3. keepAliveTime 當線程數(shù)目大于核心線程數(shù)目時,如果超過這個keepAliveTime時間,那么空閑的線程會被終止。
  4. unit keepAliveTime的時間單位。
  5. workQueue 一個保存尚未執(zhí)行的線程的隊列。這個隊列只保存由execute方法提交的Runnable任務。
  6. threadFactory 用來構造線程池的工廠。

我們再看看ThreadPoolExecutor的execute方法。

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
      //獲取ctl的int值。這個int值保存了線程池中任務數(shù)目和線程池的狀態(tài)等信息
    int c = ctl.get();
    // 當線程池中的任務數(shù)量 < 核心線程數(shù)目時,通過addWorker(command, true)新建一個線程,并將任務(command)添加到該線程中,然后啟動該線程來執(zhí)行任務。
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

我們可以看一下這個線程池調(diào)度的大致流程:

  1. 如果線程池中的線程數(shù)目小于corePoolSize,那么就新啟動一個線程,并且將這個Runnable作為這個新線程的第一個任務添加到線程中進行執(zhí)行
  2. 如果線程池中的線程數(shù)目大于等于corePoolSize,就將任務添加到workQueue隊列中等待。這種情況下,會兩次確認線程池的狀態(tài),如果第2次讀到的線程池狀態(tài)和第1次讀到的線程池狀態(tài)不同,就從隊列中刪除該任務。
  3. 如果這個隊列已經(jīng)滿了,就在線程池中新建一個線程,并將該任務添加到線程中進行執(zhí)行。如果執(zhí)行失敗,就通過reject()拒絕該任務。

總之可以發(fā)現(xiàn),線程池ThreadPoolExecutor通過workQueue來管理線程和任務,每個線程在啟動后,會執(zhí)行線程池中的任務;當一個任務執(zhí)行結(jié)束后,它會從線程池workQueue中取出任務來繼續(xù)執(zhí)行。workQueue是管理線程池任務的隊列,當添加到線程池中的任務超過線程池的最大線程數(shù)目時,這個任務就會進入阻塞隊列進行等待。

好,我們現(xiàn)在來看看android中的THREAD_POOL_EXECUTOR是怎么走的。

public static final Executor THREAD_POOL_EXECUTOR
        = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
                TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

我們看看傳進去的參數(shù):

//cpu的數(shù)量,Runtime獲取
 private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); 
 //核心線程數(shù)目
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
//最大線程數(shù)目
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE = 1;
//工作隊列,一個阻塞隊列,用來保存任務。
private static final BlockingQueue<Runnable> sPoolWorkQueue =
        new LinkedBlockingQueue<Runnable>(128);

//創(chuàng)建新線程的一個工廠方法
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
    private final AtomicInteger mCount = new AtomicInteger(1);

    public Thread newThread(Runnable r) {
        return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
    }
};

可以看到,在構建THREAD_POOL_EXECUTOR時是基于android平臺的特點來的。核心線程數(shù)是設為了CPU_COUNT+1。
但是實際上在之前的android API版本中,這些值是直接設為了固定的值。

/*4.3 版本的AsyncTask源碼 */
private static final int CORE_POOL_SIZE = 5;
private static final int MAXIMUM_POOL_SIZE = 128;
private static final int KEEP_ALIVE = 1;

private static final ThreadFactory sThreadFactory = new ThreadFactory() {
    private final AtomicInteger mCount = new AtomicInteger(1);

    public Thread More ...newThread(Runnable r) {
        return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
    }
};

private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<Runnable>(10);

從4.4版本改成了目前的配置。另外可以看到,4.4之前的sPoolWorkQueue的大小也只有10。這些配置的更改給AsyncTask的動態(tài)調(diào)度還是帶來了優(yōu)勢的。比如在4.3中,核心線程數(shù)目只有5,因此如果開了5個線程后,再繼續(xù)開的話就只能等待了。

這是對線程池的初始化和調(diào)度分析了,之后就是線程的執(zhí)行,并且接著我們最初的流程,最后走到task的finish方法,需要我們在AsyncTask實現(xiàn)時完成onPostExecute就可以了。關于Future部分的分析,我們以后再講。

實際上,面試時面試官會經(jīng)常問一個問題就是AsyncTask是可以并行的嗎?

經(jīng)過我們上面的分析,已經(jīng)可以確定AsyncTask內(nèi)部有一個線程池來進行線程的調(diào)度管理以及執(zhí)行。那么AsyncTask是可以并行執(zhí)行的嗎?先賣個關子,看看2.3.7的AsyncTask部分的代碼:

public final AsyncTask<Params, Progress, Result> More ...execute(Params... params) {
    if (mStatus != Status.PENDING) {
        switch (mStatus) {
            case RUNNING:
                throw new IllegalStateException("Cannot execute task:"
                            + " the task is already running.");
            case FINISHED:
                throw new IllegalStateException("Cannot execute task:"
                            + " the task has already been executed "
                            + "(a task can be executed only once)");
        }
    }

    mStatus = Status.RUNNING;

    onPreExecute();

    Worker.mParams = params;
    sExecutor.execute(mFuture);

    return this;
}

看到?jīng)],在2.3.7上execute(Params... params)這個方法和6.0上有什么區(qū)別?
我們在之前已經(jīng)分析過了,execute(params)是走到了 return executeOnExecutor(sDefaultExecutor, params);
而sDefaultExecutor是一個volatile的對象。

private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;

我們可以得出結(jié)論,在6.0上不是并行的,在2.3上是并行的。實際上閱讀多個版本的源碼可以發(fā)現(xiàn),這一變化是在3.0上做到的。因此我們可以理解為從3.0之后就變成串行了,1.5-2.3是并行的。最初也是串行的。
事實上,也可以自己寫demo測試一下,看看每個task的執(zhí)行的時間是不是同時開始。

那么面試官又會問了,為什么在目前的版本上要將AsyncTask設計成串行呢?
揣摩設計者的意圖其實真的是一個很有意思的事情啊- -23333

我們已經(jīng)提到了AsyncTask是線程池和Handler的一個封裝好的工具,其實做到的功能在handler message和loop也能做到,那么為什么要設計一個這樣的工具呢,主要還是方便開發(fā)者的使用,尤其是初級開發(fā)者。
而可能隨著android版本的迭代開發(fā),發(fā)現(xiàn)有開發(fā)者很少在doInBackground中做線程安全的考慮,既然很少有人會考慮最資源的并發(fā)訪問的安全性,那么干脆就不開放這個功能,保證每個線程的串行執(zhí)行。這樣就是皆大歡喜。

好了,對AsyncTask的分析就到這兒了,大家應該清楚整個的流程以及其特點了。但是可能大家也看到一些大牛說了,并不推薦使用默認的AsyncTask,因為實在有一些不能忽視的缺點啦- -

首先,默認的AsyncTask的線程池中的核心線程數(shù)是有限的,不管是和CPU數(shù)目有關還是以前的固定的5, 還是有一個數(shù)量的限制,因此不適合大量的后臺任務處理,例如瀑布流圖片的加載等。
其次,AsyncTask類必須在主線程初始化,必須在主線程創(chuàng)建,因為return executeOnExecutor(sDefaultExecutor, params)這里也只能在UI線程走。
最后,我們也講到了,AsyncTask在3.0后改成了串行的,因此想真正做一些并行的后臺任務,就不太適合了。

總之,大家想要更好的使用AsyncTask,最好自己修改一下再使用啦。目前默認的AsyncTask確實還不是最佳狀態(tài)。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

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