Kotlin+Coroutines(協(xié)程)+Retrofit+LiveData+Room安卓全局多文件斷點下載

引言

做這個起初的目的是為了學(xué)習(xí)Kotlin協(xié)程,以及JetPack中的相關(guān)組件,并且老項目準備重構(gòu),于是打算徹徹底底的進行換血.
其實中間寫了很多版,包括用RxJava也寫過,不過寫到一半兒,看了一篇Rxjava已過時的文章直接放棄了,其實我沒有在項目中"真正"用過Rxjava,正打算用,就看到了這種言論,不過說得其實也有道理,Rxjava的出現(xiàn)是當時Java環(huán)境的需要,但是Kotlin以及kotlin協(xié)程的穩(wěn)定可能真的是Rxjava消亡的契機吧.但是真正讓我放棄的是Rxjava的復(fù)雜性.Rxjava為了突顯鏈式調(diào)用,定義了很多云里霧里的函數(shù)(至少讓我云里霧里),我覺得很多函數(shù)是沒必要的或者說不應(yīng)該做為Rxjava標準庫的一部分,它擴展的功能太多了.
官方本身也為我們提供了一個用于管理下載的組件DownloadManager,但是它針對的是所有的Android app,所以它對app自身的切合度太低了,想要實現(xiàn)自定義上的操作,過于復(fù)雜了.如果我們要做一個擁有下載管理功能的app,那么這個功能還是應(yīng)該自己來實現(xiàn)的.
這個Demo本來就是在學(xué)習(xí)中誕生的,可能代碼中很多不足,或者出現(xiàn)了某些錯誤,還請各位大佬予以指正和提示

預(yù)覽

按鈕沒有做狀態(tài)選擇器,看不出來點擊步驟,抱歉

Screenrecorder-2020-04-01-10-33-40-139 00_00_00-00_00_30.gif

1 Room 用于存儲下載數(shù)據(jù)

Room想要配合協(xié)程使用,必須加入room-ktx依賴,導(dǎo)入room-ktx之后同時也會導(dǎo)入Kotlin協(xié)程相關(guān)的部分

implementation 'androidx.room:room-runtime:2.2.5'
implementation 'androidx.room:room-ktx:2.2.5'
kapt 'androidx.room:room-compiler:2.2.5'

同時需要加入對kotlin注解編譯器的支持

apply plugin: 'kotlin-kapt'

1.1 DownloadInfo用于保存下載數(shù)據(jù)的數(shù)據(jù)類

@Entity
@TypeConverters(Converters::class)
data class DownloadInfo(
    @PrimaryKey
    var url: String = "",
    var path: String? = null,
    var data: Serializable? = null,//跟下載相關(guān)的數(shù)據(jù)信息
    var fileName: String? = null,
    var contentLength: Long = -1,
    var currentLength: Long = 0,
    var status: Int = NONE,
    var lastRefreshTime: Long = 0
) {
    companion object Status {
        const val NONE = 0  //無狀態(tài)
        const val WAITING = 1 //等待中
        const val LOADING = 2 //下載中
        const val PAUSE = 3 //暫停
        const val ERROR = 4 //錯誤
        const val DONE = 5 //完成
    }

    /**
     * 重置任務(wù)
     */
    fun reset() {
        currentLength = 0
        contentLength = -1
        status = NONE
        lastRefreshTime = 0
    }
}

Serializable字段我們需要為其添加類型轉(zhuǎn)換器,詳情參考官網(wǎng)

class Converters {

    @TypeConverter
    fun toByteArray(serializable: Serializable?): ByteArray? {
        serializable ?: return null
        var byteArrayOutputStream: ByteArrayOutputStream? = null
        var objectOutputStream: ObjectOutputStream? = null
        try {
            byteArrayOutputStream = ByteArrayOutputStream()
            objectOutputStream = ObjectOutputStream(byteArrayOutputStream)
            objectOutputStream.writeObject(serializable)

            objectOutputStream.flush()
            return byteArrayOutputStream.toByteArray()
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            byteArrayOutputStream?.close()
            objectOutputStream?.close()
        }
        return null
    }

