Android Jetpack架構(gòu)組件之WorkManager入門

——你可以失望,但不能絕望疹启。累的時候可以慢一點林说,千萬不要后退煎殷,你還沒有拼勁全力。怎么知道沒有奇跡腿箩。

前言

——最近抽空又學(xué)了一個Jetpack組件 —— WorkManager豪直,由于工作繁忙,要學(xué)的東西還有很多珠移,任務(wù)重弓乙,時間緊。雖然只學(xué)到了點皮毛钧惧,但是還是要花點時間做個總結(jié)暇韧。因為人們常說:學(xué)而不思則罔,思而不學(xué)則殆浓瞪。不思不學(xué)則網(wǎng)貸懈玻。所以要想致富,好的學(xué)習(xí)方法是必要的乾颁。也跟大家分享一下所學(xué)的知識涂乌。少走的點彎路艺栈。

一、簡介

(1)是什么

—— WorkManager是Android Jetpack 中管理后臺任務(wù)的組件湾盒。
—— 常見的使用場景:1.向后端服務(wù)發(fā)送日志或分析數(shù)據(jù) 2.定期將應(yīng)用數(shù)據(jù)與服務(wù)器同步

(2)有什么用

—— 使用 WorkManager API 可以輕松地調(diào)度后臺任務(wù)湿右。可延遲運(yùn)行(即不需要立即運(yùn)行)并且在應(yīng)用退出(進(jìn)程未關(guān)閉)或應(yīng)用重啟時能夠可靠運(yùn)行的任務(wù)罚勾。

(3)有什么優(yōu)點

  • 1.兼容JobScheduler與BroadcastReceiver 和 AlarmManager
  • 2.工作約束滿足多種情況
  • 3.可使用一次性或周期性執(zhí)行的任務(wù)
  • 4.監(jiān)控和管理計劃任務(wù)
  • 5.提供API將任務(wù)鏈接起來
  • 6.遵循低電耗模式等省電功能

二诅需、基本使用

(1)添加依賴

 implementation android.arch.work:work-runtime:1.0.1

(2)創(chuàng)建后臺任務(wù)(自定義類 繼承 Worker 并重寫doWork())

public static class MyWorker extends Worker {

    public MyWorker(@NonNull Context context, @NonNull WorkerParameters params) {
        super(context, params);
    }

    @Override
    public Result doWork() {
        return Result.success();//返回成功
//      return Result.failure();//返回失敗
//      return Result.retry();//重試
    }
}

(3)創(chuàng)建請求

// 對于一次性 WorkRequest,請使用 OneTimeWorkRequest荧库,對于周期性工作,請使用 PeriodicWorkRequest.
// 構(gòu)建一次性請求
// OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(MyWorker.class).build();
// 構(gòu)建周期性請求
// PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(MyWorker.class,1, TimeUnit.HOURS).build();

(4)執(zhí)行請求(如果沒有設(shè)置約束條件則會立即執(zhí)行)

WorkManager.getInstance().enqueue(request);

(5)取消和停止工作

WorkManager.getInstance().cancelWorkById(request.getId());

總結(jié):1.創(chuàng)建任務(wù)——2.配置請求——3.執(zhí)行請求

三赵刑、進(jìn)階

(1)進(jìn)階1:構(gòu)建約束條件:

Uri uri = Uri.parse("xxxxx");
Constraints constraints = new Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED) //指定需要在有網(wǎng)的情況下
        .setRequiresBatteryNotLow(true)//指定電量在可接受范圍內(nèi)運(yùn)行
        .setRequiresStorageNotLow(true)//指定在存儲量在可接受范圍內(nèi)運(yùn)行
        .addContentUriTrigger(uri,true)//當(dāng)Uri發(fā)生變化的時候運(yùn)行
        .setRequiresDeviceIdle(true)//當(dāng)設(shè)備處于空閑狀態(tài)時運(yùn)行
        .setRequiresCharging(true)//當(dāng)設(shè)備處于充電狀態(tài)時運(yùn)行
        .build();
//在請求
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(MyWorker.class)
                .setConstraints(constraints)//添加約束
                .build();
//當(dāng)滿足約束條件后才會執(zhí)行該任務(wù)
WorkManager.getInstance().enqueue(request);

(2)進(jìn)階2:延遲執(zhí)行

OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(MyWorker.class)
                .setInitialDelay(1,TimeUnit.HOURS)//延遲1小時執(zhí)行
                .build();

(3)進(jìn)階3:設(shè)置回退/重試的策略 當(dāng)doWork()返回 Result.retry()時啟用 指定重試間隔時長

OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(MyWorker.class)
                //第一個參數(shù):設(shè)置策略模式分衫。
                //第二個參數(shù):設(shè)置第一次重試時長
                //第三個參數(shù):設(shè)置時間單位
                .setBackoffCriteria(BackoffPolicy.LINEAR,
                        OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
                        TimeUnit.MILLISECONDS)
                .build();

(4)進(jìn)階4:傳入?yún)?shù)/標(biāo)記請求任務(wù)

Data imageData = new Data.Builder()
        .putString(DateKey, "開始執(zhí)行")
        .build();
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(MyWorker.class)
                //傳入?yún)?shù)
                .setInputData(imageData)
                .build();
@Override
public Result doWork() {
    //獲取傳入的參數(shù)
    String data = getInputData().getString(DateKey);
    LogUtils.e("data:"+data);
    //創(chuàng)建輸出結(jié)果
    Data outputData = new Data.Builder()
            .putString(DateKey,"已經(jīng)開始充電")
            .build();
    return Result.success(outputData);
}

(5)進(jìn)階5:標(biāo)記請求任務(wù)

OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(MyWorker.class)
                .addTag(TAG)
                .build();
                //取消使用特定標(biāo)記的所有任務(wù)
//        WorkManager.getInstance().cancelAllWorkByTag(TAG);
//會返回 LiveData 和具有該標(biāo)記的所有任務(wù)的狀態(tài)列表
//        WorkManager.getInstance().getWorkInfosByTagLiveData(TAG); 

(6)進(jìn)階6:監(jiān)聽工作狀態(tài)

WorkManager.getInstance().getWorkInfoByIdLiveData(request1.getId())
                .observe(this, new Observer<WorkInfo>() {
                    @Override
                    public void onChanged(@Nullable WorkInfo workInfo) {
                        if (workInfo != null && (workInfo.getState() == WorkInfo.State.SUCCEEDED)){
                            //獲取成功返回的結(jié)果
                            tvText.setText(workInfo.getOutputData().getString(DateKey));
                        }
                    }
                });

(7)進(jìn)階7:鏈接工作:用于指定多個關(guān)聯(lián)任務(wù)并定義這些任務(wù)的運(yùn)行順序(可以執(zhí)行多個任務(wù))

OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(MyWorker.class).build();
OneTimeWorkRequest request1 = new OneTimeWorkRequest.Builder(MyWorker.class).build();
OneTimeWorkRequest request2 = new OneTimeWorkRequest.Builder(MyWorker.class).build();
OneTimeWorkRequest request3 = new OneTimeWorkRequest.Builder(MyWorker.class).setInputMerger(OverwritingInputMerger.class).build();
OneTimeWorkRequest request4 = new OneTimeWorkRequest.Builder(MyWorker.class).build();
//        為了管理來自多個父級 OneTimeWorkRequest 的輸入,WorkManager 使用 InputMerger般此。
//        WorkManager 提供兩種不同類型的 InputMerger:
//        OverwritingInputMerger 會嘗試將所有輸入中的所有鍵添加到輸出中蚪战。如果發(fā)生沖突,它會覆蓋先前設(shè)置的鍵铐懊。
//        ArrayCreatingInputMerger 會嘗試合并輸入邀桑,并在必要時創(chuàng)建數(shù)組。

WorkManager.getInstance()
        //使用beginWith()可以并行執(zhí)行request科乎、request1壁畸、request2 
        .beginWith(Arrays.asList(request, request1, request2)).
        //使用then()可以按順序執(zhí)行任務(wù)
        .then(request3)//在執(zhí)行request3
        .then(request4)//在執(zhí)行request4
        .enqueue();

四、源碼分析

大體流程:
1.初始化時創(chuàng)建了WorkManager任務(wù)執(zhí)行器管理線程:里面創(chuàng)建了一個單線程池管理后臺任務(wù)與拿到主線程的handle執(zhí)行UI更新
2.在Worker封裝了一個線程茅茂,通過繼承方式把我們的后臺任務(wù)交給該線程
3.使用WorkRequest配置該任務(wù)線程的執(zhí)行條件
4.最終將WorkManager與WorkRequest綁定在一起捏萍。實際是把任務(wù)線程及配置信息交給WorkManager處理。
5.也就是調(diào)用了WorkManager任務(wù)執(zhí)行器來運(yùn)行線程與更新UI空闲。

