Kotlin 學習筆記(六)—— Flow 數據流學習實踐指北(二)StateFlow 與 SharedFlow

要說最近圈內大事件,那就非 chatGPT 莫屬了!人工智能領域最新的大突破了吧?很可能引發下一場的技術革命,因為大家都懂的原因現在還不能在中國大陸使用,不過國內的度廠正在積極跟進了,預計3月份能面世,且期待一下吧~

上節主要講述了 Flow 的組成、Flow 常用操作符以及冷流的具體使用。這節自然就要介紹熱流了。先來溫習下:

冷流(Cold Flow):在數據被消費者訂閱后,即調用 collect 方法之后,生產者才開始執行發送數據流的代碼,通常是調用 emit 方法。即不消費,不生產,多次消費才會多次生產。消費者和生產者是一對一的關系。

上次說的例子不太直觀,所以這次換了個更直觀的對比例子,先來看第一個:

//code 1
val coldFlow = flow {
    println("coldFlow begin emitting")
    emit(40)
    println("coldFlow 40 is emitted")
    emit(50)
    println("coldFlow 50 is emitted")
}
binding.btn2.setOnClickListener {
    lifecycleScope.launch {
        coldFlow.collect {
            println("coldFlow = $it")
        }
    }
}

只有當點擊按鈕時,才會如圖打印出信息,即冷流只有調用了 collect 方法收集流后,emit 才會開始執行。

圖1 冷流特點日志圖

熱流(Hot Flow)就不一樣了,無論有無消費者,生產者都會生產數據。它不像冷流,Flow 必須在調用末端操作符之后才會去執行;而是可以自己控制是否發送或者生產數據流。并且熱流可以有多個訂閱者;而冷流只有一個。再來看看熱流的例子:

//code 2
val hotFlow = MutableStateFlow(0)
lifecycleScope.launch {
    println("hotFlow begin emitting")
    hotFlow.emit(40)
    println("hotFlow 40 is emitted")

    hotFlow.emit(50)
    println("hotFlow 50 is emitted")
}
binding.btn2.setOnClickListener {
    lifecycleScope.launch {
        hotFlow.collect {
            println("hotFlow collects $it")
        }
    }
}

MutableStateFlow 就是熱流中的一種,當沒有點擊按鈕時,便會輸出下圖中的前三行信息。


圖2 熱流特點日志圖

當點擊兩下按鈕后,就會依次輸出如圖第 4,5 行的信息,至于為什么只會接收到 50,這跟 MutableStateFlow 的特性有關,后面再說。

通過這兩個例子就可清楚地知道冷熱流之間的區別。熱流有兩種對象,分別是 StateFlow 和 SharedFlow。

1. SharedFlow

先來看看 SharedFlow,它是一個 subscriber 訂閱者的角色,當一個 SharedFlow 調用了 collect 方法后,它就不會正常地結束完成;但可以 cancel 掉 collect 所在的協程,這樣就可以取消掉訂閱了。SharedFlow 在每次 emit 時都會去 check 一下所在協程是否已經取消。絕大多數的終端操作符,例如 Flow.toList() 都不會使得 SharedFlow 結束完成,但 Flow.take() 之類的截斷操作符是例外,它們是可以強制完成一個 SharedFlow 的。

SharedFlow 的簡單使用樣例:

//code 3
class EventBus {
    private val _events = MutableSharedFlow<Event>() // private mutable shared flow
    val events = _events.asSharedFlow() // publicly exposed as read-only shared flow

    suspend fun produceEvent(event: Event) {
        _events.emit(event) // suspends until all subscribers receive it
    }
}

與 LiveData 相似的使用方式。但 SharedFlow 的功能更為強大,它有 replay cache 和 buffer 機制。

1.1 Replay cache

可以理解為是一個粘性事件的緩存。每個新的訂閱者會首先收到 replay cache 中之前發出并接收到的事件,再才會收到新的發射出的值。可以在 MutableSharedFlow 的構造函數中設置 cache 的大小,不能為負數,默認為 0.