    @TypeConverter
    fun toSerializable(byteArray: ByteArray?): Serializable? {
        byteArray ?: return null
        var byteArrayOutputStream: ByteArrayInputStream? = null
        var objectInputStream: ObjectInputStream? = null
        try {
            byteArrayOutputStream = ByteArrayInputStream(byteArray)
            objectInputStream = ObjectInputStream(byteArrayOutputStream)
            return objectInputStream.readObject() as Serializable
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            byteArrayOutputStream?.close()
            objectInputStream?.close()
        }
        return null
    }
}

1.2 DownloadDao

定義了訪問下載數(shù)據(jù)的方法

@Dao
interface DownloadDao {

    /**
     * 獲取所有
     */
    @Query("select * from DownloadInfo")
    suspend fun queryAll(): MutableList<DownloadInfo>

    /**
     * 通過狀態(tài)查詢?nèi)蝿?wù)
     */
    @Query("select * from DownloadInfo where status =:status")
    suspend fun queryByStatus(status: Int): MutableList<DownloadInfo>

    /**
     * 查詢正在下載的任務(wù)
     */
    @Query("select * from DownloadInfo where status != ${DownloadInfo.DONE} and status != ${DownloadInfo.NONE}")
    suspend fun queryLoading(): MutableList<DownloadInfo>

    /**
     * 查詢正在下載的任務(wù)的url
     */
    @Query("select url from DownloadInfo where status != ${DownloadInfo.DONE}  and status != ${DownloadInfo.NONE}")
    suspend fun queryLoadingUrls(): MutableList<String>

    /**
     * 查詢下載完成的任務(wù)
     */
    @Query("select * from DownloadInfo where status == ${DownloadInfo.DONE}")
    suspend fun queryDone(): MutableList<DownloadInfo>

    /**
     * 查詢下載完成的任務(wù)的url
     */
    @Query("select url from DownloadInfo where status == ${DownloadInfo.DONE}")
    suspend fun queryDoneUrls(): MutableList<String>

    /**
     * 通過url查詢,每一個任務(wù)他們唯一的標志就是url
     */
    @Query("select * from DownloadInfo where url like:url")
    suspend fun queryByUrl(url: String): DownloadInfo?

    /**
     * 插入或替換
     */
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertOrReplace(vararg downloadData: DownloadInfo): List<Long>

    /**
     * 刪除
     */
    @Delete
    suspend fun delete(downloadDao: DownloadInfo)

}

1.3 AppDataBase

數(shù)據(jù)庫持有者

@Database(entities = [DownloadInfo::class], version = 1)
abstract class AppDataBase : RoomDatabase() {

    abstract fun downloadDao(): DownloadDao
}

1.4 RoomClient

構(gòu)建AppDataBase,請忽略createMigrations方法,這里暫時沒有數(shù)據(jù)庫升級的需求

object RoomClient {

    private const val DATA_BASE_NAME = "download.db"

    val dataBase: AppDataBase by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
        Room
            .databaseBuilder(
                App.instance.applicationContext,
                AppDataBase::class.java,
                DATA_BASE_NAME
            )
            .build()
    }

    private fun createMigrations(): Array<Migration> {
        return arrayOf()
    }

}

2 下載邏輯

下載邏輯包含以下幾部分

1.DownloadService
2.RetrofitDownload
3.DownloadScope
4.AppDownload

2.1 DownloadService

定義了斷點下載的方法,Retrofit2.6之后直接支持配合協(xié)程使用

interface DownloadService {

    @Streaming
    @GET
    suspend fun download(@Header("RANGE") start: String? = "0", @Url url: String?): Response<ResponseBody>
}

2.2 RetrofitDownload

構(gòu)建Retrofit,baseurl的填寫滿足http://或者https://開頭且后面有內(nèi)容就可以了

object RetrofitDownload {

