WorkManager

本篇文章完全轉(zhuǎn)載于微笑的江豚 的博客地址:

https://my.oschina.net/JiangTun
如有問題,請及時(shí)聯(lián)系!

前言

以前我們在處理后臺(tái)任務(wù)時(shí),都是使Service(含IntentService)或線程、線程池。而Service不受頁面生命周期影響,可以常駐后臺(tái),所以很適合做一些定時(shí)、延時(shí)任務(wù),或者其他肉眼看不到的神秘勾當(dāng)。在處理一些復(fù)雜需求時(shí),比如監(jiān)聽網(wǎng)絡(luò)環(huán)境自動(dòng)暫停重啟后臺(tái)上傳下載這類任務(wù)時(shí),我們用Service結(jié)合Broadcast一起來做,非常麻煩,再加上傳輸進(jìn)度的回調(diào),更是讓人抓狂。

大量的后臺(tái)任務(wù)過度的消耗了設(shè)備的電量,比如多種第三方推送的Service都常駐后臺(tái),不良APP后臺(tái)自動(dòng)上傳用戶隱私帶來了隱私安全問題。

谷歌專項(xiàng)整頓

  • 6.0 (API 級(jí) 23) 引入了 Doze 機(jī)制和應(yīng)用程序待機(jī)。當(dāng)屏幕關(guān)閉且設(shè)備靜止時(shí), 打盹模式會(huì)限制應(yīng)用程序的行為。應(yīng)用程序待機(jī)將未使用的應(yīng)用程序置于限制其網(wǎng)絡(luò)訪問、作業(yè)和同步的特殊狀態(tài)。
  • Android 7.0 (API 級(jí) 24) 有限的隱性廣播和 Doze-on-the-go.
  • Android 8.0 (API 級(jí) 26) 進(jìn)一步限制了后臺(tái)行為, 例如在后臺(tái)獲取位置并釋放緩存的 wakelocks。

尤其在Android O(8.0)中,谷歌對于后臺(tái)的限制幾乎可以稱之為變態(tài):

  • Android 8.0 有一項(xiàng)復(fù)雜功能,系統(tǒng)不允許后臺(tái)應(yīng)用創(chuàng)建后臺(tái)服務(wù)。 因此,Android 8.0 引入了一種全新的方法,即 Context.startForegroundService(),以在前臺(tái)啟動(dòng)新服務(wù)。 在系統(tǒng)創(chuàng)建服務(wù)后,應(yīng)用有五秒的時(shí)間來調(diào)用該服務(wù)的 startForeground() 方法以顯示新服務(wù)的用戶可見通知。 如果應(yīng)用在此時(shí)間限制內(nèi)未調(diào)用 startForeground(),則系統(tǒng)將停止服務(wù)并聲明此應(yīng)用為 ANR。

而且加入了對靜態(tài)廣播的限制:

  • Android 8.0 讓這些限制更為嚴(yán)格。 針對 Android 8.0 的應(yīng)用無法繼續(xù)在其清單中為隱式廣播注冊廣播接收器。 隱式廣播是一種不專門針對該應(yīng)用的廣播。 例如,ACTION_PACKAGE_REPLACED 就是一種隱式廣播,因?yàn)樗鼘l(fā)送到注冊的所有偵聽器,讓后者知道設(shè)備上的某些軟件包已被替換。 不過,ACTION_MY_PACKAGE_REPLACED 不是隱式廣播,因?yàn)椴还芤褳樵搹V播注冊偵聽器的其他應(yīng)用有多少,它都會(huì)只發(fā)送到軟件包已被替換的應(yīng)用。 應(yīng)用可以繼續(xù)在它們的清單中注冊顯式廣播。 應(yīng)用可以在運(yùn)行時(shí)使用 Context.registerReceiver() 為任意廣播(不管是隱式還是顯式)注冊接收器。 需要簽名權(quán)限的廣播不受此限制所限,因?yàn)檫@些廣播只會(huì)發(fā)送到使用相同證書簽名的應(yīng)用,而不是發(fā)送到設(shè)備上的所有應(yīng)用。 在許多情況下,之前注冊隱式廣播的應(yīng)用使用 JobScheduler 作業(yè)可以獲得類似的功能。

