使用ComposeDesktop開發(fā)一款桌面端多功能APK工具

前言

終于算是忙完了一個階段A煮Α!借笙!從4月份開始扒怖,工作內(nèi)容以及職務(wù)上都進(jìn)行了較大的變動,最直接的就是從海外項目組調(diào)到了國內(nèi)項目組业稼。國內(nèi)項目組目前有兩個應(yīng)用在同時跑著盗痒,而且還有幾個馬甲包也要維護(hù),不知道大家發(fā)版的時候復(fù)雜不復(fù)雜低散,反正我們每次發(fā)版的時候都需要經(jīng)歷--打包俯邓、加固、對齊熔号、重簽名看成、打渠道包、上傳云存儲跨嘉、生成渠道推廣鏈接川慌、生成內(nèi)更SQL、上傳Mapping文件等等步驟(xN)祠乃,簡直是折磨人啊梦重。

所以首要任務(wù)就是做出一套自動化的基礎(chǔ)設(shè)施來,最初直接考慮到的方案是【Jenkins+Docker+360命令行加固+VasDolly+Bugly等】的方案(下一篇文章會給大家分享該方案)亮瓷,整個過程下來基本能達(dá)到自動化的目的琴拧。就這么穩(wěn)定的跑了一個多月,然而嘱支,在5月下旬的時候360加固發(fā)布了一個通知蚓胸,大致內(nèi)容就是免費(fèi)版用戶無法使用命令行的加固方式了,只能手動用工具加固除师。這就導(dǎo)致最初的方案直接垮掉沛膳,我花費(fèi)了個把月學(xué)習(xí)Linux,Pipeline汛聚,Docker锹安,還制作了各種鏡像,結(jié)果突然不能用了倚舀,心塞叹哭。然而路還是要繼續(xù)走下去的,在盡量不花錢的前提下痕貌,想到了開發(fā)桌面端工具的方案风罩。

功能一覽

接下來先給大家一覽下桌面端工具的基本功能,我的電腦是Windows的舵稠,所以都是基于Windows平臺下的build-tools相關(guān)工具進(jìn)行開發(fā)的超升。首先大部分的功能都是基于jar或exe文件入宦,那么在Java(Kotlin)中我們可以通過如下方式來調(diào)用這些外部程序,exec其實最終也是調(diào)用了ProcessBuilder廓俭,整體的原理就是如此:

//方式1
Runtime.getRuntime().exec(cmd)

//方式2
ProcessBuilder(cmd)

多渠道打包

這是該工具最基本的功能,使用VasDolly方案對APK文件進(jìn)行多渠道打包(當(dāng)然該APK文件需要是簽名好的)唉工。

多渠道包命令行工具即 VasDolly.jar研乒,該文件可以在上述GitHub倉庫中找到,常用的命令如下:

// 獲取指定APK的簽名方式
java -jar VasDolly.jar get -s [源apk地址]

// 獲取指定APK的渠道信息
java -jar VasDolly.jar get -c [源apk地址]

// 刪除指定APK的渠道信息
java -jar VasDolly.jar remove -c [源apk地址]

// 通過指定渠道字符串添加渠道信息
java -jar VasDolly.jar put -c "channel1,channel2" [源apk地址] [apk輸出目錄]

// 通過指定某個渠道字符串添加渠道信息到目標(biāo)APK
java -jar VasDolly.jar put -c "channel1" [源apk地址] [輸出apk地址]

// 通過指定渠道文件添加渠道信息
java -jar VasDolly.jar put -c channel.txt [源apk地址] [apk輸出目錄]

// 提供了FastMode淋硝,生成渠道包時不進(jìn)行強(qiáng)校驗雹熬,速度可提升10倍以上
java -jar VasDolly.jar put -c channel.txt -f [源apk地址] [apk輸出目錄]

對齊和簽名

上傳應(yīng)用市場前,APK文件大部分會被市場要求進(jìn)行加固谣膳,無論是使用騰訊樂固還是360加固等方式竿报,加固后APK的簽名信息總會被破壞,所以我們需要對加固后的APK文件重新進(jìn)行簽名继谚。

配置簽名

首先我們需要準(zhǔn)備好應(yīng)用的簽名信息烈菌,該工具支持導(dǎo)入簽名文件,并保存相應(yīng)的StorePass花履、KeyAlias芽世、KeyPass信息,如下:

當(dāng)選擇APK后诡壁,程序會判斷選擇的APK是否進(jìn)行了簽名济瓢,如果沒有簽名,那么就會彈窗提醒用戶選擇配置好的簽名文件進(jìn)行簽名妹卿,簽名之后才可進(jìn)行多渠道打包的過程旺矾。

注:該功能現(xiàn)已升級,添加簽名文件的時候綁定包名夺克,選擇apk后會自動獲取到包名然后查找到對應(yīng)的簽名文件自動對齊簽名處理箕宙,無需手動進(jìn)行選擇了。

對齊

簽名的過程則需要用到Android SDK中的兩個文件铺纽,以Windows系統(tǒng)為例扒吁,一個是處理對齊的【build-tools\版本號\zipalign.exe】文件,另一個則是用來簽名的【build-tools\版本號\lib\apksigner.jar】文件室囊。

我們先看下zipalign工具的官方說明:

zipalign is a zip archive alignment tool. It ensures that all uncompressed files in the archive are aligned relative to the start of the file. This allows those files to be accessed directly via mmap(2), removing the need to copy this data in RAM and reducing your app's memory usage. zipalign是一種zip歸檔對齊工具雕崩。它確保存檔中所有未壓縮的文件都與文件的開頭對齊。這允許通過mmap直接訪問這些文件融撞,無需將這些數(shù)據(jù)復(fù)制到RAM中盼铁,并減少應(yīng)用程序的內(nèi)存使用。

zipalign should be used to optimize your APK file before distributing it to end-users. This is done automatically if you build with Android Studio. This documentation is for maintainers of custom build systems. 在將APK文件分發(fā)給用戶之前尝偎,應(yīng)使用zipalign優(yōu)化APK文件饶火。如果您使用Android Studio進(jìn)行構(gòu)建鹏控,這將自動完成。本文檔面向定制構(gòu)建系統(tǒng)的維護(hù)人員肤寝。

Google官方現(xiàn)在要求在使用apksigner對APK文件進(jìn)行簽名前需要先使用zipalign來優(yōu)化APK文件当辐,具體命令如下,以Windows下的zipalign.exe文件為例:

//對齊APK
zipalign.exe -p -f -v 4 [源apk路徑] [輸出apk路徑]

//驗證APK是否對齊
zipalign.exe -c -v 4 [源apk路徑]

其他相關(guān)的內(nèi)容可以參閱官網(wǎng) zipalign 鲤看。

簽名

當(dāng)APK文件對齊后缘揪,就可以給對齊后的APK進(jìn)行簽名操作了,簽名的方法有兩種义桂,我們這里單說使用--ks選項指定密鑰庫的方式找筝,具體命令如下:

java -jar apksigner.jar sign 
    --verbose 
    --ks [KeyStore文件路徑] 
    --ks-pass pass:[KeyStorePass]
    --ks-key-alias [KeyAlias]
    --key-pass pass:[KeyPass]
    --out [輸出apk路徑]
    [源apk路徑]

命令本身很簡單,別搞錯參數(shù)就好慷吊,尤其是兩個密碼的參數(shù)袖裕,后面需要使用【pass:密碼】。輸入密碼這里還支持其他格式溉瓶,如果有需要請參閱官網(wǎng) apksigner 急鳄。

加固、對齊堰酿、重簽名后攒岛,這個apk就可以進(jìn)行多渠道打包的處理了,然后即可發(fā)布到相關(guān)市場和渠道胞锰。

其他內(nèi)容

在項目中還有很多其他的相關(guān)配置灾锯,比如發(fā)版的時候需要對APP進(jìn)行應(yīng)用內(nèi)的更新通知。那么就需要我們填寫發(fā)版的相關(guān)信息嗅榕,版本名顺饮、版本號、更新日志等等內(nèi)容都需要完善(可根據(jù)APK文件的命名來獲取部分信息)凌那,然后通過這些信息生成應(yīng)用內(nèi)部更新的SQL語句兼雄,發(fā)送釘釘通知給相關(guān)后臺人員處理。通知這一步又用到了釘釘?shù)腟DK帽蝶,該工具支持配置釘釘機(jī)器人Webhook地址以及需要艾特的人員信息赦肋。

