WorkManager 最全攻略

1. 介紹

關(guān)于workmanager 的介紹 官網(wǎng)上是這么說的: 使用 WorkManager API 可以輕松地調(diào)度即使在應用退出或設備重啟時仍應運行的可延遲異步任務婚瓜。 重點是后面的幾個字,運行可延遲的異步任務, 在退出或者重啟的時候,我們在之前要實現(xiàn)這種任務可能需要 BroadcastReceiver 或者 AlarmManager,現(xiàn)在的話可以使用WorkManager,這個框架最高兼容至Android api 14

  • Android 23以上是采用JobScheduler
  • Android 14-22 采用的是BroadcastReceiver 和 AlarmManager

2. 優(yōu)點

  • 向下兼容至api 14
  • 可以添加任務執(zhí)行的約束條件,比如說 延遲執(zhí)行,是否在低電量模式下執(zhí)行,是否在充電模式下執(zhí)行,是否在設備空閑時執(zhí)行等等
  • 調(diào)度一次性或周期性異步任務
  • 監(jiān)管任務,可以隨時取消任務
  • 將任務鏈接起來,比如說執(zhí)行可以指定多個任務的執(zhí)行順序

    以上傳圖片為例:我們分解一下任務:1.圖片添加濾鏡 -> 圖片壓縮 -> 圖片上傳,這個是有執(zhí)行順序的,采用workmanager 我們可以很方便的實現(xiàn)

  • 確保任務執(zhí)行宝鼓,即使應用或設備重啟也同樣執(zhí)行任務

3. 使用

3.1 為項目添加依賴

//根據(jù)項目需要自行添加依賴,不需要全部添加
    dependencies {
      def work_version = "2.3.1"

        // (Java only)
        implementation "androidx.work:work-runtime:$work_version"http://java 語言選這個

        // Kotlin + coroutines
        implementation "androidx.work:work-runtime-ktx:$work_version"http://kotlin 選這個

        // optional - RxJava2 support
        implementation "androidx.work:work-rxjava2:$work_version"http://配合rxjava2 使用

      }
    

3.2 創(chuàng)建一個后臺任務

代理示例如下(以上傳圖片為例):

class UploadPicWork(
    private val context: Context,
    private val workerParameters: WorkerParameters
) :
    Worker(context, workerParameters) {
    override fun doWork(): Result {

        uploadPic()//具體上傳圖片的邏輯

        return Result.success()
    }
}

3.3 創(chuàng)建一個workrequest

//此處的 UploadPicWork 就是之前創(chuàng)建的任務
val uploadPicWork = OneTimeWorkRequestBuilder<UploadPicWork>()
                .setConstraints(triggerContentMaxDelay).build()

3.4 執(zhí)行任務

//此處的 uploadPicWork 就是前一步創(chuàng)建的 workrequest
  WorkManager.getInstance(myContext).enqueue(uploadPicWork)

到這里一個簡單的任務就可以執(zhí)行了,但往往我們在開發(fā)的過程中,可能滿足不了我們的需求,再繼續(xù)往下看!

4. 創(chuàng)建一個"復雜的任務"

4.1 創(chuàng)建任務執(zhí)行的約束條件


//注意 以下條件都是 && 的關(guān)系

val triggerContentMaxDelay =
                Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)//網(wǎng)絡鏈接的時候使用,避免各種網(wǎng)絡判斷,省時省力
                .setRequiresDeviceIdle(false)//是否在設備空閑的時候執(zhí)行
                .setRequiresBatteryNotLow(true)//是否在低電量的時候執(zhí)行
                .setRequiresStorageNotLow(true)//是否在內(nèi)存不足的時候執(zhí)行
                .setRequiresCharging(true)//是否時充電的時候執(zhí)行
                .setTriggerContentMaxDelay(1000 * 1, TimeUnit.MILLISECONDS)//延遲執(zhí)行
                .build()
                

//目前就提供這幾種約束條件,大家可以在使用的過程中酌情添加

4.2 為任務添加約束條件


val uploadPicWork =
                OneTimeWorkRequestBuilder<UploadPicWork>()
                    .setConstraints(triggerContentMaxDelay)//約束條件
                    .build()

 WorkManager.getInstance(myContext).enqueue(uploadPicWork)//執(zhí)行
 

4.3 為worker 傳遞參數(shù)

這個可能比較重要,我們一般在執(zhí)行任務的時候需要把參數(shù)傳遞到Worker中,可以采用構(gòu)造或者 寫一個set方法給傳遞進去,但是這種方式在這可能不適用