于此同時(shí),官方推薦用5.0推出的 JobScheduler 替換 Service + Broadcast 的方案。并且在 Android O,后臺(tái) Service 啟動(dòng)后的5秒內(nèi),如果不轉(zhuǎn)為前臺(tái) Service 就會(huì) ANR!

官方的推薦(qiangzhi)做法

場景 推薦
需系統(tǒng)觸發(fā),不必完成 ThreadPool + Broadcast
需系統(tǒng)觸發(fā),必須完成,可推遲 WorkManager
需系統(tǒng)觸發(fā),必須完成,立即 ForegroundService + Broadcast
不需系統(tǒng)觸發(fā),不必完成 ThreadPool
不需系統(tǒng)觸發(fā),必須完成,可推遲 WorkManager
不需系統(tǒng)觸發(fā),必須完成,立即 ForegroundService

WorkManager的推出

WorkManager 是一個(gè) Android 庫, 它在工作的觸發(fā)器 (如適當(dāng)?shù)木W(wǎng)絡(luò)狀態(tài)和電池條件) 滿足時(shí), 優(yōu)雅地運(yùn)行可推遲的后臺(tái)工作。WorkManager 盡可能使用框架 JobScheduler , 以幫助優(yōu)化電池壽命和批處理作業(yè)。在 Android 6.0 (API 級(jí) 23) 下面的設(shè)備上, 如果 WorkManager 已經(jīng)包含了應(yīng)用程序的依賴項(xiàng), 則嘗試使用 Firebase JobDispatcher 。否則, WorkManager 返回到自定義 AlarmManager 實(shí)現(xiàn), 以優(yōu)雅地處理您的后臺(tái)工作。

也就是說,WorkManager 可以自動(dòng)維護(hù)后臺(tái)任務(wù),同時(shí)可適應(yīng)不同的條件,同時(shí)滿足后臺(tái)Service 和靜態(tài)廣播,內(nèi)部維護(hù)著 JobScheduler,而在6.0以下系統(tǒng)版本則可自動(dòng)切換為AlarmManager,Amazing!

WorkManager詳解

引入
implementation "android.arch.work:work-runtime:1.0.0-alpha06" // use -ktx for Kotlin
implementation "android.arch.work:work-runtime:1.0.0-alpha01"
重要的解析類
  • worker
    Worker 是一個(gè)抽象類,用來指定需要執(zhí)行的具體任務(wù)。我們需要繼承 Worker 類,并實(shí)現(xiàn)它的 doWork 方法:
class MyWorker:Worker() {

    val tag = javaClass.simpleName

   override fun getExtras(): Extras {
       return Extras(...) //也可以把參數(shù)寫死在這里
   }

   override fun onStopped(cancelled: Boolean) {
       super.onStopped(cancelled)
       //當(dāng)任務(wù)結(jié)束時(shí)會(huì)回調(diào)這里
       ...
   }

    override fun doWork(): Result {

        Log.d(tag,"任務(wù)執(zhí)行完畢!")
        return Worker.Result.SUCCESS
    }
}
向任務(wù)添加參數(shù)

在Request中傳參

val data=Data.Builder()
        .putInt("A",1)
        .putString("B","2")
        .build()
val request2 = PeriodicWorkRequestBuilder<MyWorker>(24,TimeUnit.SECONDS)
        .setInputData(data)
        .build()

在 Worker 中使用

class MyWorker:Worker() {

    val tag = javaClass.simpleName

    override fun doWork(): Result {

        val A = inputData.getInt("A",0)
        val B = inputData.getString("B")
        return Worker.Result.SUCCESS
    }
}

當(dāng)然除了上述代碼中的方法之外,我們也可以重寫父級(jí)的getExtras(),并在此方法中把參數(shù)寫死再返回也是可以的。

這里WorkManager就有一個(gè)不是很人性的地方了,那就是WorkManager不支持序列化傳值!這一點(diǎn)讓我怎么說啊,intent和Bundle都支持序列化傳值,為什么偏偏這貨就不行?那么如果傳一個(gè)復(fù)雜對象還要先拆解嗎?

任務(wù)的返回值