打出來的這些包都需要統(tǒng)一上傳到云存儲上面,這一步使用了AWS的云存儲SDK励稳,可以配置云存儲桶地址等信息佃乘,免去人工手動上傳apk的煩惱。上傳完畢后會根據(jù)文件名生成相應(yīng)的下載鏈接并通知到釘釘群驹尼,以便市場人員獲取到渠道最新的推廣鏈接等趣避。

桌面端開發(fā)

接下來就說下桌面端的開發(fā)過程,至于Compose MultiPlatform的介紹新翎,請參閱官網(wǎng)地址程帕。本文主要就描述下一些針對桌面端的相關(guān)需求住练。

彈窗

關(guān)于彈窗,ComposeDesktop同樣提供了Dialog可組合函數(shù):

@Composable 
public fun Dialog(
    onCloseRequest: () -> kotlin.Unit, 
    state: androidx.compose.ui.window.DialogState, 
    visible: kotlin.Boolean, 
    title: kotlin.String, 
    icon: androidx.compose.ui.graphics.painter.Painter?, 
    undecorated: kotlin.Boolean, 
    transparent: kotlin.Boolean, 
    resizable: kotlin.Boolean, 
    enabled: kotlin.Boolean, 
    focusable: kotlin.Boolean, 
    onPreviewKeyEvent: (androidx.compose.ui.input.key.KeyEvent) -> kotlin.Boolean, 
    onKeyEvent: (androidx.compose.ui.input.key.KeyEvent) -> kotlin.Boolean, 
    content: @Composable() (DialogWindowScope.() -> kotlin.Unit)
    ): kotlin.Unit { /* compiled code */ }

大部分的參數(shù)都可以直接看出他的作用愁拭,主要看一下state參數(shù)讲逛,該參數(shù)可以控制彈窗的位置及大小,例如我們配置一個在屏幕中央岭埠,寬高為500*300dp的彈窗盏混,那么示例代碼如下:

state = DialogState(
            position = WindowPosition(Alignment.Center),
            size = DpSize(500.dp, 300.dp),
        )

不過這個彈窗沒有陰影,如果想添加的話可以內(nèi)部套一層Surface來做出陰影效果:

Surface(
    modifier = Modifier.fillMaxSize().padding(20.dp),
    elevation = 10.dp,
    shape = RoundedCornerShape(16.dp)
)

文件選擇器

關(guān)于文件選擇器這一塊目前Compose還沒有專門的函數(shù)枫攀,但是我們還是可以使用原有的方案:

  • javax.swing.JFileChooser
  • java.awt.FileDialog

個人還是更偏向于使用JFileChooser括饶,因為使用第二種方案的話株茶,在頁面重組的情況下總是會莫名的彈出選擇框來来涨。一個簡單的文件選擇器如下所示:

private fun showFileSelector(
    suffixList: Array<String>,
    onFileSelected: (String) -> Unit
) {
    JFileChooser().apply {
        //設(shè)置頁面風(fēng)格
        try {
            val lookAndFeel = UIManager.getSystemLookAndFeelClassName()
            UIManager.setLookAndFeel(lookAndFeel)
            SwingUtilities.updateComponentTreeUI(this)
        } catch (e: Throwable) {
            e.printStackTrace()
        }

        fileSelectionMode = JFileChooser.FILES_ONLY
        isMultiSelectionEnabled = false
        fileFilter = FileNameExtensionFilter("文件過濾", *suffixList)

        val result = showOpenDialog(ComposeWindow())
        if (result == JFileChooser.APPROVE_OPTION) {
            val dir = this.currentDirectory
            val file = this.selectedFile
            println("Current apk dir: ${dir.absolutePath} ${dir.name}")
            println("Current apk name: ${file.absolutePath} ${file.name}")
            onFileSelected(file.absolutePath)
        }
    }
}

該方式在使用的過程中也有一定的缺陷,就是每次打開文件彈窗總是會卡頓一下启盛,所以后續(xù)也是有了尋找其他高效選擇文件方式的想法蹦掐。

文件拖拽

選擇文件除了上面的彈窗選擇方式,還有另一種神奇的方式 - 拖拽選擇僵闯,本來也是沒有頭緒卧抗,然而在Slack閑逛的時候發(fā)現(xiàn)了Jim Sproch推薦了一篇相關(guān)的文章:dev.to/tkuenneth/f… ”钏冢看完后也是恍然大悟社裆,但是在Compose Desktop中,window是整個窗口向图,如何讓某一個指定的區(qū)域響應(yīng)我們的文件拖拽事件呢泳秀?

