Android Jetpack架构组件(六)— WorkManager

一、WorkManager简介

WorkManager是适合用于持久性工作的推荐解决方案。
持久性的工作:如果工作始终要通过应用重启和系统重新启动来调度,便是持久性的工作。
由于大多数后台处理操作都是通过持久性工作完成的,因此 WorkManager 是适用于后台处理操作的主要推荐 API。

持久性工作的类型:

WorkManager 可处理三种类型的持久性工作:

  • 立即执行:必须立即开始且很快就完成的任务,可以加急。
  • 长时间运行:运行时间可能较长(有可能超过 10 分钟)的任务。
  • 可延期执行:延期开始并且可以定期运行的预定任务。


    持久性工作类型
类型 周期 使用方式
立即 一次性 OneTimeWorkRequest 和 Worker。如需处理加急工作,请对 OneTimeWorkRequest 调用 setExpedited()。
长期运行 一次性或定期 任意 WorkRequest 或 Worker。在工作器中调用 setForeground() 来处理通知。
可延期 一次性或定期 PeriodicWorkRequest 和 Worker。
WorkManager 适用于:
  • 主要用用于后台运行的操作;
  • 用于延迟运行并且再应用退出或设备重启必须能够可靠运行的任务。

Android为后台任务提供了多种解决方案,如JobScheduler,Loader,Service等。如果这些API没有被适当地使用,可能会消耗大量的电量。

Service也是用于后台运行,为什么不用Service?
原因:Google自Oreo(API 26) 开始,对后台service服务做了一些个限制,如在不被允许创建后台服务的情况下,调用了startService()方法,会导致IllegalStateException异常。因此如果要继续使用service就必须通过Context类得静态方法startForegroundService()来启动前台服务,系统也创建了服务,这就导致了应用得在5秒钟之内调用该服务的 startForeground() 方法使其位于前台被用户发现,要是超过了5s那App就抛ANR(Application Not Response)。

1、WorkManager兼容性

WorkManager最低能兼容API Level 14,几乎兼容100%Android设备。
WorkManager能依据设备的情况,选择不同的执行方案。在API Level 23+,通过JobScheduler来完成任务,而在API Level 23以下的设备中,通过AlarmManager和Broadcast Receivers组合完成任务。但无论采用哪种方案,任务最终都是交由Executor来完成。

2、WorkManager特点

1.针对不需要及时完成的任务

比如,发送应用程序日志,同步应用程序数据,备份用户数据等。站在业务的角度,这些任务都不需要立即完成,如果我们自己来管理这些任务,逻辑可能会非常复杂,若API使用不恰当,可能会消耗大量电量。

2.保证任务一定会被执行

WorkManager能保证任务一定会被执行,即使你的应用程序当前不在运行中,哪怕你的设备重启,任务仍然会在适当的时候被执行。这是因为WorkManager有自己的数据库,关于任务的所有信息和数据都保存在这个数据库中,因此,只要你的任务交给了WorkManager,哪怕你的应用程序彻底退出,或者设备重新启动,WorkManager依然能够保证完成你交给的任务。

注意:WorkManager不是一种新的工作线程,它的出现不是为了替代其它类型的工作线程。工作线程通常立即运行,并在执行完成后给到用户反馈。而WorkManager不是即时的,它不能保证任务能立即得到执行。

二、WorkManager使用

1、首先三个重要的类

  1. Worker
    我们要执行的具体任务。需要继承Worker,重写doWork方法,然后在里面写具体的逻辑。
  2. WorkRequest
    上面的Worker是定义了我们要在后台的任务,而这个类是对Worker的包装。
    下面两个都是继承了WorkRequest:
    OneTimeWorkRequest: 只执行一次的任务
    PeriodicWorkRequest: 重复执行的任务(重复间隔大于15分钟)
  3. WorkManager
    是对WorkRequest的管理类。

2、在app的build.gradle中添加依赖

dependencies {
    def work_version = "2.7.1"

    // (Java only)
    implementation "androidx.work:work-runtime:$work_version"

    // Kotlin + coroutines
    implementation "androidx.work:work-runtime-ktx:$work_version"

    // optional - RxJava2 support
    implementation "androidx.work:work-rxjava2:$work_version"

    // optional - GCMNetworkManager support
    implementation "androidx.work:work-gcm:$work_version"

    // optional - Test helpers
    androidTestImplementation "androidx.work:work-testing:$work_version"

    // optional - Multiprocess support
    implementation "androidx.work:work-multiprocess:$work_version"
}