很類似很類似的,任務(wù)的返回值也很簡單:

override fun doWork(): Result {

    val A = inputData.getInt("A",0)
    val B = inputData.getString("B")

    val data = Data.Builder()
            .putBoolean("C",true)
            .putFloat("D",0f)
            .build()
    outputData = data//返回值
    return Worker.Result.SUCCESS
}

doWork 要求最后返回一個(gè) Result,這個(gè) Result 是一個(gè)枚舉,它有幾個(gè)固定的值:

  • FAILURE 任務(wù)失敗。
  • RETRY 遇到暫時(shí)性失敗,此時(shí)可使用WorkRequest.Builder.setBackoffCriteria(BackoffPolicy, long, TimeUnit)來重試。
  • SUCCESS 任務(wù)成功。

看到這里我就很奇怪,官方不推薦我們使用枚舉,但是自己卻一直在用,什么意思?

WorkRequest

也是一個(gè)抽象類,可以對 Work 進(jìn)行包裝,同時(shí)裝裱上一系列的約束(Constraints),這些 Constraints 用來向系統(tǒng)指明什么條件下,或者什么時(shí)候開始執(zhí)行任務(wù)。

WorkManager 向我們提供了 WorkRequest 的兩個(gè)子類:

  • OneTimeWorkRequest 單次任務(wù)。

  • PeriodicWorkRequest 周期任務(wù)。

val request1 = PeriodicWorkRequestBuilder<MyWorker>(60,TimeUnit.SECONDS).build()

val request2 = OneTimeWorkRequestBuilder<MyWorker>().build()

從代碼中可以看到,我們應(yīng)該使用不同的構(gòu)造器來創(chuàng)建對應(yīng)的 WorkRequest。

接下來我們看看都有哪些約束:

  • public boolean requiresBatteryNotLow ():執(zhí)行任務(wù)時(shí)電池電量不能偏低。

  • public boolean requiresCharging ():在設(shè)備充電時(shí)才能執(zhí)行任務(wù)。

  • public boolean requiresDeviceIdle ():設(shè)備空閑時(shí)才能執(zhí)行。

  • public boolean requiresStorageNotLow ():設(shè)備儲(chǔ)存空間足夠時(shí)才能執(zhí)行。

addContentUriTrigger
@RequiresApi(24)
public @NonNull Builder addContentUriTrigger(Uri uri, boolean triggerForDescendants)

指定是否在(Uri 指定的)內(nèi)容更新時(shí)執(zhí)行本次任務(wù)(只能用于 Api24及以上版本)。瞄了一眼源碼發(fā)現(xiàn)了一個(gè) ContentUriTriggers,這什么東東?

public final class ContentUriTriggers implements Iterable<ContentUriTriggers.Trigger> {

    private final Set<Trigger> mTriggers = new HashSet<>();
    ...

public static final class Trigger {
        private final @NonNull Uri mUri;
        private final boolean mTriggerForDescendants;