還記得在Android上有ComposeView吧,用來嵌套原來的那一套View體系榄攀。那么在這里我也是采用了類似的這么一種方式嗜傅,實例一個空的JPanel控件然后給它安排到window中去。具體位置及大小的設(shè)置呢檩赢,在Compose中可以通過 onPlaced(onPlaced: (LayoutCoordinates) -> Unit) 修飾符來獲取到吕嘀,示例代碼如下所示:

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun DropBoxPanel(
    modifier: Modifier,
    window: ComposeWindow,
    component: JPanel = JPanel(),
    onFileDrop: (List<String>) -> Unit
) {

    val dropBoundsBean = remember {
        mutableStateOf(DropBoundsBean())
    }

    Box(
        modifier = modifier.onPlaced {
            dropBoundsBean.value = DropBoundsBean(
                x = it.positionInWindow().x,
                y = it.positionInWindow().y,
                width = it.size.width,
                height = it.size.height
            )
        }) {
        LaunchedEffect(true) {
            component.setBounds(
                dropBoundsBean.value.x.roundToInt(),
                dropBoundsBean.value.y.roundToInt(),
                dropBoundsBean.value.width,
                dropBoundsBean.value.height
            )
            window.contentPane.add(component)

            val target = object : DropTarget(component, object : DropTargetAdapter() {
                override fun drop(event: DropTargetDropEvent) {

                    event.acceptDrop(DnDConstants.ACTION_REFERENCE)
                    val dataFlavors = event.transferable.transferDataFlavors
                    dataFlavors.forEach {
                        if (it == DataFlavor.javaFileListFlavor) {
                            val list = event.transferable.getTransferData(it) as List<*>

                            val pathList = mutableListOf<String>()
                            list.forEach { filePath ->
                                pathList.add(filePath.toString())
                            }
                            onFileDrop(pathList)
                        }
                    }
                    event.dropComplete(true)

                }
            }) {

            }
        }

        SideEffect {
            component.setBounds(
                dropBoundsBean.value.x.roundToInt(),
                dropBoundsBean.value.y.roundToInt(),
                dropBoundsBean.value.width,
                dropBoundsBean.value.height
            )
        }

        DisposableEffect(true) {
            onDispose {
                window.contentPane.remove(component)
            }
        }
    }
}

實際運(yùn)行效果如下,個人感覺基本還是能達(dá)到目的的贞瞒。

數(shù)據(jù)的保存

最開始的時候偶房,功能很少,每個配置的數(shù)據(jù)都是使用了txt文件來一行行保存军浆,但是到了后來功能越來越復(fù)雜蝴悉,單純的按行來處理貌似有點捉襟見肘了,所以考慮使用json來保存復(fù)雜的類型數(shù)據(jù)瘾敢。

json數(shù)據(jù)的處理從原生JSON到FastJson拍冠,Gson尿这,Moshi等都已經(jīng)體驗過了,于是乎便采用了之前未使用過的Jackson庆杜。然而不得不說射众,就目前為止,jackson是我用過最簡潔晃财、優(yōu)雅的一款解析庫叨橱。

假如我有一個List類型的列表數(shù)據(jù),那么當(dāng)我要把這個數(shù)據(jù)存儲到文件的時候只需:

jacksonObjectMapper().writeValue(File, List<String>)

而從文件中讀取數(shù)據(jù)也是簡單的狠岸鲜ⅰ:

//方式1
val list = jacksonObjectMapper().readValue<List<String>>(jsonFile)

//方式2
val list : List<String> = jacksonObjectMapper().readValue(jsonFile)

這種簡潔真的是深入我心罗洗。繼續(xù)深入了解下Jackson,你會發(fā)現(xiàn)它的可擴(kuò)展性以及可定制性都很強(qiáng)钢猛,簡直相見恨晚啊伙菜。之前也是在一個舒適圈待習(xí)慣了,這次主動跳出來居然有了意想不到的收獲命迈。

但是呢贩绕,每個框架也會有它自己的注意點,比如jackson壶愤,屬性命名不可以是is開頭淑倾,否則序列化等就會報錯。這點似乎在阿里巴巴JAVA手冊中好像也有提到征椒,具體原因請大家自行百度(Google)娇哆。