@ 基于依賴implementation android.arch.work:work-runtime:1.0.1 源碼分析

(1)組件的初始化

WorkManager的初始化在ContentProvider中令杈,不需要手動添加。WorkManager是一個抽象類碴倾,它的大部分方法都是交給他的子類WorkManagerImpl實現(xiàn)的逗噩。

/**
* @Function workManager初始化
*/
public class WorkManagerInitializer extends ContentProvider {
    @Override
    public boolean onCreate() {
        // Initialize WorkManager with the default configuration.
        WorkManager.initialize(getContext(), new Configuration.Builder().build());
        return true;
    }
......
}

/**
* @Function WorkManager.initialize()最終使用單例模式創(chuàng)建WorkManagerImpl對象。
*/
public static void initialize(@NonNull Context context, @NonNull Configuration configuration) {
    WorkManagerImpl.initialize(context, configuration);
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static void initialize(@NonNull Context context, @NonNull Configuration configuration) {
    synchronized (sLock) {
        ...
        if (sDelegatedInstance == null) {
            context = context.getApplicationContext();
            if (sDefaultInstance == null) {
        //創(chuàng)建了WorkManagerImpl
                sDefaultInstance = new WorkManagerImpl(
                        context,
                        configuration,
            //創(chuàng)建了WorkManagerTaskExecutor
                        new WorkManagerTaskExecutor());
            }
            sDelegatedInstance = sDefaultInstance;
        }
    }
}

核心類:WorkManagerTaskExecutor :主要是管理后臺線程與UI線程的執(zhí)行跌榔。

//通過該類 我們可以執(zhí)行UI線程上的任務(wù)與后臺任務(wù)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class WorkManagerTaskExecutor implements TaskExecutor {

    //獲取達(dá)到UI線程的handler 
    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
    //創(chuàng)建一個Executor 綁定到UI線程上 再通過調(diào)用該Executor可以在UI線程上進(jìn)行操作
    private final Executor mMainThreadExecutor = new Executor() {
        @Override
        public void execute(@NonNull Runnable command) {
            postToMainThread(command);
        }
    };

    @Override
    public void postToMainThread(Runnable r) {
        mMainThreadHandler.post(r);
    }
    ...

    //創(chuàng)建了一個單線程池 管理workManager的后臺線程
    private final ExecutorService mBackgroundExecutor =
            Executors.newSingleThreadExecutor(mBackgroundThreadFactory);

    ... //省略部分調(diào)用方法
}

接下去我們看下 核心類 :WorkManagerImpl

   //按照執(zhí)行順序异雁,我們先看下它的構(gòu)造函數(shù) 做了哪些準(zhǔn)備工作。
   public WorkManagerImpl(
            @NonNull Context context,
            @NonNull Configuration configuration,
            @NonNull TaskExecutor workTaskExecutor,
            boolean useTestDatabase) {

        Context applicationContext = context.getApplicationContext();
        // 創(chuàng)建了一個room 數(shù)據(jù)庫用于保存 任務(wù)線程的配置信息
        WorkDatabase database = WorkDatabase.create(applicationContext, useTestDatabase);
        Logger.setLogger(new Logger.LogcatLogger(configuration.getMinimumLoggingLevel()));
        // 創(chuàng)建Scheduler根據(jù)返回一個List<Scheduler>, 
        //里面包含兩個Scheduler:GreedyScheduler僧须,SystemJobScheduler/SystemAlarmScheduler
        List<Scheduler> schedulers = createSchedulers(applicationContext);
        //建Processor片迅,Scheduler最后都調(diào)用Processor.startWork()去執(zhí)行Worker中的邏輯,也就是我們重寫的doWork()。
        Processor processor = new Processor(
                context,
                configuration,
                workTaskExecutor,
                database,
                schedulers);
        //啟動APP時檢查APP是之前否強(qiáng)制停止退出或有未執(zhí)行完的任務(wù)皆辽,是的話重啟WorkManager柑蛇,保證任務(wù)可以繼續(xù)執(zhí)行芥挣。
        internalInit(context, configuration, workTaskExecutor, database, schedulers, processor);
    }