    val downloadService: DownloadService by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
        val okHttpClient = createOkHttpClient()
        val retrofit = createRetrofit(okHttpClient)
        retrofit.create(DownloadService::class.java)
    }

    private fun createOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder().build()
    }

    private fun createRetrofit(client: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl("http://download")
            .client(client)
            .build()
    }
}

2.3 DownloadScope[核心]

代表一個下載任務(wù),實際的下載也在里面進行

/**
 * 代表一個下載任務(wù)
 * url將做為下載任務(wù)的唯一標識
 * 不要直接在外部直接創(chuàng)建此對象,那樣就可能無法同一管理下載任務(wù),請通過[AppDownload.request]獲取此對象
 */
class DownloadScope(
    var url: String,
    var path: String? = null,
    private val data: Serializable? = null
) : CoroutineScope by CoroutineScope(EmptyCoroutineContext) {

    private var downloadJob: Job? = null
    private val downloadData = MutableLiveData<DownloadInfo>()

    init {
        launch(Dispatchers.Main) {
            val downloadInfoDeferred = async(Dispatchers.IO) {
                RoomClient.dataBase.downloadDao().queryByUrl(url)
            }
            var downloadInfo = downloadInfoDeferred.await()
            //數(shù)據(jù)庫中并沒有任務(wù),這是一個新的下載任務(wù)
            if (downloadInfo == null)
                downloadInfo = DownloadInfo(url = url, path = path, data = data)
            //將原本正在下載中的任務(wù)恢復(fù)到暫停狀態(tài),防止意外退出出現(xiàn)的狀態(tài)錯誤
            if (downloadInfo.status == DownloadInfo.LOADING)
                downloadInfo.status = DownloadInfo.PAUSE
            downloadData.value = downloadInfo
        }
    }

    /**
     * 獲取[DownloadInfo]
     */
    fun downloadInfo(): DownloadInfo? {
        return downloadData.value
    }

    /**
     * 添加下載任務(wù)觀察者
     */
    fun observer(lifecycleOwner: LifecycleOwner, observer: Observer<DownloadInfo>) {
        downloadData.observe(lifecycleOwner, observer)
    }

    /**
     * 開始任務(wù)的下載
     * [DownloadInfo]是在協(xié)程中進行創(chuàng)建的,它的創(chuàng)建會優(yōu)先從數(shù)據(jù)庫中獲取,但這種操作是異步的,詳情請看init代碼塊
     * 我們需要通過觀察者觀察[DownloadInfo]來得知它是否已經(jīng)創(chuàng)建完成,只有當他創(chuàng)建完成且不為空(如果創(chuàng)建完成,它一定不為空)
     * 才可以交由[AppDownload]進行下載任務(wù)的啟動
     * 任務(wù)的開始可能并不是立即的,任務(wù)會受到[AppDownload]的管理
     */
    fun start() {
        var observer: Observer<DownloadInfo>? = null
        observer = Observer { downloadInfo ->
            downloadInfo?.let {
                observer?.let { downloadData.removeObserver(it) }
                when (downloadInfo.status) {
                    DownloadInfo.PAUSE, DownloadInfo.ERROR, DownloadInfo.NONE -> {
                        change(DownloadInfo.WAITING)
                        AppDownload.launchScope(this@DownloadScope)
                    }
                }
            }
        }
        downloadData.observeForever(observer)
    }

    /**
     * 啟動協(xié)程進行下載
     * 請不要嘗試在外部調(diào)用此方法,那樣會脫離[AppDownload]的管理
     */
    fun launch() = launch {
        try {
            download()
            change(DownloadInfo.DONE)
        } catch (e: Throwable) {
            Log.w("DownloadScope", "error:${e.message}")
            when (e) {
                !is CancellationException -> change(DownloadInfo.ERROR)
            }
        } finally {
            AppDownload.launchNext(url)
        }
    }.also { downloadJob = it }

    private suspend fun download() = withContext(context = Dispatchers.IO, block = {
        change(DownloadInfo.LOADING)
        val downloadInfo = downloadData.value
        downloadInfo ?: throw IOException("Download info is null")
        val startPosition = downloadInfo.currentLength
        //驗證斷點有效性
        if (startPosition < 0) throw IOException("Start position less than zero")
        //下載的文件是否已經(jīng)被刪除
        if (startPosition > 0 && !TextUtils.isEmpty(downloadInfo.path))
            if (!File(downloadInfo.path).exists()) throw IOException("File does not exist")
        val response = RetrofitDownload.downloadService.download(
            start = "bytes=$startPosition-",
            url = downloadInfo.url
        )
        val responseBody = response.body()
        responseBody ?: throw IOException("ResponseBody is null")
        //文件長度
        if (downloadInfo.contentLength < 0)
            downloadInfo.contentLength = responseBody.contentLength()
        //保存的文件名稱
        if (TextUtils.isEmpty(downloadInfo.fileName))
            downloadInfo.fileName = UrlUtils.getUrlFileName(downloadInfo.url)
        //創(chuàng)建File,如果已經(jīng)指定文件path,將會使用指定的path,如果沒有指定將會使用默認的下載目錄
        val file: File
        if (TextUtils.isEmpty(downloadInfo.path)) {
            file = File(AppDownload.downloadFolder, downloadInfo.fileName)
            downloadInfo.path = file.absolutePath
        } else file = File(downloadInfo.path)
        //再次驗證下載的文件是否已經(jīng)被刪除
        if (startPosition > 0 && !file.exists())
            throw IOException("File does not exist")
        //再次驗證斷點有效性
        if (startPosition > downloadInfo.contentLength)
            throw IOException("Start position greater than content length")
        //驗證下載完成的任務(wù)與實際文件的匹配度
        if (startPosition == downloadInfo.contentLength && startPosition > 0)
            if (file.exists() && startPosition == file.length()) {
                change(DownloadInfo.DONE)
                return@withContext
            } else throw IOException("The content length is not the same as the file length")
        //寫入文件
        val randomAccessFile = RandomAccessFile(file, "rw")
        randomAccessFile.seek(startPosition)
        downloadInfo.currentLength = startPosition
        val inputStream = responseBody.byteStream()
        val bufferSize = 1024 * 8
        val buffer = ByteArray(bufferSize)
        val bufferedInputStream = BufferedInputStream(inputStream, bufferSize)
        var readLength: Int
        try {
            while (bufferedInputStream.read(
                    buffer, 0, bufferSize
                ).also {
                    readLength = it
                } != -1 && downloadInfo.status == DownloadInfo.LOADING && isActive//isActive保證任務(wù)能被及時取消
            ) {
                randomAccessFile.write(buffer, 0, readLength)
                downloadInfo.currentLength += readLength
                val currentTime = System.currentTimeMillis()
                if (currentTime - downloadInfo.lastRefreshTime > 300) {
                    change(DownloadInfo.LOADING)
                    downloadInfo.lastRefreshTime = currentTime
                }
            }
        } finally {
            inputStream.close()
            randomAccessFile.close()
            bufferedInputStream.close()
        }
    })

    /**
     * 更新任務(wù)
     * @param status [DownloadInfo.Status]
     */
    private fun change(status: Int) = launch(Dispatchers.Main) {
        val downloadInfo = downloadData.value
        downloadInfo ?: return@launch
        downloadInfo.status = status
        withContext(Dispatchers.IO) {
            RoomClient.dataBase.downloadDao().insertOrReplace(downloadInfo)
        }
        downloadData.value = downloadInfo
    }

    /**
     * 暫停任務(wù)
     * 只有等待中的任務(wù)和正在下載中的任務(wù)才可以進行暫停操作
     */
    fun pause() {
        cancel(CancellationException("pause"))
        val downloadInfo = downloadData.value
        downloadInfo?.let {
            if (it.status == DownloadInfo.LOADING || it.status == DownloadInfo.WAITING)
                change(DownloadInfo.PAUSE)
        }
    }

    /**
     * 刪除任務(wù),刪除任務(wù)會同時刪除已經(jīng)在數(shù)據(jù)庫中保存的下載信息
     */
    fun remove() = launch(Dispatchers.Main) {
        this@DownloadScope.cancel(CancellationException("remove"))
        val downloadInfo = downloadData.value
        downloadInfo?.reset()
        downloadData.value = downloadInfo
        withContext(Dispatchers.IO) {
            downloadInfo?.let {
                RoomClient.dataBase.downloadDao().delete(it)
                //同時刪除已下載的文件
                it.path?.let { path ->
                    val file = File(path)
                    if (file.exists()) file.delete()
                }
            }
        }
    }

    /**
     * 取消[downloadJob],將會中斷正在進行的下載任務(wù)
     */
    private fun cancel(cause: CancellationException) {
        downloadJob?.cancel(cause)
    }

    /**
     * 是否是等待任務(wù)
     */
    fun isWaiting(): Boolean {
        val downloadInfo = downloadData.value
        downloadInfo ?: return false
        return downloadInfo.status == DownloadInfo.WAITING
    }

    /**
     * 是否是正在下載的任務(wù)
     */
    fun isLoading():Boolean {
        val downloadInfo = downloadData.value
        downloadInfo ?: return false
        return downloadInfo.status == DownloadInfo.LOADING
    }
}