//code 4
public fun <T> MutableSharedFlow(
    replay: Int = 0,
    extraBufferCapacity: Int = 0,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
)

replay 重播之前最新的 n 個事件,見字知義。下面是例子:

//code 5
private fun testSharedFlow() {
    val sharedFlow = MutableSharedFlow<Int>(replay = 2)
    lifecycleScope.launch {
        launch {
            sharedFlow.collect {
                println("++++ sharedFlow1 collected $it")
            }
        }

        launch {
            (1..3).forEach{
                sharedFlow.emit(it)
            }
        }

        delay(200)
        launch {
            sharedFlow.collect {
                println("++++ sharedFlow2 collected $it")
            }
        }
    }
}

結果為:

com.example.myapplication I/System.out: ++++ sharedFlow1 collected 1
com.example.myapplication I/System.out: ++++ sharedFlow1 collected 2
com.example.myapplication I/System.out: ++++ sharedFlow1 collected 3
com.example.myapplication I/System.out: ++++ sharedFlow2 collected 2
com.example.myapplication I/System.out: ++++ sharedFlow2 collected 3

emit 發射數據前后分別設置了一個訂閱者,后面還延時了 200ms 才進行訂閱。第一個訂閱者 1、2、3都收到了;而第二個訂閱者卻只收到了 2 和 3. 這是因為在第二個訂閱者開始訂閱時,數據已經都發射完了,而 SharedFlow 的重播 replay 為 2,就可將最近發射的兩個數據再依次發送一遍,這就可以收到 2 和 3 了。

1.2 extraBufferCapacity

SharedFlow 構造函數的第二個參數 extraBufferCapacity 的作用是,在 replay cache 之外還能額外設置的緩存。常用于當生產者生產數據的速度 > 消費者消費數據的速度時的情況,可以有效提升吞吐量。

所以,若 replay = m,extraBufferCapacity = n,那么這個 SharedFlow 總共的 BufferSize = m + n. replay 會存儲最近發射的數據,如果滿了就會往 extraBuffer 中存。接下來看一個例子:

//code 6
private fun coroutineStudy() {
    val sharedFlow = MutableSharedFlow<Int>(replay = 1, extraBufferCapacity = 1)
    lifecycleScope.launch {
        launch {
            sharedFlow.collect {
                println("++++ sharedFlow1 collected $it")
                delay(6000)
            }
        }

        launch {
            (1..4).forEach{
                sharedFlow.emit(it)
                println("+++emit $it")
                delay(1000)
            }
        }

        delay(4000)
        launch {
            sharedFlow.collect {
                println("++++ sharedFlow2 collected $it")
                delay(20000)
            }
        }
    }
}

運行結果為:

17:32:09.283 28184-28184 System.out com.wen.testdemo I  +++emit 1
17:32:09.284 28184-28184 System.out com.wen.testdemo I  ++++ sharedFlow1 collected 1
17:32:10.285 28184-28184 System.out com.wen.testdemo I  +++emit 2
17:32:11.289 28184-28184 System.out com.wen.testdemo I  +++emit 3
17:32:13.286 28184-28184 System.out com.wen.testdemo I  ++++ sharedFlow2 collected 3
17:32:15.292 28184-28184 System.out com.wen.testdemo I  +++emit 4
17:32:15.293 28184-28184 System.out com.wen.testdemo I  ++++ sharedFlow1 collected 2
17:32:21.301 28184-28184 System.out com.wen.testdemo I  ++++ sharedFlow1 collected 3
17:32:27.311 28184-28184 System.out com.wen.testdemo I  ++++ sharedFlow1 collected 4
17:32:33.292 28184-28184 System.out com.wen.testdemo I  ++++ sharedFlow2 collected 4

打印結果可能會有點懵,對照著時序圖更容易理解(此圖來自于參考文獻3,感謝 fundroid 大佬的輸出~):


圖 3

