JetPack DataStore 源碼解析

DataStore 是一種數(shù)據(jù)存儲(chǔ)解決方案,使用協(xié)議緩沖區(qū)存儲(chǔ)鍵值對(duì)或類(lèi)型化對(duì)象骑歹。DataStore 使用 Kotlin 協(xié)程和 Flow 以異步预烙、一致的事務(wù)方式存儲(chǔ)數(shù)據(jù)。

如果您當(dāng)前在使用SharedPreferences 存儲(chǔ)數(shù)據(jù)道媚,請(qǐng)考慮遷移到 DataStore扁掸。

Preferences DataStore 和 Proto DataStore

  • Preferences DataStore 使用鍵存儲(chǔ)和訪(fǎng)問(wèn)數(shù)據(jù)。此實(shí)現(xiàn)不需要預(yù)定義的架構(gòu)最域,也不確保類(lèi)型安全谴分。
  • Proto DataStore 將數(shù)據(jù)作為自定義數(shù)據(jù)類(lèi)型的實(shí)例進(jìn)行存儲(chǔ)。此實(shí)現(xiàn)要求您使用協(xié)議緩沖區(qū)來(lái)定義架構(gòu)羡宙,但可以確保類(lèi)型安全狸剃。

基本使用

引入

implementation("androidx.datastore:datastore:1.0.0")
implementation("androidx.datastore:datastore-preferences:1.0.0")

Preferences DataStore 的使用

(1)創(chuàng)建datasource實(shí)例

val dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

datasouce會(huì)將數(shù)據(jù)保存在內(nèi)部存儲(chǔ)的以下目錄

[圖片上傳失敗...(image-caad2-1638167023752)]

其中,settings為上邊name中設(shè)置的值狗热,后綴名是preferences_pb文件

注意,在實(shí)際的開(kāi)發(fā)中虑省,建議將上述的dataStore設(shè)置為單例模式匿刮。

(2)寫(xiě)數(shù)據(jù)

datastore 使用edit方法以異步的方式保存數(shù)據(jù)。

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
}

其中EXAMPLE_COUNTER 為key值探颈,并且必須是一個(gè)Preferences.Key熟丸,datastore目前支持以下幾種類(lèi)型的key:

  • intPreferencesKey

  • doublePreferencesKey

  • stringPreferencesKey

  • booleanPreferencesKey

  • floatPreferencesKey

  • longPreferencesKey

(3)讀數(shù)據(jù)

datastore以flow的方式觀(guān)察數(shù)據(jù)的變化。

val exampleCounterFlow: Flow<Int> = dataStore.data
    .map { preferences ->
        preferences[EXAMPLE_COUNTER] ?: 0
    }

GlobalScope.launch {
    exampleCounterFlow.collectLatest {
        Log.i(TAG, "read value:$it")
    }
}

Proto DataStore 的使用

Proto DataStore 用于保存實(shí)例對(duì)象伪节,使用之前需要先了解Proto協(xié)議及在A(yíng)ndroid下的基本使用方式光羞,這里不再贅述。
Proto DataStore 的使用和 Preferences DataStore基本類(lèi)似怀大,在寫(xiě)入數(shù)據(jù)時(shí)使用updateData提交數(shù)據(jù)

dataStore.updateData { settings ->
    settings.toBuilder()
        .setExampleCounter(settings.exampleCounter + 1)
        .setId(currentSettings.id + 1)
        .build()
}

讀取方式和Preferences DataStore一樣這里也不再贅述纱兑。

使用方式就先介紹到這里,下面來(lái)分析下DataStore的源碼

源碼分析

我們以Preferences DataStore 的讀寫(xiě)為例化借,Proto DataStore的過(guò)程類(lèi)似Preferences DataStore潜慎。

先從datastore的實(shí)例化入手

val dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

dataStore初始化以委托的方式調(diào)用 preferencesDataStore 函數(shù) 返回一個(gè) DataStore<Preferences>實(shí)例

我們來(lái)看preferencesDataStore的實(shí)現(xiàn)