//可以采用這種方式傳遞參數(shù)
   val UploadPicWork =
                OneTimeWorkRequestBuilder<UploadPicWork>()
                    //此處set input data 需要的參數(shù) 是一個Data對象,注意只可以添加一次,如果有多個參數(shù)需要傳遞,可以封裝成一個data 數(shù)據(jù)類
                    .setInputData(workDataOf("params_tag" to "params"))
                    .setConstraints(triggerContentMaxDelay).build()

    ...
    

/**
 * Converts a list of pairs to a [Data] object.
 *
 * If multiple pairs have the same key, the resulting map will contain the value
 * from the last of those pairs.
 *
 * Entries of the map are iterated in the order they were specified.
 */
inline fun workDataOf(vararg pairs: Pair<String, Any?>): Data {
    val dataBuilder = Data.Builder()
    for (pair in pairs) {
        dataBuilder.put(pair.first, pair.second)
    }
    return dataBuilder.build()
}

可以看看這個 workDataOf 函數(shù),就是將一個 Pair對象轉(zhuǎn)成一個Data對象,將
pair.first 作為 key ,pair.second 作為 value 構(gòu)建了一個Data 對象. 有的同學可能對 "params_tag" to "params" 這種寫法比較差異,解釋一下,這是構(gòu)造了一個 Pair 對象,
不了解 Pair 這個類的同學,可以看下Pair,參數(shù)的類可以是基本數(shù)據(jù)類型,也可以引用數(shù)據(jù)類型

4.4 獲取參數(shù)


class UploadPicWork(
    private val context: Context,
    private val workerParameters: WorkerParameters
) :
    Worker(context, workerParameters) {
    override fun doWork(): Result {

       val params = inputData.getString("params_tag")//獲取傳遞的參數(shù)

        uploadPic()//上傳圖片

        return Result.success()
    }
}

inputDataWorker 類 父類的一個函數(shù),在Java 中 可以 類似 this.getInputData() 返回的是一個 Data 對象,就可以獲取傳遞的參數(shù)了.

5. Worker 的狀態(tài)

在之前的創(chuàng)健的過程中,在 doWork 函數(shù)中,我們返回的 Result.success(); 我們默認 ,任務 uploadPic 函數(shù)順利的執(zhí)行完成了,所以返回了 success 狀態(tài),但是在實際開發(fā)過程中 可以能因為各種各樣的問題會導致 失敗,這時候就不能返回success了,類似這樣:



class UploadPicWork(
    private val context: Context,
    private val workerParameters: WorkerParameters
) :
    Worker(context, workerParameters) {
    override fun doWork(): Result {

       val params = inputData.getString("params_tag")//獲取傳遞的參數(shù)

        try {
            uploadPic()//上傳圖片
        } catch (e: Exception) {
            return Result.failure(Data())//執(zhí)行失敗了
        }

        return Result.success()
    }
}

Result.failure(Data()),這個函數(shù)可以什么都不傳,但是如果關(guān)注失敗的原因的話,可以把封裝一個Data對象,傳遞出去!!,至于怎么觀察任務的執(zhí)行結(jié)果,以及拿到執(zhí)行失敗傳遞的參數(shù),后面會講!

5.1 Worker 的各種狀態(tài)說明

在Worker 生命周期內(nèi),會經(jīng)歷不同的 State

  • 如果有尚未完成的前提性工作茂附,則工作處于 BLOCKED State督弓。
  • 如果工作能夠在滿足 約束條件 和時機條件后立即運行愚隧,則被視為處于 ENQUEUED 狀態(tài)。
  • Worker 在活躍地執(zhí)行時录煤,其處于 RUNNING State荞胡。
  • 如果 Worker 返回 Result.success(),則被視為處于 SUCCEEDED 狀態(tài)廊营。這是一種終止 State露筒;只有 OneTimeWorkRequest 可以進入這種 State。
  • 如果 Worker 返回 Result.failure()伶氢,則被視為處于 FAILED 狀態(tài)瘪吏。這也是一個終止 State;只有 OneTimeWorkRequest 可以進入這種 State劣砍。所有依賴工作也會被標記為 FAILED扇救,并且不會運行迅腔。
  • 當取消尚未終止的 WorkRequest 時,它會進入 CANCELLED State掠兄。所有依賴工作也會被標記為 CANCELLED锌雀,并且不會運行。

6. 觀察Worker 的狀態(tài)(需要搭配 LiveData組件使用)