1)Emitter 發送 1,因為 Subscriber1 在 Emitter 發送數據前就已開始訂閱,所以 Subscriber1 可馬上接收;此時 replay 存儲 1;
2)Emitter 發送 2,Subscriber1 還在處理中處于掛起態,此時 replay 存儲 2;
3)Emitter 發送 3,此時還沒有任何消費者能消費,則 replay 存儲 3,將 2 放入 extra 中;
4)Emitter 想要發送 4,但發現 SharedFlow 的 Buffer 已滿,則按照默認的策略進行掛起等待(默認策略就是 onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND);
5)Subscriber2 開始訂閱,接收到 replay 中的 3,此時 Subscriber1 還是掛起態,Buffer 中數據沒變化,即 replay 存儲 3,extra 存儲 2;
6)Subscriber1 處理完 1 后,依次處理 Buffer 中 的下一個數據,即消費 extra 中的 2,這時 Buffer 終于有空間了,Emitter 結束掛起,發送 4,replay 存儲 4,將 3 放入 extra 中;
7)Subscriber1 消費完 2 后接著再消費 extra 中的 3,此時 Buffer 中就只有 4 了。后面的就不用多說了

比較繞,需要多看幾次思考一下。需要注意的是,代碼運行結果中下面兩行輸出到底誰先誰后的問題:

17:32:15.292 28184-28184 System.out com.wen.testdemo I  +++emit 4
17:32:15.293 28184-28184 System.out com.wen.testdemo I  ++++ sharedFlow1 collected 2

打印出的時間戳幾乎是一樣的,若嚴格按照 log 打印的時間戳順序,應該是 Emitter 先發送的 4,Subscriber1 再才接收到的 2,但根據反復實踐的結果來看,實際上是 Subscriber1 先接收緩沖區中的 2,等緩沖區有剩余空間后,Emitter 才結束掛起繼續發送 4. 把上面的例子簡化一下,再改改數據:

//code 7
private fun coroutineStudy() {
    val sharedFlow = MutableSharedFlow<Int>(replay = 1, extraBufferCapacity = 1)
    lifecycleScope.launch {
        launch {
            sharedFlow.collect {
                println("++++ sharedFlow1 collected $it")
                delay(10000)
            }
        }

        launch {
            (1..4).forEach{
                sharedFlow.emit(it)
                println("+++emit $it")
                delay(1000)
            }
        }
    }
}

打印結果如下所示,因為把 sharedFlow delay 的時長設置為 10s,所以很明顯地看到 Emitter 在發送 1、2、3 時時間間隔均是 1s,發送 4 時足足過了 8s,這段時間就是 Emitter 被掛起了,一直等到 sharedFlow1 接收到 2 之后,4 才被 Emitter 發送,而 sharedFlow1 的每次接收都是間隔 10s,所以是先接收的 2,再結束掛起發送的 4.

00:25:52.481 29483-29483/com.example.myapplication I/System.out: +++emit 1
00:25:52.482 29483-29483/com.example.myapplication I/System.out: ++++ sharedFlow1 collected 1
00:25:53.483 29483-29483/com.example.myapplication I/System.out: +++emit 2
00:25:54.486 29483-29483/com.example.myapplication I/System.out: +++emit 3
00:26:02.487 29483-29483/com.example.myapplication I/System.out: +++emit 4
00:26:02.488 29483-29483/com.example.myapplication I/System.out: ++++ sharedFlow1 collected 2
00:26:12.497 29483-29483/com.example.myapplication I/System.out: ++++ sharedFlow1 collected 3
00:26:22.516 29483-29483/com.example.myapplication I/System.out: ++++ sharedFlow1 collected 4

通過源碼也可看出這個結論,從 collect 方法進入,最終可以找到實際上是調用了 SharedFlowImpl 中的 collect 方法:

//code 8
    override suspend fun collect(collector: FlowCollector<T>) {
        val slot = allocateSlot()
        try {
            if (collector is SubscribedFlowCollector) collector.onSubscription()
            val collectorJob = currentCoroutineContext()[Job]
            while (true) {
                var newValue: Any?
                while (true) {
                    newValue = tryTakeValue(slot) //首先嘗試直接獲取值
                    if (newValue !== NO_VALUE) break
                    awaitValue(slot) //沒獲取到則只能掛起等待新值到來
                }
                collectorJob?.ensureActive()
                collector.emit(newValue as T)
            }
        } finally {
            freeSlot(slot)
        }
    }