public fun preferencesDataStore(
    name: String,
    corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
    produceMigrations: (Context) -> List<DataMigration<Preferences>> = { listOf() },
    scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): ReadOnlyProperty<Context, DataStore<Preferences>> {
    return PreferenceDataStoreSingletonDelegate(name, corruptionHandler, produceMigrations, scope)
}

preferencesDataStore 函數(shù)中,

  • name 上邊解釋過(guò)蓖康,就是保存的文件的名字

  • corruptionHandler 中包含了讀寫(xiě)失敗時(shí)的錯(cuò)誤铐炫。

  • produceMigrations 用于數(shù)據(jù)遷移,當(dāng)我們需要從sp中將數(shù)據(jù)遷移到datastore時(shí)需要此參數(shù)蒜焊。

  • scope 指定了線(xiàn)程調(diào)度器倒信,默認(rèn)是在IO線(xiàn)程中。

preferencesDataStore 函數(shù)返回了一個(gè)PreferenceDataStoreSingletonDelegate的實(shí)例泳梆,我們來(lái)看PreferenceDataStoreSingletonDelegate的具體實(shí)現(xiàn)

internal class PreferenceDataStoreSingletonDelegate internal constructor(
    private val name: String,
    private val corruptionHandler: ReplaceFileCorruptionHandler<Preferences>?,
    private val produceMigrations: (Context) -> List<DataMigration<Preferences>>,
    private val scope: CoroutineScope
) : ReadOnlyProperty<Context, DataStore<Preferences>> {

    private val lock = Any()

    @GuardedBy("lock")
    @Volatile
    private var INSTANCE: DataStore<Preferences>? = null

    /**
     * Gets the instance of the DataStore.
     *
     * @param thisRef must be an instance of [Context]
     * @param property not used
     */
    override fun getValue(thisRef: Context, property: KProperty<*>): DataStore<Preferences> {
        return INSTANCE ?: synchronized(lock) {
            if (INSTANCE == null) {
                val applicationContext = thisRef.applicationContext

                INSTANCE = PreferenceDataStoreFactory.create(
                    corruptionHandler = corruptionHandler,
                    migrations = produceMigrations(applicationContext),
                    scope = scope
                ) {
                    applicationContext.preferencesDataStoreFile(name)
                }
            }
            INSTANCE!!
        }
    }
}

代碼很容易理解鳖悠,PreferenceDataStoreSingletonDelegate 實(shí)現(xiàn)了 ReadOnlyProperty 接口榜掌,在ReadOnlyProperty接口中 有一個(gè)重載的getValue方法

public fun interface ReadOnlyProperty<in T, out V> {
    public operator fun getValue(thisRef: T, property: KProperty<*>): V
}

PreferenceDataStoreSingletonDelegate 中g(shù)etValue方法返回了一個(gè)DataStore<Preferences>的單例對(duì)象。

INSTANCE通過(guò) PreferenceDataStoreFactory.create 函數(shù)創(chuàng)建竞穷,我們先來(lái)看這句

applicationContext.preferencesDataStoreFile(name)

preferencesDataStoreFile 是context的一個(gè)擴(kuò)展唐责,需要我們前邊傳入的name參數(shù),猜測(cè)這里是設(shè)置保存文件路徑瘾带。

public fun Context.preferencesDataStoreFile(name: String): File =
    this.dataStoreFile("$name.preferences_pb")
    
public fun Context.dataStoreFile(fileName: String): File =
    File(applicationContext.filesDir, "datastore/$fileName")

preferencesDataStoreFile 進(jìn)一步調(diào)用了dataStoreFile函數(shù)鼠哥,dataStoreFile函數(shù)中設(shè)置了保存文件的具體路徑,

到這里我們知道了看政,DataSource是將數(shù)據(jù)保存在了

data/data/包名/files/datastore/xxx.preferences_pb

文件中朴恳。我們回到PreferenceDataStoreSingletonDelegate類(lèi)中,繼續(xù)看 INSTANCE的創(chuàng)建過(guò)程允蚣。