由于源碼太多這里就不一一摘錄了,小弟不才耻台,文采有限空免。寫不出通俗易懂的句子。大家將就看看大體過程就好盆耽。初始化階段就介紹到這里蹋砚。

回顧一下初始化過程:

1.首先創(chuàng)建了WorkManagerImpl類,并持有WorkManagerTaskExecutor類摄杂,該類是后臺線程與UI線程的主要執(zhí)行者坝咐。

2.在WorkManagerImpl構(gòu)造方法中創(chuàng)建了數(shù)據(jù)庫保存任務(wù)線程的信息,主要用于App重啟時保證任務(wù)可以繼續(xù)執(zhí)行析恢。

3.又創(chuàng)建了Schedulers墨坚,用來滿足不同條件的情況下執(zhí)行特定的任務(wù)。

4.啟動APP時從數(shù)據(jù)庫中獲取任務(wù)列表判斷是否由未執(zhí)行的任務(wù)映挂,并啟動 泽篮。保證在滿足條件的情況下可以繼續(xù)執(zhí)行。

分析到了這里柑船。我們就回發(fā)現(xiàn)這里還缺少一個主要的組成部分帽撑。那就是我們的任務(wù)。如何把我們的后臺任務(wù)交給workManager處理呢鞍时。這就是我們需要收到操作的部分亏拉。也就是我們使用WorkManger的過程。

(2)創(chuàng)建后臺任務(wù):Worker

//這是一個抽象類逆巍,所以需要自定義一個類來繼承該類并重寫 doWork()方法來編寫后臺任務(wù)
public abstract class Worker extends ListenableWorker {
    ...
    //從該方法中可以看出dowork()在一個線程中執(zhí)行专筷。getBackgroundExecutor()則是調(diào)用了單線程池來管理該線程。
    @Override
    public final @NonNull ListenableFuture<Result> startWork() {
        mFuture = SettableFuture.create();
        getBackgroundExecutor().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result result = doWork();
                    mFuture.set(result);
                } catch (Throwable throwable) {
                    mFuture.setException(throwable);
                }

            }
        });
        return mFuture;
    }
}

(3)配置后臺任務(wù)的執(zhí)行條件:WorkRequest

——WorkRequest配置后臺任務(wù)的執(zhí)行條件蒸苇,該類是一個抽象類磷蛹,有WorkManager有兩種具體的實現(xiàn)OneTimeWorkRequest/PeriodicWorkRequest。

new OneTimeWorkRequest.Builder(MyWorker.class)
                .setConstraints(constraints)//添加約束
                .setInitialDelay(1,TimeUnit.HOURS)//進(jìn)階2:延遲執(zhí)行
                .setBackoffCriteria(BackoffPolicy.LINEAR,
                        OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
                        TimeUnit.MILLISECONDS)//進(jìn)階3:退避政策:當(dāng)doWork()返回 Result.retry()時 啟用
                .setInputData(imageData)//進(jìn)階4:傳入?yún)?shù)
                .addTag(TAG)//進(jìn)階4:標(biāo)記請求任務(wù)
                .build();

//創(chuàng)建了配置信息類WorkSpec 溪烤,將執(zhí)行條件和參數(shù)都保存到WorkSpec中
public abstract static class Builder<B extends Builder, W extends WorkRequest> {
   ...

   Builder(@NonNull Class<? extends ListenableWorker> workerClass) {
         mId = UUID.randomUUID();
         mWorkSpec = new WorkSpec(mId.toString(), workerClass.getName());
         addTag(workerClass.getName());
   }

   public final @NonNull B setBackoffCriteria(
            @NonNull BackoffPolicy backoffPolicy,
            long backoffDelay,
            @NonNull TimeUnit timeUnit) {
        mBackoffCriteriaSet = true;
        mWorkSpec.backoffPolicy = backoffPolicy;
        mWorkSpec.setBackoffDelayDuration(timeUnit.toMillis(backoffDelay));
        return getThis();
    }
    ...
    public final @NonNull B setConstraints(@NonNull Constraints constraints) {
          mWorkSpec.constraints = constraints;
          return getThis();
    }
    ...
}