        Trigger(@NonNull Uri uri, boolean triggerForDescendants) {
            mUri = uri;
            mTriggerForDescendants = triggerForDescendants;
        }

特么驚呆了,居然是個(gè)HashSet,而HashSet的核心是個(gè)HashMap啊,谷歌聲明不建議用HashMap,當(dāng)然也就不建議用HashSet,可是官方自己在背地里面干的這些勾當(dāng)啊...

setRequiredNetworkType
public void setRequiredNetworkType (NetworkType requiredNetworkType)

指定任務(wù)執(zhí)行時(shí)的網(wǎng)絡(luò)狀態(tài)。其中狀態(tài)見下表:

枚舉 狀態(tài)
NOT_REQUIRED 不需要網(wǎng)絡(luò)
CONNECTED 任何可用網(wǎng)絡(luò)
UNMETERED 需要不計(jì)量網(wǎng)絡(luò),如 WiFi
NOT_ROAMING 需要非漫游網(wǎng)絡(luò)
METERED 需要計(jì)量網(wǎng)絡(luò),如4G
setRequiresBatteryNotLow
public void setRequiresBatteryNotLow (boolean requiresBatteryNotLow)

指定設(shè)備電池電量低于閥值時(shí)是否啟動(dòng)任務(wù),默認(rèn) false。

setRequiresCharging
public void setRequiresCharging (boolean requiresCharging)

指定設(shè)備在充電時(shí)是否啟動(dòng)任務(wù)。

setRequiresDeviceIdle
public void setRequiresDeviceIdle (boolean requiresDeviceIdle)

指明設(shè)備是否為空閑時(shí)是否啟動(dòng)任務(wù)

setRequiresStorageNotLow
public void setRequiresStorageNotLow (boolean requiresStorageNotLow)

指明設(shè)備儲(chǔ)存空間低于閥值時(shí)是否啟動(dòng)任務(wù)。給任務(wù)加約束:

val myConstraints = Constraints.Builder()
        .setRequiresDeviceIdle(true)//指定{@link WorkRequest}運(yùn)行時(shí)設(shè)備是否為空閑
        .setRequiresCharging(true)//指定要運(yùn)行的{@link WorkRequest}是否應(yīng)該插入設(shè)備
        .setRequiredNetworkType(NetworkType.NOT_ROAMING)
        .setRequiresBatteryNotLow(true)//指定設(shè)備電池是否不應(yīng)低于臨界閾值
        .setRequiresCharging(true)//網(wǎng)絡(luò)狀態(tài)
        .setRequiresDeviceIdle(true)//指定{@link WorkRequest}運(yùn)行時(shí)設(shè)備是否為空閑
        .setRequiresStorageNotLow(true)//指定設(shè)備可用存儲(chǔ)是否不應(yīng)低于臨界閾值
        .addContentUriTrigger(myUri,false)//指定內(nèi)容{@link android.net.Uri}時(shí)是否應(yīng)該運(yùn)行{@link WorkRequest}更新
        .build()
val request = PeriodicWorkRequestBuilder<MyWorker>(24,TimeUnit.SECONDS)
        .setConstraints(myConstraints)//注意看這里!!!
        .build()

給任務(wù)加標(biāo)簽分組

val request1 = OneTimeWorkRequestBuilder<MyWorker>()
                .addTag("A")//標(biāo)簽
                .build()
val request2 = OneTimeWorkRequestBuilder<MyWorker>()
                .addTag("A")//標(biāo)簽
                .build()

上述代碼我給兩個(gè)相同任務(wù)的request都加上了標(biāo)簽,使他們成為了一個(gè)組:A組。這樣的好處是以后可以直接控制整個(gè)組就行了,組內(nèi)的每個(gè)成員都會(huì)受到影響。

WorkManager

經(jīng)過上面的操作,相信我們已經(jīng)能夠成功創(chuàng)建 request 了,接下來我們就需要把任務(wù)放進(jìn)任務(wù)隊(duì)列,我們使用 WorkManager。

WorkManager 是個(gè)單例,它負(fù)責(zé)調(diào)度任務(wù)并且監(jiān)聽任務(wù)狀態(tài)。

WorkManager.getInstance().enqueue(request)

當(dāng)我們的 request 入列后,WorkManager 會(huì)給它分配一個(gè) work ID,之后我們可以使用這個(gè)work id 來取消或者停止任務(wù):

WorkManager.getInstance().cancelWorkById(request.id)

注意:WorkManager 并不一定能結(jié)束任務(wù),因?yàn)槿蝿?wù)有可能已經(jīng)執(zhí)行完畢了。
同時(shí),WorkManager 還提供了其他結(jié)束任務(wù)的方法:

  • cancelAllWork():取消所有任務(wù)。
  • cancelAllWorkByTag(tag:String):取消一組帶有相同標(biāo)簽的任務(wù)。
  • cancelUniqueWork(uniqueWorkName:String):取消唯一任務(wù)。
WorkStatus

當(dāng) WorkManager 把任務(wù)加入隊(duì)列后,會(huì)為每個(gè)WorkRequest對象提供一個(gè) LiveData(如果這個(gè)東東不了解的話趕緊去學(xué))。 LiveData 持有 WorkStatus;通過觀察該 LiveData, 我們可以確定任務(wù)的當(dāng)前狀態(tài), 并在任務(wù)完成后獲取所有返回的值。

val liveData: LiveData<WorkStatus> = WorkManager.getInstance().getStatusById(request.id)