資源的拷貝

當(dāng)我們使用[java -jar xxx.jar]命令執(zhí)行jar文件的時候,需要明確指定 jar文件的地址勃救,但是在Compose Desktop中我們要怎么存放并讀取這個jar文件呢 碍讨?我們可以從Compose Desktop中讀取并展示圖片的相關(guān)代碼中得到啟發(fā),假如有一個sample.svg圖標(biāo)文件存放到了項目的 resources 文件夾下剪芥,那么我們在引用這張圖片的時候就可以使用:

painterResource("sample.svg")

我們點進(jìn)去這個方法看下:

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun painterResource(
    resourcePath: String
): Painter = painterResource(
    resourcePath,
    ResourceLoader.Default
)

@ExperimentalComposeUiApi
@Composable
fun painterResource(
    resourcePath: String,
    loader: ResourceLoader
): Painter = when (resourcePath.substringAfterLast(".")) {
    "svg" -> rememberSvgResource(resourcePath, loader)
    "xml" -> rememberVectorXmlResource(resourcePath, loader)
    else -> rememberBitmapResource(resourcePath, loader)
}

里面居然有個ResourceLoader類垄开,這名字一聽就有戲啊,大概率就是我們需要的內(nèi)容税肪,而傳遞的默認(rèn)參數(shù)是ResourceLoader.Default溉躲,那么就看下Default的源碼吧:

//==========Resources.desktop.kt文件==========
@ExperimentalComposeUiApi
interface ResourceLoader {
    companion object {
        /**
         * Resource loader which is capable to load resources from `resources` folder in an application's
         * project. Ability to load from dependent modules resources is not guaranteed in the future.
         * Use explicit `ClassLoaderResourceLoader` instance if such guarantee is needed.
         */
        @ExperimentalComposeUiApi
        val Default = ClassLoaderResourceLoader()
    }
    fun load(resourcePath: String): InputStream
}

@ExperimentalComposeUiApi
class ClassLoaderResourceLoader : ResourceLoader {
    override fun load(resourcePath: String): InputStream {
        // TODO(https://github.com/JetBrains/compose-jb/issues/618): probably we shouldn't use
        //  contextClassLoader here, as it is not defined in threads created by non-JVM
        val contextClassLoader = Thread.currentThread().contextClassLoader!!
        val resource = contextClassLoader.getResourceAsStream(resourcePath)
            ?: (::ClassLoaderResourceLoader.javaClass).getResourceAsStream(resourcePath)
        return requireNotNull(resource) { "Resource $resourcePath not found" }
    }
}

//==========ClassLoader類==========
public InputStream getResourceAsStream(String name) {
    Objects.requireNonNull(name);
    URL url = getResource(name);
    try {
        return url != null ? url.openStream() : null;
    } catch (IOException e) {
        return null;
    }
}

public URL getResource(String name) {
    Objects.requireNonNull(name);
    URL url;
    if (parent != null) {
        url = parent.getResource(name);
    } else {
        url = BootLoader.findResource(name);
    }
    if (url == null) {
        url = findResource(name);
    }
    return url;
}

上述源碼的整個邏輯基本上就是兩步,根據(jù)資源文件名獲取到資源文件益兄,然后獲取資源文件的輸入流锻梳。看到這里其實我們已經(jīng)有兩種方案了:

  • 方案一:直接拿到文件的URL然后獲取到文件的路徑
  • 方案二:根據(jù)文件的輸入流净捅,將文件重新保存到本機(jī)相關(guān)目錄

然而事情并沒有這么簡單疑枯,如果我們使用方案一,那么在編譯運(yùn)行的時候完全沒有問題蛔六,所有的資源文件會被保存到【\build\processedResources\jvm】下荆永,此時我們直接可以通過文件的URL獲取到文件路徑废亭,然后調(diào)用即可。但是具钥,當(dāng)我們打包成安裝包后豆村,例如在Windows下使用packageMsi命令打包出msi文件并安裝到電腦上后,運(yùn)行程序骂删,這時候你就會發(fā)現(xiàn)資源文件所在的路徑就很奇怪掌动,例如我的工程下是【C:\Program Files\工程名\app\工程名-jvm-1.0-SNAPSHOT-xxxxxx.jar!/資源文件名】,也就是說所有的資源文件被打包進(jìn)了這個快照文件宁玫,如果此時直接使用該路徑運(yùn)行java -jar 等命令粗恢,那么肯定就會報錯了。