(4)執(zhí)行任務(wù)

    // WorkManager.getInstance().enqueue(request1)

    @Override
    @NonNull
    public Operation enqueue(
            @NonNull List<? extends WorkRequest> workRequests) {
        ...
        return new WorkContinuationImpl(this, workRequests).enqueue();
    }

    @Override
    public @NonNull Operation enqueue() {
        if (!mEnqueued) {
            //調(diào)用單線程池執(zhí)行EnqueueRunnable   后面詳細(xì)分析下EnqueueRunnable
            EnqueueRunnable runnable = new EnqueueRunnable(this);
            mWorkManagerImpl.getWorkTaskExecutor().executeOnBackgroundThread(runnable);
            mOperation = runnable.getOperation();
        } else {
            Logger.get().warning(TAG,
                    String.format("Already enqueued work ids (%s)", TextUtils.join(", ", mIds)));
        }
        return mOperation;
    }
 //該線程會執(zhí)行run()方法 并執(zhí)行兩個重要的方法addToDatabase(), scheduleWorkInBackground();
 public class EnqueueRunnable implements Runnable {
     ...

    @Override
    public void run() {
        try {
            if (mWorkContinuation.hasCycles()) {
                throw new IllegalStateException(
                        String.format("WorkContinuation has cycles (%s)", mWorkContinuation));
            }
            //將后臺任務(wù)及配置信息存到數(shù)據(jù)庫 并返回是否需要執(zhí)行任務(wù)
            boolean needsScheduling = addToDatabase();
            if (needsScheduling) {
                // Enable RescheduleReceiver, only when there are Worker's that need scheduling.
                final Context context =
                        mWorkContinuation.getWorkManagerImpl().getApplicationContext();
                PackageManagerHelper.setComponentEnabled(context, RescheduleReceiver.class, true);
                scheduleWorkInBackground();
            }
            mOperation.setState(Operation.SUCCESS);
        } catch (Throwable exception) {
            mOperation.setState(new Operation.State.FAILURE(exception));
        }

   }

    //最后會啟用 初始化時創(chuàng)建的GreedyScheduler味咳,SystemJobScheduler/SystemAlarmScheduler等調(diào)度類來執(zhí)行工作.
    @VisibleForTesting
    public void scheduleWorkInBackground() {
        WorkManagerImpl workManager = mWorkContinuation.getWorkManagerImpl();
        Schedulers.schedule(
                workManager.getConfiguration(),
                workManager.getWorkDatabase(),
                workManager.getSchedulers());
    }

更詳細(xì)的代碼就不貼了,大家要是腦補(bǔ)不了檬嘀。在按流程仔細(xì)看一遍源碼會了解的更深槽驶。

本想畫個圖加深一下印象,結(jié)果發(fā)現(xiàn)是個手殘黨 鸳兽,對不住大家 掂铐。

五、內(nèi)容推薦

若您發(fā)現(xiàn)文章中存在錯誤或不足的地方,希望您能指出全陨!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末爆班,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子辱姨,更是在濱河造成了極大的恐慌柿菩,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雨涛,死亡現(xiàn)場離奇詭異枢舶,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)替久,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門凉泄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蚯根,你說我怎么就攤上這事后众。” “怎么了稼锅?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長僚纷。 經(jīng)常有香客問我矩距,道長,這世上最難降的妖魔是什么怖竭? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任锥债,我火速辦了婚禮,結(jié)果婚禮上痊臭,老公的妹妹穿的比我還像新娘哮肚。我一直安慰自己,他們只是感情好广匙,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布允趟。 她就那樣靜靜地躺著,像睡著了一般鸦致。 火紅的嫁衣襯著肌膚如雪潮剪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天分唾,我揣著相機(jī)與錄音抗碰,去河邊找鬼。 笑死绽乔,一個胖子當(dāng)著我的面吹牛弧蝇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼看疗,長吁一口氣:“原來是場噩夢啊……” “哼沙峻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鹃觉,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤专酗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后盗扇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體祷肯,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年疗隶,在試婚紗的時候發(fā)現(xiàn)自己被綠了佑笋。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡斑鼻,死狀恐怖蒋纬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情坚弱,我是刑警寧澤蜀备,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站荒叶,受9級特大地震影響碾阁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜些楣,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一脂凶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧愁茁,春花似錦蚕钦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至促煮,卻和暖如春食听,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背污茵。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工樱报, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人泞当。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓迹蛤,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子盗飒,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

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