將工作加入隊列后婿牍,可以通過 WorkManager 檢查其狀態(tài)等脂。相關(guān)信息在 WorkInfo 對象中獲取撑蚌,包括 Workeridtag粉楚、當前 State 和返回數(shù)據(jù)。

6.1 獲取 WorkInfo

  • 聽過 id 獲取,可以聽過 WorkManager.getWorkInfoById(UUID)WorkManager.getWorkInfoByIdLiveData(UUID) 來通過 WorkRequest id 來獲取 WorkInfo伟骨。
 WorkManager.getInstance(this)
                .getWorkInfoByIdLiveData(UploadPicWork.id)// 通過id 獲取
                .observe(this, Observer { //it:WorkInfo
                    it?.apply {
                        when (this.state) {
                            WorkInfo.State.BLOCKED -> println("BLOCKED")
                            WorkInfo.State.CANCELLED -> println("CANCELLED")
                            WorkInfo.State.RUNNING -> println("RUNNING")
                            WorkInfo.State.ENQUEUED -> println("ENQUEUED")
                            WorkInfo.State.FAILED -> println("FAILED")
                            WorkInfo.State.SUCCEEDED -> println("SUCCEEDED")
                            else -> println("else status ${this.state}")
                        }
                    }

                })

  • 通過 tag 獲取,可以利用 WorkManager.getWorkInfosByTag(String)WorkManager.getWorkInfosByTagLiveData(String) 來通過 WorkRequestWorkInfo 對象。
//要通過 tag 獲取,則需要先設置 tag
val UploadPicWork =
                OneTimeWorkRequestBuilder<UploadPicWork>()
                    .setInputData(workDataOf("params_tag" to "params"))//傳遞參數(shù)
                    .setConstraints(triggerContentMaxDelay)//設置約束條件
                    .addTag("tag")//設置tag
                    .build()

//獲取 workInfo

WorkManager.getInstance(this)
                .getWorkInfosByTagLiveData("tag")
                .observe(this, Observer {it:List<WorkInfo>//此處返回的是一個集合,作為示例代碼,默認只取 0 index
                    it?.apply {
                        when (this[0].state) {
                            WorkInfo.State.BLOCKED -> println("BLOCKED")
                            WorkInfo.State.CANCELLED -> println("CANCELLED")
                            WorkInfo.State.RUNNING -> println("RUNNING")
                            WorkInfo.State.ENQUEUED -> println("ENQUEUED")
                            WorkInfo.State.FAILED -> println("FAILED")
                            WorkInfo.State.SUCCEEDED -> println("SUCCEEDED")
                            else -> println("else status ${this[0]}")
                        }
                    }

                })

  • 對于 唯一工作名稱 的一個 Worker ,可以利用 WorkManager.getWorkInfosForUniqueWork(String)WorkManager.getWorkInfosForUniqueWorkLiveData(String) 檢索所有匹配的 WorkRequestWorkInfo 對象。此處估計不太好理解,唯一工作是一個概念性非常強的術(shù)語鲫剿,可確保一次只有一個具有特定名稱的工作鏈稻轨。與 id 不同的是,唯一名稱是人類可讀的政冻,由開發(fā)者指定线欲,而不是由 WorkManager 自動生成李丰。與標記不同,唯一名稱僅與“一個”工作鏈關(guān)聯(lián)舟舒。您可以通過調(diào)用 WorkManager.enqueueUniqueWork(String, ExistingWorkPolicy, OneTimeWorkRequest)WorkManager.enqueueUniquePeriodicWork(String, ExistingPeriodicWorkPolicy, PeriodicWorkRequest) 創(chuàng)建唯一工作序列嗜憔。
    可以參考官網(wǎng)的說法 唯一工作名稱
WorkManager.getInstance(this)
                .getWorkInfosForUniqueWorkLiveData("UploadPicWork")//唯一工作名稱 
                .observe(this, Observer {it:List<WorkInfo> //此處返回的是一個集合,作為示例代碼,默認只取 0

                    it?.apply {
                        when (this[0].state) {
                            WorkInfo.State.BLOCKED -> println("BLOCKED")
                            WorkInfo.State.CANCELLED -> println("CANCELLED")
                            WorkInfo.State.RUNNING -> println("RUNNING")
                            WorkInfo.State.ENQUEUED -> println("ENQUEUED")
                            WorkInfo.State.FAILED -> println("FAILED")
                            WorkInfo.State.SUCCEEDED -> println("SUCCEEDED")
                            else -> println("else status ${this[0]}")
                        }
                    }

                })