我們來看這個(gè) WorkStatus 到底都包涵什么,我們點(diǎn)進(jìn)去看它的源碼:

public final class WorkStatus {    private @NonNull UUID mId;    private @NonNull State mState;    private @NonNull Data mOutputData;    private @NonNull Set<String> mTags;    public WorkStatus(
            @NonNull UUID id,
            @NonNull State state,
            @NonNull Data outputData,
            @NonNull List<String> tags) {
        mId = id;
        mState = state;
        mOutputData = outputData;
        mTags = new HashSet<>(tags);
    }

我們需要關(guān)注的只有 State 和 Data 這兩個(gè)屬性,首先看 State:

public enum State {

    ENQUEUED,//已加入隊(duì)列
    RUNNING,//運(yùn)行中
    SUCCEEDED,//已成功
    FAILED,//已失敗
    BLOCKED,//已刮起
    CANCELLED;//已取消

    public boolean isFinished() {        return (this == SUCCEEDED || this == FAILED || this == CANCELLED);
    }
}

這特么又一個(gè)枚舉。看過代碼之后,State 枚舉其實(shí)就是用來給我們做最后的結(jié)果判斷的。但是要注意其中有個(gè)已掛起 BLOCKED,這是啥子情況?通過看它的注釋,我們得知,如果 WorkRequest 的約束沒有通過,那么這個(gè)任務(wù)就會(huì)處于掛起狀態(tài)。

接下來,Data 當(dāng)然就是我們在任務(wù)中 doWork 的返回值了。看到這里,我感覺谷歌大佬的設(shè)計(jì)思維還是非常之強(qiáng)的,把狀態(tài)和返回值同時(shí)輸出,非常方便我們做判斷的同時(shí)來取值,并且這樣的設(shè)計(jì)就可以達(dá)到‘多次返回’的效果,有時(shí)間一定要去看一下源碼,先立個(gè) flag!

任務(wù)鏈

在很多場景中,我們需要把不同的任務(wù)弄成一個(gè)隊(duì)列,比如在用戶注冊的時(shí)候,我們要先驗(yàn)證手機(jī)短信驗(yàn)證碼,驗(yàn)證成功后再注冊,注冊成功后再調(diào)登陸接口實(shí)現(xiàn)自動(dòng)登陸。類似這樣相似的邏輯比比皆是,實(shí)話說筆者以前都是在 service 里面用 rxjava 來實(shí)現(xiàn)的。但是現(xiàn)在 service 在 Android8.0版本以上系統(tǒng)不能用了怎么辦?當(dāng)然還是用我們今天學(xué)到的 WorkManager 來實(shí)現(xiàn),接下來我們就一起看一下 WorkManager 的任務(wù)鏈。

  • 鏈?zhǔn)絾?dòng)-并發(fā)
val request1 = OneTimeWorkRequestBuilder<MyWorker1>().build()
val request2 = OneTimeWorkRequestBuilder<MyWorker2>().build()
val request3 = OneTimeWorkRequestBuilder<MyWorker3>().build()

WorkManager.getInstance().beginWith(request1,request2,request3)
.enqueue()

這樣等同于 WorkManager 把一個(gè)個(gè)的 WorkRequest enqueue 進(jìn)隊(duì)列,但是這樣寫明顯更整齊!同時(shí)隊(duì)列中的任務(wù)是并行的。

  • then 操作符-串發(fā)
val request1 = OneTimeWorkRequestBuilder<MyWorker>().build()
val request2 = OneTimeWorkRequestBuilder<MyWorker>().build()
val request3 = OneTimeWorkRequestBuilder<MyWorker>().build()

WorkManager.getInstance().beginWith(request1)
        .then(request2)
        .then(request3)
        .enqueue()

上述代碼的意思就是先1,1成功后再2,2成功后再3,這期間如果有任何一個(gè)任務(wù)失敗(返回 Worker.WorkerResult.FAILURE),則整個(gè)隊(duì)列就會(huì)被中斷。