所以最穩(wěn)妥的方式還是使用方案二欧瘪,使用ResourceLoader獲取到資源文件流然后重新保存到本機(jī)上的相關(guān)目錄就好了眷射,偽代碼如下:

ResourceLoader.Default.load(resourcesPath)
    .use { inputStream ->
        val fos = FileOutputStream(file)
        val buffer = ByteArray(1024)
        var len: Int
            while (((inputStream.read(buffer).also { len = it })) != -1) {
                fos.write(buffer, 0, len)
                }
          fos.flush()
              inputStream.close()
              fos.close()
          }

打包MSI

在Windows環(huán)境下打包Msi格式安裝包的時候,有一個downloadWix的Task恋追,該Task涉及到了Wix資源的下載凭迹,如下 :

Task :downloadWix Download github.com/wixtoolset/…

在IDEA中下載可能會非常的緩慢罚屋,此時我們可以復(fù)制上述地址苦囱,登上梯子,然后直接去GitHub下載脾猛。下載完畢后直接放入【/build/wixToolset】目錄下即可撕彤,再次編譯速度就會起飛了。

總結(jié)

簡直沒想到啊猛拴,作為一個Android開發(fā)者羹铅,現(xiàn)在借助Compose Desktop開發(fā)起桌面端居然能這么的輕車熟路,我對Compose真是越來越喜歡了愉昆。

另外呢职员,跳出業(yè)務(wù)這一段時間來處理這些東西也讓我對干預(yù)APK的打包等過程從理論邁出了實踐的一步,同時對市場和運(yùn)營同學(xué)的工作也有了更多了解跛溉,通過該工具幫助其處理了部分重復(fù)機(jī)械式的工作焊切,部門間的感情也得到了進(jìn)一步的增溫(狗頭滑稽)。

就編到這吧芳室,桌面工具還需要持續(xù)的維護(hù)跟優(yōu)化专肪,基本是面向市場和運(yùn)營同事編程了。關(guān)于開頭說的Jenkins那一套其實早就寫好了堪侯,是鄙人少有的萬字長文嚎尤,但是中間變故太大,一直也沒發(fā)布出來伍宦,接下來會重新整理下并發(fā)布芽死,還請大家多多指正乏梁。

作者:樂翁龍
鏈接:https://juejin.cn/post/7122645579439538183

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市关贵,隨后出現(xiàn)的幾起案子掌呜,更是在濱河造成了極大的恐慌,老刑警劉巖坪哄,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件质蕉,死亡現(xiàn)場離奇詭異,居然都是意外死亡翩肌,警方通過查閱死者的電腦和手機(jī)模暗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來念祭,“玉大人兑宇,你說我怎么就攤上這事×焕ぃ” “怎么了隶糕?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長站玄。 經(jīng)常有香客問我枚驻,道長,這世上最難降的妖魔是什么株旷? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任再登,我火速辦了婚禮,結(jié)果婚禮上晾剖,老公的妹妹穿的比我還像新娘锉矢。我一直安慰自己,他們只是感情好齿尽,可當(dāng)我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布沽损。 她就那樣靜靜地躺著,像睡著了一般循头。 火紅的嫁衣襯著肌膚如雪绵估。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天贷岸,我揣著相機(jī)與錄音壹士,去河邊找鬼。 笑死偿警,一個胖子當(dāng)著我的面吹牛躏救,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼盒使,長吁一口氣:“原來是場噩夢啊……” “哼崩掘!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起少办,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤苞慢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后英妓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挽放,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年蔓纠,在試婚紗的時候發(fā)現(xiàn)自己被綠了辑畦。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡腿倚,死狀恐怖纯出,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情敷燎,我是刑警寧澤暂筝,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站硬贯,受9級特大地震影響焕襟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜澄成,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一胧洒、第九天 我趴在偏房一處隱蔽的房頂上張望畏吓。 院中可真熱鬧墨状,春花似錦、人聲如沸菲饼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宏悦。三九已至镐确,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間饼煞,已是汗流浹背源葫。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留砖瞧,地道東北人息堂。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親荣堰。 傳聞我的和親對象是個殘疾皇子床未,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,490評論 2 348

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