注意如采用這種方式獲取 workinfo ,在執(zhí)行 worker 的時候與之前不一樣,需要采用 WorkManager.enqueueUniqueWork(String, ExistingWorkPolicy, OneTimeWorkRequest)WorkManager.enqueueUniquePeriodicWork(String, ExistingPeriodicWorkPolicy, PeriodicWorkRequest) 來執(zhí)行

//全部代碼如下


//創(chuàng)建約束條件
val triggerContentMaxDelay =
                Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED)
//                    .setRequiresDeviceIdle(false)
                    .setRequiresBatteryNotLow(true)
                    .setRequiresStorageNotLow(true)
                    .setRequiresCharging(true)
                    .setTriggerContentMaxDelay(1000 * 1, TimeUnit.MILLISECONDS)
                    .build()

// 創(chuàng)建workrequest
            val UploadPicWork =
                OneTimeWorkRequestBuilder<UploadPicWork>()
                    .setInputData(workDataOf("params_tag" to "params"))
                    .setConstraints(triggerContentMaxDelay)
                    .build()

    //注意!!!,此處區(qū)別與之前的 WorkManager.getInstance(this).enqueue(UploadPicWork)
    
    // "UploadPicWork" 需要與下面代碼 getWorkInfosForUniqueWorkLiveData("UploadPicWork") 中字符串對應
    // ExistingWorkPolicy.APPEND 一個枚舉值,worker 執(zhí)行的策略,想要了解的同學,可以看下面的鏈接
    WorkManager.getInstance(this).enqueueUniqueWork("UploadPicWork",ExistingWorkPolicy.APPEND,UploadPicWork)

            WorkManager.getInstance(this)
                .getWorkInfosForUniqueWorkLiveData("UploadPicWork")
                .observe(this, Observer {

                    it?.apply {
                        when (this[0].state) {
                            WorkInfo.State.BLOCKED -> println("BLOCKED")
                            WorkInfo.State.CANCELLED -> println("CANCELLED")
                            WorkInfo.State.RUNNING -> println("RUNNING")
                            WorkInfo.State.ENQUEUED -> println("ENQUEUED")
                            WorkInfo.State.FAILED -> println("FAILED")
                            WorkInfo.State.SUCCEEDED -> println("SUCCEEDED")
                            else -> println("else status ${this[0]}")
                        }
                    }



ExistingWorkPolicy

7. 多個Worker 的順序執(zhí)行

在我們實際開發(fā)中可能遇到如下場景.一個任務可能依賴與其他的任務,并且每個任務之間有先后順序,以前面簡介中,圖片上傳為例:上傳圖片之前 需要 先壓縮,壓縮之前需要先剪裁, 流程如下 濾鏡--> 壓縮 --> 上傳圖片,對于這部分內(nèi)容,官網(wǎng)上介紹的已經(jīng)足夠清清楚了! 鏈接工作

  • 您可以使用 WorkManager 創(chuàng)建工作鏈并為其排隊。工作鏈用于指定多個關(guān)聯(lián)任務并定義這些任務的運行順序帚稠。當您需要以特定的順序運行多個任務時滋早,這尤其有用。
    要創(chuàng)建工作鏈搁进,您可以使用 WorkManager.beginWith(OneTimeWorkRequest)WorkManager.beginWith(List<OneTimeWorkRequest>),這會返回 WorkContinuation 實例影兽。
    然后莱革,可以通過 WorkContinuation 使用 WorkContinuation.then(OneTimeWorkRequest)WorkContinuation.then(List<OneTimeWorkRequest>) 來添加從屬 OneTimeWorkRequest盅视。
    每次調(diào)用 WorkContinuation.then(...) 都會返回一個新的 WorkContinuation 實例。如果添加了 OneTimeWorkRequestList镶蹋,這些請求可能會并行運行赏半。
    最后,您可以使用 WorkContinuation.enqueue() 方法為 WorkContinuation 鏈排隊牧氮。
    讓我們看一個示例:某個應用對3個不同的圖像執(zhí)行圖像濾鏡(可能會并行執(zhí)行)瑰枫,然后將這些圖像壓縮在一起光坝,再上傳它們。
    WorkManager.getInstance(myContext)
        // Candidates to run in parallel
        //濾鏡1 濾鏡2 濾鏡3 ...
        .beginWith(listOf(filter1, filter2, filter3))
        // Dependent work (only runs after all previous work in chain)
        //壓縮
        .then(compress)
        //上傳
        .then(upload)
        // Don't forget to enqueue()
        .enqueue()//執(zhí)行

8. 執(zhí)行重復任務

很好理解,就是在給定的時間間隔內(nèi)定期執(zhí)行任務,比如說 每個一個小時,上報位置信息,每個3個小時備份一個日志等等... 代碼示例如下:


val triggerContentMaxDelay =
                Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED)
//                    .setRequiresDeviceIdle(false)
                    .setRequiresBatteryNotLow(true)
                    .setRequiresStorageNotLow(true)
                    .setRequiresCharging(true)
                    .setTriggerContentMaxDelay(1000 * 1, TimeUnit.MILLISECONDS)
                    .build()

//            val UploadPicWork =
//                OneTimeWorkRequestBuilder<UploadPicWork>()
//                    .setInputData(workDataOf("params_tag" to "params"))
//                    .setConstraints(triggerContentMaxDelay)
//                    .addTag("tag")
//                    .build()
//

            val build = PeriodicWorkRequestBuilder<UploadPicWork>(
                1000 * 60 *15,
                TimeUnit.MICROSECONDS
            ).setConstraints(triggerContentMaxDelay).build()

            WorkManager.getInstance(this).enqueue(build)

注意!!!這個時間間隔不可低于15分鐘

9.任務執(zhí)行失敗重試

這個場景在實際開發(fā)中也經(jīng)常遇到,比如在任務執(zhí)行的過程中可能由于一些別的原因?qū)е氯蝿請?zhí)行失敗,但是我們希望過一段時間可以重試 代碼示例如下:


//約束條件
  val triggerContentMaxDelay =
                Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED)
//                    .setRequiresDeviceIdle(false)
                    .setRequiresBatteryNotLow(true)
                    .setRequiresStorageNotLow(true)
                    .setRequiresCharging(true)
                    .setTriggerContentMaxDelay(1000 * 1, TimeUnit.MILLISECONDS)
                    .build()

            val UploadPicWork =
                OneTimeWorkRequestBuilder<UploadPicWork>()
                    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10 * 1000, TimeUnit.MICROSECONDS)//10s 后失敗重試
                    .setInputData(workDataOf("params_tag" to "params"))
                    .setConstraints(triggerContentMaxDelay)
                    .addTag("tag")
                    .build()
                    
            WorkManager.getInstance(this).enqueue(UploadPicWork)


...

class UploadPicWork(
    private val context: Context,
    private val workerParameters: WorkerParameters
) :
    Worker(context, workerParameters) {

    private var count: Int = 1

    override fun doWork(): Result {
        
        uploadPic()

        return Result.retry()//失敗重試狀態(tài)
    }

    private fun uploadPic() {
        SystemClock.sleep(1000 * 3)//模擬圖片上傳
    }
}


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鸳惯,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子绪商,更是在濱河造成了極大的恐慌辅鲸,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件锣尉,死亡現(xiàn)場離奇詭異决采,居然都是意外死亡,警方通過查閱死者的電腦和手機拇厢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門旺嬉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來厨埋,“玉大人捐顷,你說我怎么就攤上這事》显蓿” “怎么了叮姑?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵传透,是天一觀的道長。 經(jīng)常有香客問我群嗤,道長兵琳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任者春,我火速辦了婚禮碧查,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘传惠。我一直安慰自己稻扬,他們只是感情好,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布盼砍。 她就那樣靜靜地躺著逝她,像睡著了一般黔宛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上觉渴,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天徽惋,我揣著相機與錄音险绘,去河邊找鬼。 笑死瓣距,一個胖子當著我的面吹牛渺氧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播白华,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼贩耐,長吁一口氣:“原來是場噩夢啊……” “哼潮太!你這毒婦竟也來了虾攻?” 一聲冷哼從身側(cè)響起更鲁,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤澡为,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后顶别,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拒啰,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡谋旦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了赴叹。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片指蚜。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡摊鸡,死狀恐怖蚕冬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情猎提,我是刑警寧澤旁蔼,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布棺聊,位于F島的核電站,受9級特大地震影響葵诈,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜理疙,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一泞坦、第九天 我趴在偏房一處隱蔽的房頂上張望暇矫。 院中可真熱鬧,春花似錦槽奕、人聲如沸房轿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纷妆。三九已至,卻和暖如春逊拍,著一層夾襖步出監(jiān)牢的瞬間际邻,已是汗流浹背世曾。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留骗露,地道東北人蕊程。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓藻茂,卻偏偏與公主長得像玫恳,于是被迫代替她去往敵國和親优俘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

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