在內層 while 循環中,首先是通過 tryTakeValue 方法直接取值,如果沒取到則通過 awaitValue 方法掛起等待新值,awaitValue 是個掛起函數。取到新值之后,才會跳出內層 while 循環,并執行 collector.emit(newValue as T),而這一段代碼,實際上就是調用的 code 7 中的 sharedFlow.emit(it) 代碼。

此處源代碼還可以看出,SharedFlow 每次在 emit 之前,確實都會查看所在協程是否還在運行;且它確實是不會停止的,哪怕沒有接收到新值,也會一直處于掛起等待的狀態,想要結束則得使用截斷類型的操作符。

1.3 onBufferOverflow

SharedFlow 構造函數的第三個參數就是設置超過 Buffer 之后的策略,默認是將生產者掛起暫時不再發送數據,即 BufferOverflow.SUSPEND。

還有另外兩個數據丟棄策略:
1)BufferOverflow.DROP_LATEST 丟棄最新數據;


圖 4

Emitter 在發送 4 時,因為 Buffer 已滿,所以只能按照策略將最新的數據 4 丟棄。而在發送 3 時,由于 1 已經被消費過,所以可以從 Buffer 中移除,從而騰出存儲空間緩存 3。

2)BufferOverflow.DROP_OLDEST 丟棄最老數據:


圖 5

這個策略就比較簡單,Buffer 中只會存儲最新的數據。不管較老的數據是否被消費,當 Buffer 已滿而又有新的數據到達時,老數據都會從 Buffer 中移除,騰出空間讓給新數據。

注意點:當 replay、extra 都為 0,即沒有 Buffer 的時候,那么 onBufferOverflow 只能是 BufferOverflow.SUSPEND。丟棄策略啟動的前提是 SharedFlow 至少有 Buffer 且 Buffer 已滿。

1.4 emit 與 tryEmit

由前一節可知,當 SharedFlow 的 Buffer 已滿且 onBufferOverflow 為 BufferOverflow.SUSPEND 的時候,emit 會被掛起(emit 是個掛起函數),但這會影響到 Emitter 的速度。如果不想在發送數據的時候被掛起,除了設置 onBufferOverflow 丟棄策略外,還可以使用 tryEmit 方法。

//code 9
    override fun tryEmit(value: T): Boolean {
        var resumes: Array<Continuation<Unit>?> = EMPTY_RESUMES
        val emitted = synchronized(this) {
            if (tryEmitLocked(value)) {
                resumes = findSlotsToResumeLocked(resumes)
                true
            } else {
                false
            }
        }
        for (cont in resumes) cont?.resume(Unit)
        return emitted
    }

    @Suppress("UNCHECKED_CAST")
    private fun tryEmitLocked(value: T): Boolean {
        // Fast path without collectors -> no buffering
        // 1.沒有訂閱者時,直接返回 true,因為沒有人接收,發了也沒用,也不用緩存
        if (nCollectors == 0) return tryEmitNoCollectorsLocked(value) // always returns true
        // With collectors we'll have to buffer
        // 2.有訂閱者,就得考慮緩存發送的值了
        // cannot emit now if buffer is full & blocked by slow collectors
        // 3.如果緩存空間已滿,且訂閱者還在掛起處理上次的數據,則不能 emit
        if (bufferSize >= bufferCapacity && minCollectorIndex <= replayIndex) {
            when (onBufferOverflow) {
                BufferOverflow.SUSPEND -> return false // will suspend
                BufferOverflow.DROP_LATEST -> return true // just drop incoming
                BufferOverflow.DROP_OLDEST -> {} // force enqueue & drop oldest instead
            }
        }
        // 4.代碼能走到這里,說明緩存還有空間或丟棄策略為DROP_OLDEST
        enqueueLocked(value)
        bufferSize++ // value was added to buffer
        // drop oldest from the buffer if it became more than bufferCapacity
        if (bufferSize > bufferCapacity) dropOldestLocked()
        // keep replaySize not larger that needed
        if (replaySize > replay) { // increment replayIndex by one
            updateBufferLocked(replayIndex + 1, minCollectorIndex, bufferEndIndex, queueEndIndex)
        }
        return true
    }