3、创建自定义Worker任务

class MyWorker extends Worker {

    private WorkerParameters workerParameters;

    public MyWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
        workerParameters = workerParams;
    }

    @NonNull
    @Override
    public Result doWork() {
        Log.e("MyWorker", "doWork()");
        //从外面传进来的值
        String value = workerParameters.getInputData().getString("key");
        //处理数据后创建data
        Data data = new Data.Builder().putString("key", value).build();
        return Result.success(data);
    }
}

doWork()方法有三种类型的返回值:

  • 执行成功返回Result.success(),可携带参数
  • 执行失败返回Result.failure(),可携带参数
  • 需要重新执行返回Result.retry()

4、使用WorkRequest配置任务并把请求添加到请求队列

通过WorkRequest配置我们的任务何时运行以及如何运行。

      //1、创建传递给Work的参数
      Data sendData = new Data.Builder().putString("key", "来自Activity的数据").build();
      //2、初始化请求对象request,并添加自定义Worker
      OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder(MyWorker.class).setInputData(sendData).build();
      //3、添加回调更新
      WorkManager.getInstance(activity).getWorkInfoByIdLiveData(oneTimeWorkRequest.getId()).observe(activity, new Observer<WorkInfo>() {
         @Override
         public void onChanged(WorkInfo workInfo) {
            //注意这个方法会回调多次,要加这个判断
            if(workInfo.getState().isFinished()){
               //执行完成的处理,接受worker返回的结果
               //这里的key与MyWorker的key需要一致
               String result = workInfo.getOutputData().getString("key");
            }
         }
      });


      //4、添加到任务队列
      WorkManager.getInstance(activity).enqueue(oneTimeWorkRequest);

注意:Data只能用于传递一些小的基本类型数据,且数据最大不能超过10kb。

三、WorkManager其他使用方式

1、WorkRequest两个子类
  • OneTimeWorkRequest:执行一次性任务
  • PeriodicWorkRequest:执行周期性任务
2、WorkRequest可设置任务触发条件

以下皆使用OneTimeWorkRequest,OneTimeWorkRequest和PeriodicWorkRequest使用方式相同。

      //1、创建条件:在设备处于充电,网络已连接,且电池电量充足的状态下,才触发我们设置的任务。
      Constraints constraints = new Constraints.Builder()
              //在设备处于充电
              .setRequiresCharging(true)
              //网络已连接
              .setRequiredNetworkType(NetworkType.CONNECTED)
              //电池电量充足的状态下
              .setRequiresBatteryNotLow(true)
              //充电中
              .setRequiresCharging(true)
              //cpu空闲时
              .setRequiresDeviceIdle(true)
              //可用存储是否不低于最小值
              .setRequiresStorageNotLow(true)
              .build();

      //2、设置条件
      OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder(MyWorker.class)
              .setInputData(sendData)
              .setConstraints(constraints)
              .build();
3、设置延迟执行任务

假设你没有设置触发条件,或者当你设置的触发条件符合系统的执行要求,此时,系统有可能立刻执行该任务,但如果你希望能够延迟执行,那么可以通过setInitialDelay()方法,延后任务的执行。


OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder(MyWorker.class)
              //设置传入Worker数据
              .setInputData(sendData)
              //设置触发条件
              .setConstraints(constraints)
              //设置符合触发条件后,延迟10秒执行
              .setInitialDelay(10, TimeUnit.SECONDS)
              //设置指数退避策略
              .setBackoffCriteria(BackoffPolicy.LINEAR, OneTimeWorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS)//设置指数退避算法
              //设置标签
              .addTag("UploadTag")
              .build();
4、设置指数退避策略

假如Worker线程的执行出现了异常,比如服务器宕机,那么你可能希望过一段时间,重试该任务。那么你可以在Worker的doWork()方法中返回Result.retry(),系统会有默认的指数退避策略来帮你重试任务,你也可以通过setBackoffCriteria()方法,自定义指数退避策略。

OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder(MyWorker.class)
              //设置传入Worker数据
              .setInputData(sendData)
              //设置触发条件
              .setConstraints(constraints)
              //设置符合触发条件后,延迟10秒执行
              .setInitialDelay(10, TimeUnit.SECONDS)
              //设置指数退避策略
              .setBackoffCriteria(BackoffPolicy.LINEAR, OneTimeWorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS)//设置指数退避算法
              //设置标签
              .addTag("UploadTag")
              .build();
5、为任务设置Tag标签

设置Tag后,你就可以通过该抱歉跟踪任务的状态WorkManager.getWorkInfosByTagLiveData(String tag)或者取消任务WorkManager.cancelAllWorkByTag(String tag)。

OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder(MyWorker.class)
              //设置传入Worker数据
              .setInputData(sendData)
              //设置触发条件
              .setConstraints(constraints)
              //设置符合触发条件后,延迟10秒执行
              .setInitialDelay(10, TimeUnit.SECONDS)
              //设置指数退避策略
              .setBackoffCriteria(BackoffPolicy.LINEAR, OneTimeWorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS)//设置指数退避算法
              //设置标签
              .addTag("UploadTag")
              .build();
6、将任务提交给系统。

WorkManager.enqueue()方法会将你配置好的WorkRequest交给系统来执行。

      WorkManager.getInstance(activity).enqueue(oneTimeWorkRequest);
7、观察任务的状态

任务在提交给系统后,通过WorkInfo获知任务的状态,WorkInfo包含了任务的id,tag,以及Worker对象传递过来的outputData,以及任务当前的状态。有三种方式可以得到WorkInfo对象。

WorkManager.getWorkInfosByTag()

WorkManager.getWorkInfoById()

WorkManager.getWorkInfosForUniqueWork()

如果你希望能够实时获知任务的状态。这三个方法还有对应的LiveData方法。

WorkManager.getWorkInfosByTagLiveData()

WorkManager.getWorkInfoByIdLiveData()

WorkManager.getWorkInfosForUniqueWorkLiveData()

通过观察LiveData,我们便可以在任务状态发生变化的时候,收到通知。

WorkManager.getInstance(activity).getWorkInfoByIdLiveData(oneTimeWorkRequest.getId()).observe(activity, new Observer<WorkInfo>() {
         @Override
         public void onChanged(WorkInfo workInfo) {
            //注意这个方法会回调多次,要加这个判断
            if(workInfo.getState().isFinished()){
               //执行完成的处理,接受worker返回的结果
               //这里的key与MyWorker的key需要一致
               String result = workInfo.getOutputData().getString("key");
            }
         }
      });
8、取消任务

与观察任务类似的,我们也可以根据Id或者Tag取消某个任务,或者取消所有任务。

      WorkManager.getInstance(activity).cancelWorkById(UUID)
      WorkManager.getInstance(activity).cancelAllWorkByTag("")
      WorkManager.getInstance(activity).cancelAllWork();
9、周期任务PeriodicWorkRequest

一次性任务,即任务在成功完成后,便彻底结束。而周期性任务则会按照设定的时间定期执行。

注意:周期性任务的间隔时间不能小于15分钟。源码设置最小15分钟。

PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(MyWorker.class, 15, TimeUnit.MINUTES)
              .setConstraints(constraints)
              .build();
10、任务链

如果你有一系列的任务需要顺序执行,那么可以利用WorkManager.beginWith().then().then().enqueue()方法;
例如:我们在上传图片之前,需要先对图片进行压缩,还需要更新本地图片。压缩与更新本地图片二者没有顺序,但与上传图片存在先后顺序。

WorkManager.getInstance(this).beginWith(compressWorkRequest, updateLocalWorkRequest).then(uploadWorkRequest).enqueue();
11、WorkContinuation复杂任务链

执行顺序:
任务链一:A—>B
任务链二:C—>D
任务链一—>任务链二—>E

WorkContinuation workContinuation1 =  WorkManager.getInstance(activity).beginWith(WorkRequestA).then(WorkRequestB);
      WorkContinuation workContinuation2 =  WorkManager.getInstance(activity).beginWith(WorkRequestC).then(WorkRequestD);
      List<WorkContinuation> taskList = new ArrayList<>();
      taskList.add(workContinuation1);
      taskList.add(workContinuation2);
      WorkContinuation.combine(taskList).then(WorkRequestE).enqueue();