2.4 AppDownload[核心]

對DownloadScope的管理,同時也是獲取DownloadScope的唯一途徑

object AppDownload {

    private const val MAX_SCOPE = 3

    val downloadFolder: String? by lazy {
        Environment.getExternalStorageState()
        App.instance.applicationContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
            ?.absolutePath
    }

    private val scopeMap: ConcurrentHashMap<String, DownloadScope> by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
        ConcurrentHashMap<String, DownloadScope>()
    }

    private val taskScopeMap: ConcurrentHashMap<String, DownloadScope> by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
        ConcurrentHashMap<String, DownloadScope>()
    }

    /**
     * 請求一個下載任務(wù)[DownloadScope]
     * 這是創(chuàng)建[DownloadScope]的唯一途徑,請不要通過其他方式創(chuàng)建[DownloadScope]
     * 首次任務(wù)調(diào)用此方法獲取[DownloadScope]并不會在數(shù)據(jù)庫中生成數(shù)據(jù)
     * 首次任務(wù)只有調(diào)用了[DownloadScope.start]并且成功進入[DownloadInfo.WAITING]狀態(tài)才會在數(shù)據(jù)庫中生成數(shù)據(jù)
     * 首次任務(wù)的判斷依據(jù)為數(shù)據(jù)庫中是否保留有當前的任務(wù)數(shù)據(jù)
     */
    fun request(url: String?, data: Serializable? = null, path: String? = null): DownloadScope? {
        if (TextUtils.isEmpty(url)) return null
        var downloadScope = scopeMap[url]
        if (downloadScope == null) {
            downloadScope = DownloadScope(url = url!!, data = data, path = path)
            scopeMap[url] = downloadScope
        }
        return downloadScope
    }

    /**
     * 通過url恢復(fù)任務(wù)
     *
     * @param urls 需要恢復(fù)任務(wù)的連接,url請通過DownloadDao進行獲取
     */
    fun restore(urls: List<String>): MutableList<DownloadScope> {
        val downloadScopes = mutableListOf<DownloadScope>()
        for (url in urls) {
            var downloadScope = scopeMap[url]
            if (downloadScope == null) {
                downloadScope = DownloadScope(url = url)
                scopeMap[url] = downloadScope
            }
            downloadScopes.add(downloadScope)
        }
        return downloadScopes
    }

    /**
     * 暫停所有的任務(wù)
     * 只有任務(wù)的狀態(tài)為[DownloadInfo.WAITING]和[DownloadInfo.LOADING]才可以被暫停
     * 暫停任務(wù)會先暫停[DownloadInfo.WAITING]的任務(wù)而后再暫停[DownloadInfo.LOADING]的任務(wù)
     */
    fun pauseAll() {
        for (entry in scopeMap) {
            val downloadScope = entry.value
            if (downloadScope.isWaiting())
                downloadScope.pause()
        }
        for (entry in scopeMap) {
            val downloadScope = entry.value
            if (downloadScope.isLoading())
                downloadScope.pause()
        }
    }

    /**
     * 移除所有的任務(wù)
     * 移除任務(wù)會先移除狀態(tài)不為[DownloadInfo.LOADING]的任務(wù)
     * 而后再移除狀態(tài)為[DownloadInfo.LOADING]的任務(wù)
     */
    fun removeAll() {
        for (entry in scopeMap) {
            val downloadScope = entry.value
            if (!downloadScope.isLoading())
                downloadScope.remove()
        }
        for (entry in scopeMap) {
            val downloadScope = entry.value
            if (downloadScope.isLoading())
                downloadScope.pause()
        }
    }

    /**
     * 啟動下載任務(wù)
     * 請不要直接使用此方法啟動下載任務(wù),它是交由[DownloadScope]進行調(diào)用
     */
    fun launchScope(scope: DownloadScope) {
        if (taskScopeMap.size >= MAX_SCOPE) return
        if (taskScopeMap.contains(scope.url)) return
        taskScopeMap[scope.url] = scope
        scope.launch()
    }

    /**
     * 啟動下一個任務(wù),如果有正在等待中的任務(wù)的話
     * 請不要直接使用此方法啟動下載任務(wù),它是交由[DownloadScope]進行調(diào)用
     * @param previousUrl 上一個下載任務(wù)的下載連接
     */
    fun launchNext(previousUrl: String) {
        taskScopeMap.remove(previousUrl)
        for (entrySet in scopeMap) {
            val downloadScope = entrySet.value
            if (downloadScope.isWaiting()) {
                launchScope(downloadScope)
                break
            }
        }
    }
}