由代碼可見 tryEmit 不是一個掛起函數,它有返回值,如果返回 true 則說明發送數據成功了;如果返回 false,則說明這時發送數據需要被掛起等待。其中最主要的就是 tryEmitLocked 方法。

tryEmitLocked 方法主要邏輯已在注釋中說明,需要額外說明的是,bufferCapacity 就是 replay + extraBufferCapacity 的大小;replayIndex 指的是最近開始訂閱的訂閱者在 replay cache 緩存數組中需要重播的最小 index。所以當使用默認構造的 SharedFlow 時,replayextraBufferCapacity 都為 0,如果這時再使用 tryEmit 方法進行發送,則會使得 if (bufferSize >= bufferCapacity && minCollectorIndex <= replayIndex) 判斷為 true,默認的丟棄策略又是 BufferOverflow.SUSPEND,就會導致這里會直接返回 false,永遠都不會發送出值。所以,在使用默認構造的 SharedFlow 時,不能使用 tryEmit 發送值,否則無法發送。 一般使用 emit 即可。

在 SharedFlow 具體實現中,emit 方法就是先嘗試使用 tryEmit 來發送值,如果不能馬上發送再使用掛起函數 emitSuspend 方法:

//code 10    class SharedFlowImpl
    override suspend fun emit(value: T) {
        if (tryEmit(value)) return // fast-path
        emitSuspend(value)
    }

2. StateFlow

看完 SharedFlow 再來看 StateFlow 的話就比較簡單了。因為 StateFlow 就是 SharedFlow 的一種特殊子類,特點有三:
1)它的 replay cache 容量為 1;即可緩存最近的一次粘性事件;
2)初始化時必須給它設置一個初始值;
3)每次發送數據都會與上次緩存的數據作比較,如果不一樣才會發送,自動過濾掉沒有發生變化的數據。
它還可直接訪問它自己的 value 參數獲取當前結果值,總體來說,在使用上與 LiveData 相似,下面是它倆的異同點對比。

2.1 與 LiveData 比較的相同點

  1. 均提供了 可讀可寫 和 僅可讀 兩個版本:MutableStateFlow、StateFlow 與 MutableLiveData、LiveData;
  2. 允許被多個觀察者觀察,即生產者對消費者可以為一對多的關系;
  3. 都只會把最新的值給到觀察者,即使沒有觀察者,也會更新自己的值;
  4. 都會產生粘性事件問題;
  5. 都可能產生丟失值的問題;

粘性事件問題:因為 StateFlow 初始化時必須給定初始值,且 replay 為 1,所以每個觀察者進行觀察時,都會收到最近一次的回播數據。如果想避免粘性事件問題,換用 SharedFlow 即可,replay 使用默認值 0 。

值丟失問題:出現在消費者處理數據比生產者生產數據慢的情況,消費者來不及處理數據,就會把之前生產者發送的舊數據丟棄掉,看個例子:

//code 11
    private fun stateFlowDemo1() {
        val stateFlow = MutableStateFlow(0)
        CoroutineScope(Dispatchers.Default).launch {
            var count = 1
            while (true) {
                val tmp = count++
                delay(1000)
                println("+++++ tmp = $tmp")
                stateFlow.value = tmp
            }
        }

        CoroutineScope(Dispatchers.Default).launch {
            stateFlow.collect{
                println("++++ count = $it")
                delay(5000)  //模擬耗時操作
            }
        }
    }
圖 6 StateFlow丟失值log

可以從打印結果看出,StateFlow 會丟棄掉生產者之前發送的值,其實 MutableStateFlow 的丟棄策略就是設置的 BufferOverflow.DROP_OLDEST。