PreferenceDataStoreFactory.kt

    public fun create(
        corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
        migrations: List<DataMigration<Preferences>> = listOf(),
        scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
        produceFile: () -> File
    ): DataStore<Preferences> {
        val delegate = DataStoreFactory.create(
            serializer = PreferencesSerializer,
            corruptionHandler = corruptionHandler,
            migrations = migrations,
            scope = scope
        ) {
            val file = produceFile()
            check(file.extension == PreferencesSerializer.fileExtension) {
                "File extension for file: $file does not match required extension for" +
                    " Preferences file: ${PreferencesSerializer.fileExtension}"
            }
            file
        }
        return PreferenceDataStore(delegate)
    }
}

PreferenceDataStoreFactory的create方法沒(méi)有太多邏輯于颖,先是繼續(xù)調(diào)用DataStoreFactory.create方法,返回一個(gè)DataStore<T>的代理嚷兔,同時(shí)森渐,檢查創(chuàng)建的文件名稱(chēng)合法性。最后返回一個(gè)PreferenceDataStore實(shí)例冒晰。

我們先看DataStoreFactory.create方法

    public fun <T> create(
        serializer: Serializer<T>,
        corruptionHandler: ReplaceFileCorruptionHandler<T>? = null,
        migrations: List<DataMigration<T>> = listOf(),
        scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
        produceFile: () -> File
    ): DataStore<T> =
        SingleProcessDataStore(
            produceFile = produceFile,
            serializer = serializer,
            corruptionHandler = corruptionHandler ?: NoOpCorruptionHandler(),
            initTasksList = listOf(DataMigrationInitializer.getInitializer(migrations)),
            scope = scope
        )
}

我們先看DataStoreFactory 從名稱(chēng)上看像是一個(gè)工廠(chǎng)類(lèi)同衣,其實(shí)他只是個(gè)單例對(duì)象,create方法直接返回了一個(gè)
SingleProcessDataStore的實(shí)例壶运。

SingleProcessDataStore是最后真正的datastore實(shí)例化的類(lèi)DataStore的讀寫(xiě)關(guān)鍵邏輯也是在這里實(shí)現(xiàn)耐齐。

到這里,datastore的實(shí)例化基本分析完畢蒋情,下面來(lái)看數(shù)據(jù)的讀寫(xiě)過(guò)程埠况。

寫(xiě)

datastore通過(guò)edit方式實(shí)現(xiàn)數(shù)據(jù)的更新

dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
}

在edit函數(shù)的實(shí)現(xiàn)中,直接調(diào)用了datastore的updateData函數(shù)棵癣,


public suspend fun DataStore<Preferences>.edit(
    transform: suspend (MutablePreferences) -> Unit
): Preferences {
    return this.updateData {
        // It's safe to return MutablePreferences since we freeze it in
        // PreferencesDataStore.updateData()
        it.toMutablePreferences().apply { transform(this) }
    }
}

在上面datastore的實(shí)例化時(shí)我們知道辕翰,datastore對(duì)象實(shí)際上是一個(gè)SingleProcessDataStore的實(shí)例化對(duì)象,那我們直接看SingleProcessDataStore 中的updateData方法:

override suspend fun updateData(transform: suspend (t: T) -> T): T {

    val ack = CompletableDeferred<T>()
    val currentDownStreamFlowState = downstreamFlow.value

    val updateMsg =
        Message.Update(transform, ack, currentDownStreamFlowState, coroutineContext)

    actor.offer(updateMsg)

    return ack.await()
}

updateData 函數(shù)利用了協(xié)程中的處理并發(fā)時(shí)的 Actors解決方案浙巫,如果你對(duì)Actors不了解可以看下這里https://www.kotlincn.net/docs/reference/coroutines/shared-mutable-state-and-concurrency.html 金蜀,總之在這里只要記住這是為了處理寫(xiě)同步的操作即可。