全部代碼

https://gitee.com/tomato_wl/CoroutineDownlaod.git

總結(jié)

1.由于沒有新建一個module,所以很多不應(yīng)該暴露出來的方法都暴露出來了,不過不應(yīng)該暴露出來的都已經(jīng)注釋注明了.
2.pauseAll和removeAll這兩個方法并沒有經(jīng)過實際的測試
3.部分邏輯參考了開源項目OkGo,感謝!!!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末驯镊,一起剝皮案震驚了整個濱河市蔓钟,隨后出現(xiàn)的幾起案子唉擂,更是在濱河造成了極大的恐慌软舌,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件停巷,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機射窒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來将塑,“玉大人脉顿,你說我怎么就攤上這事〉懔龋” “怎么了艾疟?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長敢辩。 經(jīng)常有香客問我蔽莱,道長,這世上最難降的妖魔是什么戚长? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任盗冷,我火速辦了婚禮,結(jié)果婚禮上同廉,老公的妹妹穿的比我還像新娘仪糖。我一直安慰自己柑司,他們只是感情好,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布锅劝。 她就那樣靜靜地躺著攒驰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪故爵。 梳的紋絲不亂的頭發(fā)上玻粪,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天,我揣著相機與錄音稠集,去河邊找鬼奶段。 笑死,一個胖子當著我的面吹牛剥纷,可吹牛的內(nèi)容都是我干的痹籍。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼晦鞋,長吁一口氣:“原來是場噩夢啊……” “哼蹲缠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起悠垛,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤线定,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后确买,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體斤讥,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年湾趾,在試婚紗的時候發(fā)現(xiàn)自己被綠了芭商。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡搀缠,死狀恐怖铛楣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情艺普,我是刑警寧澤簸州,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站歧譬,受9級特大地震影響岸浑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瑰步,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一助琐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧面氓,春花似錦兵钮、人聲如沸蛆橡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泰演。三九已至,卻和暖如春葱轩,著一層夾襖步出監(jiān)牢的瞬間睦焕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工靴拱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留垃喊,地道東北人。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓袜炕,卻偏偏與公主長得像本谜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子偎窘,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350