2.2 與 LiveData 比較的不同點

  1. StateFlow 必須在構建的時候傳入初始值,LiveData 不需要;
  2. StateFlow 默認是防抖的,LiveData 默認不防抖;
  3. 對于 Android 來說 StateFlow 默認沒有和生命周期綁定,直接使用會有問題;

StateFlow 默認防抖:即如果發送的值與上次相同,則生產者并不會真正發送。在源碼中也有說明,具體在 StateFlow.kt -> class StateFlowImpl -> private fun updateState -> if (oldState == newState) return true
感興趣的可以自行查閱,我看的版本是 1.5.0.

與 LiveData 相比,沒有和 Activity 的生命周期綁定恐怕是使用 StateFlow 最不方便的地方了。當 View 進入 STOPPED 狀態時,LiveData.observe() 會自動取消注冊使用方,這樣就不會再接收到數據了,也符合常理。因為用戶此時已經離開頁面,再接收數據已沒有意義,如果繼續處理后續邏輯可能還會出 bug。

而如果使用的是 StateFlow 或其他數據流,在 View 進入 STOPPED 狀態時,收集數據的操作并不會自動停止。如需實現相同的行為,則需要從 Lifecycle.repeatOnLifecycle 塊收集數據流。如下是來自官方文檔的例子:

//code 12
class LatestNewsActivity : AppCompatActivity() {
    private val latestNewsViewModel = // getViewModel()
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // Start a coroutine in the lifecycle scope
        lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // Note that this happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                latestNewsViewModel.uiState.collect { uiState ->
                    // New value received
                    when (uiState) {
                        is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
                        is LatestNewsUiState.Error -> showError(uiState.exception)
                    }
                }
            }
        }
    }
}
//注意:repeatOnLifecycle API 僅在 androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 庫及更高版本中提供。

英文部分注釋說的比較明確了,repeatOnLifecycle(Lifecycle.State.STARTED) 的作用就是每次進入 STARTED 可見狀態時都會重新觀察并收集數據;而在 STOPPED 狀態時就會 cancel 掉 StateFlow 收集流所在的協程從而停止收集。

總結

最后總結一下 Flow 第二小節的內容吧:
1)熱流有無消費者都可發送數據,生產者和消費者的關系可以是一對多;
2)SharedFlow 可構建熱流,可設置 replay 重播數據量及 extraBufferCapacity 緩沖區大小,以及 onBufferOverflow 緩沖區滿的策略;
3)emittryEmit 發送方法的異同,前者是掛起函數,注意在使用默認構造的 SharedFlow 時不要使用 tryEmit
4)StateFlow 是 SharedFlow 的一個子類,replay = 1,必須給定初始值,自帶防抖;
5)使用 StateFlow 或 SharedFlow 收集值時,記得在 repeatOnLifecycle(Lifecycle.State.STARTED) 方法中,防止出現崩潰等問題。

更多內容,歡迎查看專輯:修之竹公眾號 Android 專輯

贊人玫瑰,手留余香!歡迎點贊、轉發~ 轉發請注明出處~

參考文獻

  1. Reactive Streams on Kotlin: SharedFlow and StateFlow; Ricardo Costeira; https://www.raywenderlich.com/22030171-reactive-streams-on-kotlin-sharedflow-and-stateflow
  2. Kotlin中 Flow、SharedFlow與StateFlow區別;五問;https://juejin.cn/post/7142038525997744141
  3. 一看就懂!圖解 Kotlin SharedFlow 緩存系統;fundroid;https://juejin.cn/post/7156408785886511111
  4. Kotlin:深入理解StateFlow與SharedFlow,StateFlow和LiveData使用差異區分,SharedFlow實現源碼解析; pumpkin的玄學; https://blog.csdn.net/weixin_44235109/article/details/121594988?spm=1001.2014.3001.5502
  5. StateFlow 和 SharedFlow 官方文檔 https://developer.android.google.cn/kotlin/flow/stateflow-and-sharedflow?hl=zh-cn
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內容