四、Kotlin中使用

1、CoroutineWorker 中的线程处理

对于 Kotlin 用户,WorkManager 为协程提供了一流的支持。如要开始使用,请将 work-runtime-ktx 包含到您的 gradle 文件中。不要扩展 Worker,而应扩展 CoroutineWorker,后者包含 doWork() 的挂起版本。例如,如果要构建一个简单的 CoroutineWorker 来执行某些网络操作,您需要执行以下操作:

class CoroutineDownloadWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        val data = downloadSynchronously("https://www.google.com")
        saveData(data)
        return Result.success()
    }
}

请注意,CoroutineWorker.doWork() 是一个“挂起”函数。此代码不同于 Worker,不会在 Configuration 中指定的 Executor 中运行,而是默认为 Dispatchers.Default。您可以提供自己的 CoroutineContext 来自定义这个行为。在上面的示例中,您可能希望在 Dispatchers.IO 上完成此操作,如下所示:

class CoroutineDownloadWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        withContext(Dispatchers.IO) {
            val data = downloadSynchronously("https://www.google.com")
            saveData(data)
            return Result.success()
        }
    }
}

CoroutineWorker 通过取消协程并传播取消信号来自动处理停工情况。您无需执行任何特殊操作来处理停工情况

2、在其他进程中运行 CoroutineWorker

您还可以使用 RemoteCoroutineWorkerListenableWorker 的实现)将工作器绑定到特定进程。

RemoteCoroutineWorker 会使用您在构建工作请求时于输入数据中提供的两个额外参数绑定到特定进程:ARGUMENT_CLASS_NAMEARGUMENT_PACKAGE_NAME

以下示例演示了如何构建绑定到特定进程的工作请求:

val PACKAGE_NAME = "com.example.background.multiprocess"

val serviceName = RemoteWorkerService::class.java.name
val componentName = ComponentName(PACKAGE_NAME, serviceName)

val data: Data = Data.Builder()
   .putString(ARGUMENT_PACKAGE_NAME, componentName.packageName)
   .putString(ARGUMENT_CLASS_NAME, componentName.className)
   .build()

return OneTimeWorkRequest.Builder(ExampleRemoteCoroutineWorker::class.java)
   .setInputData(data)
   .build()

对于每个 RemoteWorkerService,您还需要在 AndroidManifest.xml 文件中添加服务定义:

<manifest ... >
    <service
            android:name="androidx.work.multiprocess.RemoteWorkerService"
            android:exported="false"
            android:process=":worker1" />

        <service
            android:name=".RemoteWorkerService2"
            android:exported="false"
            android:process=":worker2" />
    ...
</manifest>

3、RxWorker 中的线程处理

我们在 WorkManager 与 RxJava 之间提供互操作性。如需开始使用这种互操作性,除了在您的 gradle 文件中包含 work-runtime 之外,还应包含 work-rxjava3 依赖项。而且还有一个支持 rxjava2 的 work-rxjava2 依赖项,您可以根据情况使用。

然后,您应该扩展 RxWorker,而不是扩展 Worker。最后替换 RxWorker.createWork() 方法以返回 Single<Result>,用于表示代码执行的 Result,如下所示:

class RxDownloadWorker(
        context: Context,
        params: WorkerParameters
) : RxWorker(context, params) {
    override fun createWork(): Single<Result> {
        return Observable.range(0, 100)
                .flatMap { download("https://www.example.com") }
                .toList()
                .map { Result.success() }
    }
}

请注意,RxWorker.createWork() 在主线程上调用,但默认情况下会在后台线程上订阅返回值。您可以替换 RxWorker.getBackgroundScheduler() 来更改订阅线程。

RxWorkeronStopped() 时,系统会处理订阅,因此您无需以任何特殊方式处理停工情况

4、ListenableWorker 中的线程处理

在某些情况下,您可能需要提供自定义线程处理策略。例如,您可能需要处理基于回调的异步操作。在这种情况下,不能只依靠 Worker 来完成操作,因为它无法以阻塞方式完成这项工作。WorkManager 通过 ListenableWorker 支持该用例。ListenableWorker 是最基本的工作器 API;WorkerCoroutineWorkerRxWorker 都是从这个类衍生而来的。ListenableWorker 只会发出信号以表明应该开始和停止工作,而线程处理则完全交您决定。开始工作信号在主线程上调用,因此请务必手动转到您选择的后台线程。

