Jetpack之WorkManager

  • 概述

    WorkManager是什么?

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

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

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

    除此之外,WorkManager还可添加很多额外功能:

    1. 使用工作约束明确定义工作运行的最佳条件。例如,仅在设备采用不按流量计费的网络连接时、当设备处于空闲状态或者有足够的电量时运行。

    2. WorkManager 允许您使用灵活的调度窗口调度工作,以运行一次性重复工作。您还可以对工作进行标记或命名,以便调度唯一的、可替换的工作以及监控或取消工作组。

      已调度的工作存储在内部托管的 SQLite 数据库中,由 WorkManager 负责确保该工作持续进行,并在设备重新启动后重新调度。

      此外,WorkManager 遵循低电耗模式等省电功能和最佳做法,因此您在这方面无需担心。

    3. 您可以使用 WorkManager 调度需在后台立即执行的工作。您应该使用加急工作来处理对用户来说很重要且会在几分钟内完成的任务。

    4. 有时工作会失败。WorkManager 提供了灵活的重试政策,包括可配置的指数退避政策

    5. 对于复杂的相关工作,您可以使用直观的接口将各个工作任务串联起来,这样您便可以控制哪些部分依序运行,哪些部分并行运行。对于每项工作任务,您可以定义工作的输入和输出数据。将工作串联在一起时,WorkManager 会自动将输出数据从一个工作任务传递给下一个工作任务。

    6. WorkManager 无缝集成 CoroutinesRxJava,让您可以插入自己的异步 API,非常灵活。

    WorkManager 适用于需要可靠运行的工作,即使用户导航离开屏幕、退出应用或重启设备也不影响工作的执行。例如:

    • 向后端服务发送日志或分析数据。
    • 定期将应用数据与服务器同步。

    WorkManager 不适用于那些可在应用进程结束时安全终止的进程内后台工作。它也并非对所有需要立即执行的工作都适用的通用解决方案,对于对于短暂的、应用存活周期内的可以使用协程、RxJava、线程池等来处理;而对于精确的、在低电耗模式下也能被唤醒的任务,则使用AlarmManager(在电源和资源管理方面并不高效,但是贵在精确)来完成,适合用于精确闹钟或通知(例如日历活动)场景,不适合后台工作。

  • 使用入门

    • 添加依赖

      dependencies {
          def work_version = "2.8.0"
      
          // (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"
      }
      
    • 创建Worker

      class UploadWorker(appContext: Context, workerParams: WorkerParameters):
             Worker(appContext, workerParams) {
         override fun doWork(): Result {
             // Do the work here--in this case, upload the images.
             uploadImages()
             /**结果返回*/
               //Result.failure()
                   //Result.retry()
             return Result.success(
                  Data.Builder()
                      .putString("name","MPH")
                      .build()
             )
         }
      }
      
    • 创建request并开启后台任务

      val uploadWorkRequest: WorkRequest = OneTimeWorkRequestBuilder<UploadWorker>().build()
      //启动任务
      WorkManager.getInstance(myContext).enqueue(uploadWorkRequest)
      
  • 任务配置

    上面的使用入门介绍了使用WorkManager的一般步骤,下面我们来看看如何根据不同需求做具体的任务配置。

    WorkRequest 本身是抽象基类。该类有两个派生实现,可用于创建 OneTimeWorkRequestPeriodicWorkRequest 请求。顾名思义,OneTimeWorkRequest 适用于调度非重复性工作,而 PeriodicWorkRequest 则更适合调度以一定间隔重复执行的工作。

    • 一次性工作OneTimeWorkRequest创建

      对于无需额外配置的简单工作,可以使用静态方法 from

      val myWorkRequest = OneTimeWorkRequest.from(MyWork::class.java)
      

      对于更复杂的工作,可以使用构建器:

      val uploadWorkRequest: WorkRequest =
         OneTimeWorkRequestBuilder<MyWork>()
             // Additional configuration
             .build()
      
    • 定期任务PeriodicWorkRequest创建

      有时可能需要定期运行某些工作。例如,可能要定期备份数据、定期下载应用中的新鲜内容或者定期上传日志到服务器。

      //内联方法简化创建方式(内部也是用PeriodicWorkRequest.Builder)
      val saveRequest =
             PeriodicWorkRequestBuilder<SaveImageToFileWorker>(1, TimeUnit.HOURS)
                       // 间隔一小时
                 .build()
      //手动创建
      PeriodicWorkRequest.Builder(NewsWork::class.java, 20, TimeUnit.MINUTES).build()
      

      时间间隔定义为两次重复执行之间的最短时间,实际上任务执行时机并不是严格按照间隔时间安排,但是一定会超过最短间隔时间。工作器的确切执行时间取决于您在 WorkRequest 对象中设置的约束以及系统执行的优化:

      val myUploadWork = PeriodicWorkRequestBuilder<SaveImageToFileWorker>(
             1, TimeUnit.HOURS, // repeatInterval (the period cycle)
             15, TimeUnit.MINUTES) // flexInterval
          .build()
      

      任务会在从 repeatInterval - flexInterval 开始,一直到间隔结束的这段时间内运行。

      任务间隔时间和分给任务的执行时间段都有最小限制:

      /**
       * The minimum interval duration for {@link PeriodicWorkRequest} (in milliseconds).
       */
      public static final long MIN_PERIODIC_INTERVAL_MILLIS = 15 * 60 * 1000L; // 15 minutes.
      /**
       * The minimum flex duration for {@link PeriodicWorkRequest} (in milliseconds).
       */
      public static final long MIN_PERIODIC_FLEX_MILLIS = 5 * 60 * 1000L; // 5 minutes.
      
    • 加急工作

      加急工作具有以下特征:

      • 重要性:加急工作适用于对用户很重要或由用户启动的任务。
      • 速度:加急工作最适合那些立即启动并在几分钟内完成的简短任务。
      • 配额:限制前台执行时间的系统级配额决定了加急作业是否可以启动。
      • 电源管理电源管理限制(如省电模式和低电耗模式)不太可能影响加急工作。
      • 延迟时间:系统立即执行加急工作,前提是系统的当前工作负载允许执行此操作。这意味着这些工作对延迟时间较为敏感,不能安排到以后执行。

      在用户想要发送消息或附加的图片时,可能会在聊天应用内使用加急工作。同样,处理付款或订阅流程的应用也可能需要使用加急工作。这是因为这些任务对用户很重要,会在后台快速执行,并需要立即开始执行。

      执行时间并非无限制,而是受配额限制。如果您的应用使用其执行时间并达到分配的配额,在配额刷新之前,您无法再执行加急工作。这样,Android 可以更有效地在应用之间平衡资源。

      每个应用均有自己的前台执行时间配额。可用的执行时间取决于待机模式存储分区和进程的重要性。

      调用 setExpedited() 来声明 WorkRequest 应该使用加急作业,以尽可能快的速度运行:

      val request = OneTimeWorkRequestBuilder()
          .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
          .build()
      
      WorkManager.getInstance(context)
          .enqueue(request)
      

      参数表示配额策略,有两种:

      • OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST,这会导致作业作为普通工作请求运行。
      • OutOfQuotaPolicy.DROP_WORK_REQUEST,这会在配额不足时导致请求取消。

      在系统负载过高或者配额满了的情况下加急任务也会被延迟。

    • 工作约束

      可以对定期工作设置约束。例如,你可以为工作请求添加约束,以便工作仅在用户设备充电时运行。在这种情况下,除非满足约束条件,否则即使过了定义的重复间隔,PeriodicWorkRequest 也不会运行。这可能会导致工作在某次运行时出现延迟,甚至会因在相应间隔内未满足条件而被跳过。

      image-20230222140850528

      例如,以下代码会构建了一个工作请求,该工作请求仅在用户设备正在充电且连接到 Wi-Fi 网络时才会运行:

      val constraints = Constraints.Builder()
         .setRequiredNetworkType(NetworkType.UNMETERED)
         .setRequiresCharging(true)
         .build()
      
      val myWorkRequest: WorkRequest =
         OneTimeWorkRequestBuilder<MyWork>()
             .setConstraints(constraints)
             .build()
      

      如果指定了多个约束,工作将仅在满足所有约束时才会运行。

      如果在工作运行时不再满足某个约束,WorkManager 将停止工作器。系统将在满足所有约束后重试工作。

    • 延迟执行

      如果工作没有约束,或者当工作加入队列时所有约束都得到了满足,那么系统可能会选择立即运行该工作。如果您不希望工作立即运行,可以将工作指定为在经过一段最短初始延迟时间后再启动。

      val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
           //10分钟后执行
         .setInitialDelay(10, TimeUnit.MINUTES)
         .build()
      
    • 重试策略

      如果需要让 WorkManager 重试工作,可以从doWork方法返回 Result.retry()。然后,系统将根据退避延迟时间退避政策重新调度工作。

      • 退避延迟时间指定了首次尝试后重试工作前的最短等待时间。此值建议不超过 10 秒~5小时范围。
      • 退避政策定义了在后续重试过程中,退避延迟时间随时间以怎样的方式增长。WorkManager 支持 2 个退避政策,即 LINEAREXPONENTIAL

      每个工作请求都有退避政策和退避延迟时间。默认政策是 EXPONENTIAL,延迟时间为 10 秒,但您可以在工作请求配置中替换此设置。如果政策为 LINEAR,每次尝试重试时,重试间隔都会增加约 10 秒。例如,第一次运行以 Result.retry() 结束并在 10 秒后重试;然后,如果工作在后续尝试后继续返回 Result.retry(),那么接下来会在 20 秒、30 秒、40 秒后重试,以此类推。如果退避政策设置为 EXPONENTIAL,那么重试时长序列将接近 20、40、80 秒,以此类推。

      val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
         .setBackoffCriteria(
             BackoffPolicy.LINEAR,
             OneTimeWorkRequest.MIN_BACKOFF_MILLIS,//可以指定自定义时长
             TimeUnit.MILLISECONDS)
         .build()
      
    • 输入数据和输出数据

      // Define the Worker requiring input
      class UploadWork(appContext: Context, workerParams: WorkerParameters)
         : Worker(appContext, workerParams) {
      
         override fun doWork(): Result {
             val imageUriInput =
                 inputData.getString("IMAGE_URI") ?: return Result.failure()
      
             uploadFile(imageUriInput)
               //若Worker 停止后,从 Worker.doWork() 返回什么已不重要;Result 将被忽略
             return Result.success(
                      //设置输出数据
                          Data.Builder()
                                   .putString("name","MPH")
                                   .build()
             )
         }
         ...
      }
      val myUploadWork = OneTimeWorkRequestBuilder<UploadWork>()
           //设置输入数据
         .setInputData(workDataOf(
             "IMAGE_URI" to "http://..."
         ))
         .build()
      

      输入输出数据都是Map形式的Data类型。

    • 设置Tag

      每个工作请求都可以添加多个标识符,该标识符可用于在以后标识该工作,以便取消工作或观察其进度

      如果有一组在逻辑上相关的工作,对这些工作项进行标记可能也会很有帮助。通过标记,您一起处理一组工作请求。

      val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
         .addTag("cleanup")
           .addTag("room")  //可以添加多个,相当于添加分类
         .build()
      
  • 管理任务

    • 设置唯一工作

      在将工作加入队列时请小心谨慎,以避免重复。例如,应用可能会每 24 小时尝试将其日志上传到后端服务。如果不谨慎,即使作业只需运行一次,最终也可能会多次将同一作业加入队列。为了实现此目标,您可以将工作调度为唯一工作

      唯一工作既可用于一次性工作,也可用于定期工作:

      • WorkManager.enqueueUniqueWork()(用于一次性工作)
      • WorkManager.enqueueUniquePeriodicWork()(用于定期工作)

      这两种方法都接受 3 个参数:

      • uniqueWorkName - 用于唯一标识工作请求的 String
      • existingWorkPolicy - 此 enum 可告知 WorkManager:如果已有使用该名称且尚未完成的唯一工作链,应执行什么操作。
      • work - 要调度的 WorkRequest
      val sendLogsWorkRequest =
             PeriodicWorkRequestBuilder<SendLogsWorker>(24, TimeUnit.HOURS)
                 .setConstraints(Constraints.Builder()
                     .setRequiresCharging(true)
                     .build()
                  )
                 .build()
      WorkManager.getInstance(this).enqueueUniquePeriodicWork(
                 "sendLogs", //自定义名字
                 ExistingPeriodicWorkPolicy.KEEP,
                 sendLogsWorkRequest
      )
      

      第二个参数是当设置多个同一名字的唯一任务时如何处理这种冲突。

      对于一次性工作,您需要提供一个 ExistingWorkPolicy,支持用于处理冲突的 4 个选项如下:

      • REPLACE:用新工作替换现有工作。此选项将取消现有工作。

      • KEEP:保留现有工作,并忽略新工作。

      • APPEND:将新工作附加到现有工作的末尾。此政策将导致您的新工作链接到现有工作,在现有工作完成后运行。现有工作将成为新工作的先决条件。如果现有工作变为 CANCELLEDFAILED 状态,新工作也会变为 CANCELLEDFAILED。如果您希望无论现有工作的状态如何都运行新工作,请改用 APPEND_OR_REPLACE

      • APPEND_OR_REPLACE 函数类似于 APPEND,不过它并不依赖于先决条件工作状态。即使现有工作变为 CANCELLEDFAILED 状态,新工作仍会运行。

      而对于定期工作,需要提供一个 ExistingPeriodicWorkPolicy,它支持 REPLACEKEEP 这两个选项。这些选项的功能与其对应的 ExistingWorkPolicy 功能相同。

    • 监听任务进度和结果

      //获取
      workManager.getWorkInfoById(syncWorker.id) // ListenableFuture<WorkInfo>
      workManager.getWorkInfosForUniqueWork("sync") // ListenableFuture<List<WorkInfo>>
      workManager.getWorkInfosByTag("syncTag") // ListenableFuture<List<WorkInfo>>
      
      val work1Id = getWorkInfoById(work1.id).get().id
      getWorkInfosByTag("cleanup").get().forEach {
          val workState = it.state
          val workOutputData = it.outputData
      }
      //取消
      workManager.cancelWorkById(syncWorker.id)
      workManager.cancelUniqueWork("sync")
      workManager.cancelAllWorkByTag("syncTag")
      
      
      workManager.getWorkInfoByIdLiveData(syncWorker.id)
                     .observe(viewLifecycleOwner) { workInfo ->
         if(workInfo?.state == WorkInfo.State.SUCCEEDED) {
             Snackbar.make(requireView(),
            R.string.work_completed, Snackbar.LENGTH_SHORT)
                 .show()
         }
      }
      

      WorkManager 2.4.0 及更高版本支持使用 WorkQuery 对象对已加入队列的作业进行复杂查询。WorkQuery 支持按工作的标记、状态和唯一工作名称的组合进行查询:

      val workQuery = WorkQuery.Builder
             .fromTags(listOf("syncTag"))
             .addStates(listOf(WorkInfo.State.FAILED, WorkInfo.State.CANCELLED))
             .addUniqueWorkNames(listOf("preProcess", "sync")
          )
         .build()
      
      val workInfos: ListenableFuture<List<WorkInfo>> = workManager.getWorkInfos(workQuery)
      

      WorkManager 会在后台检查工作的 State。如果工作已经完成,系统不会执行任何操作。否则,工作的状态会更改为 CANCELLED,之后就不会运行这个工作。任何依赖于此工作WorkRequest 作业也将变为 CANCELLED。可以重写ListenableWorker.onStopped()方法来处理清理工作,它会在停止时调用,可以调用 ListenableWorker.isStopped() 方法以检查工作器是否已停止。

      对于使用 ListenableWorkerWorker 的 Java 开发者,setProgressAsync() API 会返回 ListenableFuture<Void>;更新进度是异步过程,因为更新过程涉及将进度信息存储在数据库中。在 Kotlin 中,可以使用 CoroutineWorker 对象的 setProgress() 扩展函数来更新进度信息:

      import android.content.Context
      import androidx.work.CoroutineWorker
      import androidx.work.Data
      import androidx.work.WorkerParameters
      import kotlinx.coroutines.delay
      
      class ProgressWorker(context: Context, parameters: WorkerParameters) :
          CoroutineWorker(context, parameters) {
      
          companion object {
              const val Progress = "Progress"
              private const val delayDuration = 1L
          }
      
          override suspend fun doWork(): Result {
              val firstUpdate = workDataOf(Progress to 0)
              val lastUpdate = workDataOf(Progress to 100)
              //如果是Worker,则调用setProgressAsync方法
              setProgress(firstUpdate)
              delay(delayDuration)
              setProgress(lastUpdate)
              return Result.success()
          }
      }
      

      观察进度信息也很简单,可以使用 getWorkInfoBy…()getWorkInfoBy…LiveData() 方法,并引用 WorkInfo:

      WorkManager.getInstance(applicationContext)
          // requestId is the WorkRequest id
          .getWorkInfoByIdLiveData(requestId)
          .observe(observer, Observer { workInfo: WorkInfo? ->
                  if (workInfo != null) {
                      val progress = workInfo.progress
                      val value = progress.getInt(Progress, 0)
                      // Do something with progress information
                  }
          })
      
    • 链接任务

      链接任务只能用于OneTimeWorkRequest

      如需创建工作链,您可以使用 WorkManager.beginWith(OneTimeWorkRequest)WorkManager.beginWith(List),这会返回 WorkContinuation 实例。

      然后,可以使用 WorkContinuation 通过 then(OneTimeWorkRequest)then(List) 添加 OneTimeWorkRequest 依赖实例。 .

      每次调用 WorkContinuation.then(...) 都会返回一个新的 WorkContinuation 实例。如果添加了 OneTimeWorkRequest 实例的 List,这些请求可能会并行运行。

      最后,您可以使用 WorkContinuation.enqueue() 方法对 WorkContinuation 工作链执行 enqueue() 操作。

      WorkManager.getInstance(myContext)
         // Candidates to run in parallel
         .beginWith(listOf(plantName1, plantName2, plantName3))
         // Dependent work (only runs after all previous work in chain)
         .then(cache)
         .then(upload)
         // Call enqueue to kick things off
         .enqueue()
      

      当您链接 OneTimeWorkRequest 实例时,父级工作请求的输出将作为子级的输入传入。因此,在上面的示例中,plantName1plantName2plantName3 的输出将作为 cache 请求的输入传入。

      为了管理来自多个父级工作请求的输入,WorkManager 使用 InputMerger

      WorkManager 提供两种不同类型的 InputMerger

      • OverwritingInputMerger 会尝试将所有输入中的所有键添加到输出中。如果发生冲突,它会覆盖先前设置的键。是默认的合并规则。

        image-20230222154711873
      • ArrayCreatingInputMerger 会尝试合并输入,并在必要时创建数组。

        val cache: OneTimeWorkRequest = OneTimeWorkRequestBuilder<PlantWorker>()
           .setInputMerger(ArrayCreatingInputMerger::class)
           .setConstraints(constraints)
           .build()
        
        image-20230222154827076

      如果有更具体的用例,则可以创建 InputMerger 的子类来编写自己的用例。

      当上游的任务成功后才会执行下游的任务,只要上游的任务失败或者取消,则其下游的任务都会失败或者取消,但是对同级的任务不会被影响。

  • WorkManager配置和初始化

    从WorkManager2.6开始,在androidx.work:work-runtime库中的AndroidManifest文件中就配置了WorkManagerInitializer的provider:

    <application>
        <provider
            android:name="androidx.startup.InitializationProvider"
            android:authorities="${applicationId}.androidx-startup"
            android:exported="false"
            tools:node="merge" >
            <meta-data
                android:name="androidx.work.WorkManagerInitializer"
                android:value="androidx.startup" />
        </provider>
          ...
    </application>
    
    public final class WorkManagerInitializer implements Initializer<WorkManager> {
        @Override
        public WorkManager create(@NonNull Context context) {
            WorkManager.initialize(context, new Configuration.Builder().build());
            return WorkManager.getInstance(context);
        }
          ...
    }
    

    这就说明,如果你使用的是2.6以上版本的话,在应用启动时,WorkManager就会自动初始化。那如果我们想要在使用时才初始化的话怎么办呢?这时就得根据Manifest合并规则在app的AndroidManifest中重写来覆盖它:

     <!-- 如果应用不需要InitializationProvider的话可以直接移除InitializationProvider -->
    <provider
       android:name="androidx.startup.InitializationProvider"
       android:authorities="${applicationId}.androidx-startup"
       tools:node="remove">
    </provider> 
    <!--如果还用到其他Initializer,只是禁用WorkManagerInitializer的话-->
    <provider
        android:name="androidx.startup.InitializationProvider"
        android:authorities="${applicationId}.androidx-startup"
        android:exported="false"
        tools:node="merge">
        <meta-data
            android:name="androidx.work.WorkManagerInitializer"
            android:value="androidx.startup"
            tools:node="remove" />
     </provider>
    

    如果使用2.6之前的版本,则为:

    <provider
        android:name="androidx.work.impl.WorkManagerInitializer"
        android:authorities="${applicationId}.workmanager-init"
        tools:node="remove" />
    

    如果想要自定义WorkManager配置,则需要让 Application 类实现 Configuration.Provider 接口,并提供自己的 Configuration.Provider.getWorkManagerConfiguration() 实现。当需要使用 WorkManager 时,请务必调用方法 WorkManager.getInstance(Context)。WorkManager 会调用应用的自定义 getWorkManagerConfiguration() 方法来发现其 Configuration(无需自行调用 WorkManager.initialize()):

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

推荐阅读更多精彩内容