actor.offer(updateMsg) 這句將任務(wù)提交到actor中的畴,updateMsg 中攜帶了數(shù)據(jù)及狀態(tài)信息渊抄,我們來(lái)看actor中實(shí)現(xiàn)

private val actor = SimpleActor<Message<T>>(
    scope = scope,
    onComplete = {
        it?.let {
            downstreamFlow.value = Final(it)
        }
        // We expect it to always be non-null but we will leave the alternative as a no-op
        // just in case.

        synchronized(activeFilesLock) {
            activeFiles.remove(file.absolutePath)
        }
    },
    onUndeliveredElement = { msg, ex ->
        if (msg is Message.Update) {
            // TODO(rohitsat): should we instead use scope.ensureActive() to get the original
            //  cancellation cause? Should we instead have something like
            //  UndeliveredElementException?
            msg.ack.completeExceptionally(
                ex ?: CancellationException(
                    "DataStore scope was cancelled before updateData could complete"
                )
            )
        }
    }
) { msg ->
    when (msg) {
        is Message.Read -> {
            handleRead(msg)
        }
        is Message.Update -> {
            handleUpdate(msg)
        }
    }
}

actor 根據(jù)消息類(lèi)型,進(jìn)行讀寫(xiě)操作丧裁,由于上一步我們傳遞的是Message.Update類(lèi)型护桦,因此會(huì)調(diào)用handleUpdate(msg)函數(shù),我們繼續(xù)進(jìn)入handleUpdate(msg)函數(shù)中

private suspend fun handleUpdate(update: Message.Update<T>) {
    // All branches of this *must* complete ack either successfully or exceptionally.
    // We must *not* throw an exception, just propagate it to the ack.
    update.ack.completeWith(
        runCatching {

            when (val currentState = downstreamFlow.value) {
                is Data -> {
                    // We are already initialized, we just need to perform the update
                    transformAndWrite(update.transform, update.callerContext)
                }
                is ReadException, is UnInitialized -> {
                    if (currentState === update.lastState) {
                        // we need to try to read again
                        readAndInitOrPropagateAndThrowFailure()

                        // We've successfully read, now we need to perform the update
                        transformAndWrite(update.transform, update.callerContext)
                    } else {
                        // Someone else beat us to read but also failed. We just need to
                        // signal the writer that is waiting on ack.
                        // This cast is safe because we can't be in the UnInitialized
                        // state if the state has changed.
                        throw (currentState as ReadException).readException
                    }
                }

                is Final -> throw currentState.finalException // won't happen
            }
        }
    )
}

我們先看主線(xiàn)邏輯煎娇,在handleUpdate中 根據(jù)狀態(tài)類(lèi)型進(jìn)一步跳轉(zhuǎn)二庵,如果我們上來(lái)就執(zhí)行寫(xiě)操作贪染,downstreamFlow.value的初始狀態(tài)是UnInitialized ,那么會(huì)執(zhí)行這里的邏輯

if (currentState === update.lastState) {
    // we need to try to read again
    readAndInitOrPropagateAndThrowFailure()

    // We've successfully read, now we need to perform the update
    transformAndWrite(update.transform, update.callerContext)
}

我們看在進(jìn)一步看transformAndWrite 方法

private suspend fun transformAndWrite(
    transform: suspend (t: T) -> T,
    callerContext: CoroutineContext
): T {
    // value is not null or an exception because we must have the value set by now so this cast
    // is safe.
    val curDataAndHash = downstreamFlow.value as Data<T>
    curDataAndHash.checkHashCode()

    val curData = curDataAndHash.value
    val newData = withContext(callerContext) { transform(curData) }

    // Check that curData has not changed...
    curDataAndHash.checkHashCode()

    return if (curData == newData) {
        curData
    } else {
        writeData(newData)
        downstreamFlow.value = Data(newData, newData.hashCode())
        newData
    }
}

關(guān)鍵邏輯在這里

return if (curData == newData) {
    curData
} else {
    writeData(newData)
    downstreamFlow.value = Data(newData, newData.hashCode())
    newData
}