在任務(wù)鏈的串行中,也就是兩個(gè)任務(wù)使用了 then 操作符連接,那么上一個(gè)任務(wù)的返回值就會(huì)自動(dòng)轉(zhuǎn)為下一個(gè)任務(wù)的參數(shù)!

  • combine 操作符-組合
    現(xiàn)在我們有個(gè)復(fù)雜的需求:共有A、B、C、D、E這五個(gè)任務(wù),要求 AB 串行,CD 串行,但兩個(gè)串之間要并發(fā),并且最后要把兩個(gè)串的結(jié)果匯總到E。

我們看到這種復(fù)雜的業(yè)務(wù)邏輯,往往都會(huì)嚇一跳,但是牛X的谷歌提供了combine操作符專門應(yīng)對這種奇葩邏輯,不得不說:谷歌是我親哥!

val chuan1 = WorkManager.getInstance()
    .beginWith(A)
    .then(B)
val chuan2 = WorkManager.getInstance()
    .beginWith(C)
    .then(D)
WorkContinuation
    .combine(chuan1, chuan2)
    .then(E)
    .enqueue()
唯一鏈

什么是唯一鏈,就是同一時(shí)間內(nèi)隊(duì)列里不能存在相同名稱的任務(wù)。

val request = OneTimeWorkRequestBuilder<MyWorker>().build()

WorkManager.getInstance().beginUniqueWork("tag",ExistingWorkPolicy.REPLACE,request,request,request)

從上面代碼我們可以看到,首先與之前不同的是,這次我們用的是 beginUniqueWork 方法,這個(gè)方法的最后一個(gè)參數(shù)是一個(gè)可變長度的數(shù)組,那就證明這一定是一根鏈條。然后我們看這個(gè)方法的第一個(gè)參數(shù),要求輸入一個(gè)名稱,這個(gè)名稱就是用來標(biāo)識(shí)任務(wù)的唯一性。那如果兩個(gè)不同的任務(wù)我們給了相同的名稱也是可以的,但是這兩個(gè)任務(wù)在隊(duì)列中只能存活一個(gè)。最后我們再來看第二個(gè)參數(shù) ExistingWorkPolicy,點(diǎn)進(jìn)去果然又雙叒是枚舉:

public enum ExistingWorkPolicy {

    REPLACE,
    KEEP,
    APPEND
}
  • REPLACE:如果隊(duì)列里面已經(jīng)存在相同名稱的任務(wù),并且該任務(wù)處于掛起狀態(tài)則替換之。

  • KEEP:如果隊(duì)列里面已經(jīng)存在相同名稱的任務(wù),并且該任務(wù)處于掛起狀態(tài),則什么也不做。

  • APPEND:如果隊(duì)列里面已經(jīng)存在相同名稱的任務(wù),并且該任務(wù)處于掛起狀態(tài),則會(huì)緩存新任務(wù)。當(dāng)隊(duì)列中所有任務(wù)執(zhí)行完畢后,以這個(gè)新任務(wù)做為序列的第一個(gè)任務(wù)。

總結(jié)

看到這里相信大家對于 WorkManager 的基本用法已經(jīng)了解的差不多了吧!

另外通過這次對 WorkManager 的學(xué)習(xí),我們也看到官方在代碼里面也仍舊在用一些他自己不推薦使用的東西,比如 HashMap、HashSet、Enum 等,只許州官放火不許百姓點(diǎn)燈?這很谷歌!其實(shí)不是的,所謂萬事無絕對,只要你夠自信,自己做好取舍,掌握平衡,用什么還是由你自己做主!

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

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,591評(píng)論 25 707
  • 1、概述 在 I / O '18中,Google發(fā)布了Android Jetpack。它是一組庫,工具和架構(gòu)指南,...
    高丕基閱讀 7,518評(píng)論 1 12
  • 01 小狗錢錢引發(fā)的思考 群主大人分享小狗錢錢音頻,然后群里就熱鬧了起來,有的說“女兒學(xué)書法,鼓勵(lì)她教其他同學(xué),在...
    陳念媛閱讀 160評(píng)論 0 0
  • 一部豆瓣評(píng)分9.0的電影,又是徐崢主演的,沒有看影評(píng),直接沖著口碑去的。如此高的口碑和呼聲之下,究竟給我的感覺是震...
    檸綰綰閱讀 327評(píng)論 0 3