抽象方法 ListenableWorker.startWork() 会返回一个将使用操作的 Result 设置的 ListenableFutureListenableFuture 是一个轻量级接口:它是一个 Future,用于提供附加监听器和传播异常的功能。在 startWork 方法中,应该返回 ListenableFuture,完成操作后,您需要使用操作的 Result 设置这个返回结果。您可以通过以下两种方式之一创建 ListenableFuture 实例:

  1. 如果您使用的是 Guava,请使用 ListeningExecutorService
  2. 否则,请将 councurrent-futures 包含到您的 gradle 文件中并使用 CallbackToFutureAdapter

如果您希望基于异步回调执行某些工作,则应以类似如下的方式执行:

class CallbackWorker(
        context: Context,
        params: WorkerParameters
) : ListenableWorker(context, params) {
    override fun startWork(): ListenableFuture<Result> {
        return CallbackToFutureAdapter.getFuture { completer ->
            val callback = object : Callback {
                var successes = 0

                override fun onFailure(call: Call, e: IOException) {
                    completer.setException(e)
                }

                override fun onResponse(call: Call, response: Response) {
                    successes++
                    if (successes == 100) {
                        completer.set(Result.success())
                    }
                }
            }

            repeat(100) {
                downloadAsynchronously("https://example.com", callback)
            }

            callback
        }
    }
}

如果您的工作停止会发生什么?如果预计工作会停止,则始终会取消 ListenableWorkerListenableFuture。通过使用 CallbackToFutureAdapter,您只需添加一个取消监听器即可,如下所示:

class CallbackWorker(
        context: Context,
        params: WorkerParameters
) : ListenableWorker(context, params) {
    override fun startWork(): ListenableFuture<Result> {
        return CallbackToFutureAdapter.getFuture { completer ->
            val callback = object : Callback {
                var successes = 0

                override fun onFailure(call: Call, e: IOException) {
                    completer.setException(e)
                }

                override fun onResponse(call: Call, response: Response) {
                    ++successes
                    if (successes == 100) {
                        completer.set(Result.success())
                    }
                }
            }

 completer.addCancellationListener(cancelDownloadsRunnable, executor)

            repeat(100) {
                downloadAsynchronously("https://example.com", callback)
            }

            callback
        }
    }
}
在其他进程中运行 ListenableWorker

您还可以使用 RemoteListenableWorkerListenableWorker 的实现)将工作器绑定到特定进程。

RemoteListenableWorker 会使用您在构建工作请求时于输入数据中提供的两个额外参数绑定到特定进程:ARGUMENT_CLASS_NAMEARGUMENT_PACKAGE_NAME

以下示例演示了如何构建绑定到特定进程的工作请求:

val PACKAGE_NAME = "com.example.background.multiprocess"

val serviceName = RemoteWorkerService::class.java.name
val componentName = ComponentName(PACKAGE_NAME, serviceName)

val data: Data = Data.Builder()
   .putString(ARGUMENT_PACKAGE_NAME, componentName.packageName)
   .putString(ARGUMENT_CLASS_NAME, componentName.className)
   .build()

return OneTimeWorkRequest.Builder(ExampleRemoteListenableWorker::class.java)
   .setInputData(data)
   .build()

对于每个 RemoteWorkerService,您还需要在 AndroidManifest.xml 文件中添加服务定义:

<manifest ... >
    <service
            android:name="androidx.work.multiprocess.RemoteWorkerService"
            android:exported="false"
            android:process=":worker1" />

        <service
            android:name=".RemoteWorkerService2"
            android:exported="false"
            android:process=":worker2" />
    ...
</manifest>

五、总结

1、WorkManager作用处理后台任务。出于设备电量的考虑,为开发者提供了WorkManager,旨在将一些不需要及时完成的任务交给它来完成。
2、WorkManager会根据系统的版本,使用不同的策略来完成任务,有可能因为版本原因无法正常使用。
3、周期任务:Android认为Success和Failure都属于终止类的通知,可能使用LiveData观察周期任务时不会收到Success这类的通知。

参考:Android开发者网站WorkManager使用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容