如果要更新的值和當(dāng)前值相等催享,就不再繼續(xù)執(zhí)行杭隙,否則,執(zhí)行writeData 然后更新downstreamFlow.value的狀態(tài)為Data因妙,繼續(xù)看writeData 函數(shù)

internal suspend fun writeData(newData: T) {
    file.createParentDirectories()

    val scratchFile = File(file.absolutePath + SCRATCH_SUFFIX)
    try {
        FileOutputStream(scratchFile).use { stream ->
            serializer.writeTo(newData, UncloseableOutputStream(stream))
            stream.fd.sync()
            // TODO(b/151635324): fsync the directory, otherwise a badly timed crash could
            //  result in reverting to a previous state.
        }

        if (!scratchFile.renameTo(file)) {
            throw IOException(
                "Unable to rename $scratchFile." +
                    "This likely means that there are multiple instances of DataStore " +
                    "for this file. Ensure that you are only creating a single instance of " +
                    "datastore for this file."
            )
        }
    } catch (ex: IOException) {
        if (scratchFile.exists()) {
            scratchFile.delete() // Swallow failure to delete
        }
        throw ex
    }
}

這里真正執(zhí)行了數(shù)據(jù)的保存操作痰憎,而且是先將數(shù)據(jù)寫(xiě)到了一個(gè).tmp的臨時(shí)文件中,然后調(diào)用

scratchFile.renameTo(file)

將scratchFile文件重命名為file文件攀涵。到這里數(shù)據(jù)就保存成功了铣耘。

這里有兩個(gè)地方需要注意下

 stream.fd.sync()

(1)這句代碼是通過(guò)文件描述符刷新數(shù)據(jù),執(zhí)行這句之后以故,內(nèi)存中的數(shù)據(jù)會(huì)立即同步到文件中蜗细,這是linux的機(jī)制,知道即可怒详。

scratchFile.renameTo(file)

(2)renameTo方法是將文件重命名炉媒,測(cè)試時(shí)發(fā)現(xiàn)在A(yíng)ndroid平臺(tái)下不論目標(biāo)文件是否存在,均會(huì)執(zhí)行成功昆烁,除非scratchFile 不存在橱野,這可能和Java中的不一致,具體還需要看下renameTo的源碼善玫。

我們?cè)賮?lái)看下數(shù)據(jù)的讀取過(guò)程。

在上面的示例中我們知道 讀的過(guò)程就是觀(guān)察dataStore.data流的過(guò)程密强,我們繼續(xù)查看SingleProcessDataStore相關(guān)代碼

override val data: Flow<T> = flow {
    val currentDownStreamFlowState = downstreamFlow.value
    if (currentDownStreamFlowState !is Data) {
        // We need to send a read request because we don't have data yet.
        actor.offer(Message.Read(currentDownStreamFlowState))
    }
    emitAll(
        downstreamFlow.dropWhile {
            if (currentDownStreamFlowState is Data<T> ||
                currentDownStreamFlowState is Final<T>
            ) {
                false
            } else {
                it === currentDownStreamFlowState
            }
        }.map {
            when (it) {
                is ReadException<T> -> throw it.readException
                is Final<T> -> throw it.finalException
                is Data<T> -> it.value
                is UnInitialized -> error(
                    "This is a bug in DataStore. Please file a bug at: " +
                        "https://issuetracker.google.com/issues/new?" +
                        "component=907884&template=1466542"
                )
            }
        }
    )
}

如果讀之前沒(méi)有寫(xiě)操作或者第一次會(huì)先執(zhí)行

if (currentDownStreamFlowState !is Data) {
    // We need to send a read request because we don't have data yet.
    actor.offer(Message.Read(currentDownStreamFlowState))
}

downstreamFlow.value的初始值為*UnInitialized

但是如果之前有過(guò)寫(xiě)操作茅郎,就可以直接從緩存中讀取最新值,因?yàn)樵趯?xiě)完時(shí)downstreamFlow.value中保存了最新值

private suspend fun transformAndWrite(
    transform: suspend (t: T) -> T,
    callerContext: CoroutineContext
): T {
    ...
    return if (curData == newData) {
        curData
    } else {
        writeData(newData)
        downstreamFlow.value = Data(newData, newData.hashCode())
        ...
    }
}

我們來(lái)看下actor中執(zhí)行讀的相關(guān)操作或渤,直接查看handlRead函數(shù)

private suspend fun handleRead(read: Message.Read<T>) {
    when (val currentState = downstreamFlow.value) {
        ...
        UnInitialized -> {
            readAndInitOrPropagateFailure()
        }
        ...
    }
}

在進(jìn)入readAndInitOrPropagateFailure函數(shù)中

private suspend fun readAndInitOrPropagateFailure() {
    try {
        readAndInit()
    } catch (throwable: Throwable) {
        downstreamFlow.value = ReadException(throwable)
    }
}

在readAndInit繼續(xù)調(diào)用 readDataOrHandleCorruption執(zhí)行讀操作

var initData = readDataOrHandleCorruption()
private suspend fun readDataOrHandleCorruption(): T {
    try {
        return readData()
    } catch (ex: CorruptionException) {
    ...
    }
}

最后在readData中執(zhí)行了真正的讀操作系冗。

private suspend fun readData(): T {
    try {
        FileInputStream(file).use { stream ->
            return serializer.readFrom(stream)
        }
    } catch (ex: FileNotFoundException) {
        if (file.exists()) {
            throw ex
        }
        return serializer.defaultValue
    }
}

在獲取到數(shù)據(jù)后downstreamFlow.value變?yōu)镈ata

private suspend fun readAndInit() {
    ...
    downstreamFlow.value = Data(initData, initData.hashCode())
}

最后通過(guò)emitAll發(fā)射出去

emitAll(
    downstreamFlow.dropWhile {
        ...
    }.map {
        when (it) {
            ...
            is Data<T> -> it.value
            ...
        }
    }
)

以上就是 DataStore讀寫(xiě)的主要流程,涉及到的其他細(xì)節(jié)由于篇幅原因這里就不展開(kāi)了薪鹦,感興趣的小伙伴們可以自己閱讀掌敬。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市池磁,隨后出現(xiàn)的幾起案子奔害,更是在濱河造成了極大的恐慌,老刑警劉巖地熄,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件华临,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡端考,警方通過(guò)查閱死者的電腦和手機(jī)雅潭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)揭厚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人扶供,你說(shuō)我怎么就攤上這事筛圆。” “怎么了椿浓?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵太援,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我轰绵,道長(zhǎng)粉寞,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任左腔,我火速辦了婚禮唧垦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘液样。我一直安慰自己振亮,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布鞭莽。 她就那樣靜靜地躺著坊秸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪澎怒。 梳的紋絲不亂的頭發(fā)上褒搔,一...
    開(kāi)封第一講書(shū)人閱讀 51,624評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音喷面,去河邊找鬼星瘾。 笑死,一個(gè)胖子當(dāng)著我的面吹牛惧辈,可吹牛的內(nèi)容都是我干的琳状。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼盒齿,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼念逞!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起边翁,我...
    開(kāi)封第一講書(shū)人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤翎承,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后倒彰,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體审洞,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了芒澜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片仰剿。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖痴晦,靈堂內(nèi)的尸體忽然破棺而出南吮,到底是詐尸還是另有隱情,我是刑警寧澤誊酌,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布部凑,位于F島的核電站,受9級(jí)特大地震影響碧浊,放射性物質(zhì)發(fā)生泄漏涂邀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一箱锐、第九天 我趴在偏房一處隱蔽的房頂上張望比勉。 院中可真熱鬧,春花似錦驹止、人聲如沸芭届。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)镶骗。三九已至呀狼,卻和暖如春李请,著一層夾襖步出監(jiān)牢的瞬間匾寝,已是汗流浹背咏窿。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留撤卢,地道東北人践樱。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像凸丸,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子袱院,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

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