Data 解析 Doom 的 WAD 文件

作者:Terhechte,原文鏈接腻菇,原文日期:2016/07/15
譯者:BigbigChai作岖;校對:way;定稿:千葉知風

Swift 3 : 從 NSData 到 Data 的轉變

Swift 3 帶來了許多大大小小的變化胁住。其中一個是為常見的 Foundation 引用類型(例如將 NSData 封裝成 Data 趁猴,將 NSDate 封裝成 Date)添加值類型的封裝。這些新類型除了改變了內存行為和名字以外彪见,在方法上也與對應的引用類型有所區(qū)別 <a id="fnr.1" name="fnr.1" class="footref" href="#fn.1">1</a>儡司。 從更換新方法名這類小改動,到完全去掉某一功能這種大改動余指,我們需要一些時間去適應這些新的值類型捕犬。本文會重點介紹作為值類型的 Data 是如何封裝 NSData 的。

不僅如此酵镜,在學習完基礎知識之后碉碉,我們還會寫一個簡單的示例應用。這個應用會讀取和解析一個 Doom 毀滅戰(zhàn)士的 WAD 文件 <a id="fnr.2" name="fnr.2" class="footref" href="#fn.2">2</a>淮韭。

基本區(qū)別

對于 NSData垢粮,其中一個最常見的使用場景就是調用以下方法加載和寫入數據:

func writeToURL(_ url: NSURL, atomically atomically: Bool) -> Bool
func writeToURL(_ url: NSURL, options writeOptionsMask: NSDataWritingOptions) throws
// ... (implementations for file: String instead of NSURL)
init?(contentsOfURL url: NSURL)
init(contentsOfURL url: NSURL, options readOptionsMask: NSDataReadingOptions) throws
// ... (implementations for file: String instead of NSURL)

基本的使用方法并沒有什么改動。新的 Data 類型提供了以下方法:

init(contentsOf: URL, options: ReadingOptions)
func write(to: URL, options: WritingOptions)

留意到 Data 簡化了從文件讀寫數據的方法靠粪,原本 NSData 提供了多種不同的方法蜡吧,現在只精簡到兩個方法。

比較一下 NSDataData 的方法占键,可以發(fā)現另一個變化昔善。NSData 提供了三十個方法和屬性,而 Data 提供了一百三十個畔乙。Swift 強大的協議擴展可以輕易地解釋這個巨大的差異君仆。Data 從以下協議里獲得了許多方法:

  • CustomStringConvertible
  • Equatable
  • Hashable
  • MutableCollection
  • RandomAccessCollection
  • RangeReplaceableCollection
  • ReferenceConvertible

這給 Data 提供了許多 NSData 不具備的功能。這里列出部分例子:

func distance(from: Int, to: Int)
func dropFirst(Int)
func dropLast(Int)
func filter((UInt8) -> Bool)
func flatMap<ElementOfResult>((UInt8) -> ElementOfResult?)
func forEach((UInt8) -> Void)
func index(Int, offsetBy: Int, limitedBy: Int)
func map<T>((UInt8) -> T)
func max()
func min()
func partition()
func prefix(Int)
func reversed()
func sort()
func sorted()
func split(separator: UInt8, maxSplits: Int, omittingEmptySubsequences: Bool)
func reduce<Result>(Result, (partialResult: Result, UInt8) -> Result)

如你所見牲距,許多函數式方法袖订,例如 mapping 和 filtering 現在都可以操作 Data 類型的字節(jié)內容了。我認為這是相對 NSData 的一大進步嗅虏。優(yōu)勢在于洛姑,現在可以輕松地使用下標以及對數據內容進行比較了。

var data = Data(bytes: [0x00, 0x01, 0x02, 0x03])  
print(data[2]) // 2
data[2] = 0x09
print (data == Data(bytes: [0x00, 0x01, 0x09, 0x03])) // true

Data 還提供了一些新的初始化方法專門用于處理 Swift 里常見的數據類型:

init(bytes: Array<UInt8>)
init<SourceType>(buffer: UnsafeMutableBufferPointer<SourceType>)
init(repeating: UInt8, count: Int)

獲取字節(jié)

如果你使用 Data 與底層代碼(例如 C庫)交互皮服,你會發(fā)現另一個明顯的區(qū)別:Data 缺少了 NSDatagetBytes 方法:

// NSData
func getBytes(_ buffer: UnsafeMutablePointer<Void>, length length: Int)

getBytes 方法有許多不同的應用場景楞艾。其中最常見的是参咙,當你需要解析一個文件并按字節(jié)讀取并存儲到數據類型/變量里。例如說硫眯,你想讀取一個包含項目列表的二進制文件蕴侧。這個文件經過編碼,而編碼方式如下:

數據類型 大小 功能
Char 4 頭部 (ABCD)
UInt32 4 數據開始
UInt32 4 數量

該文件包含了一個四字節(jié)字符串 ABCD 標簽两入,用來表示正確的文件類型(做校驗)净宵。接著的四字節(jié)定義了實際數據(例如頭部的結束和項目的開始),頭部最后的四字節(jié)定義了該文件存儲項目的數量裹纳。

NSData 解析這段數據非常簡單:

let data = ...
var length: UInt32 = 0
var start: UInt32 = 0
data.getBytes(&start, range: NSRange(location: 4, length: 4))
data.getBytes(&length, range: NSRange(location: 8, length: 4))

如此將返回正確結果<a id="fnr.3" name="fnr.3" class="footref" href="#fn.3">3</a>择葡。如果數據不包含 C 字符串,方法會更簡單剃氧。你可以直接用正確的字段定義一個 結構體敏储,然后把字節(jié)讀到結構體里:

數據類型 大小 功能
UInt32 4 數據開始
UInt32 4 數量
let data = ...
struct Header { 
    let start: UInt32
    let length: UInt32
}
var header = Header(start: 0, length: 0)
data.getBytes(&header, range: NSRange(location: 0, length: 8))

Data 中 getBytes 的替代方案

不過 Data 里 getBytes 這個功能不再可用,轉而提供了一個新方法作替代:

// 從數據里獲得字節(jié)
func withUnsafeBytes<ResultType, ContentType>((UnsafePointer<ContentType>) -> ResultType)

通過這個方法朋鞍,我們可以從閉包中直接讀取數據的字節(jié)內容已添。來看一個簡單的例子:

let data = Data(bytes: [0x01, 0x02, 0x03])
data.withUnsafeBytes { (pointer: UnsafePointer<UInt8>) -> Void in
    print(pointer)
    print(pointer.pointee)
}
// 打印
// : 0x00007f8dcb77cc50
// : 1

好了,現在有一個指向數據的 unsafe UInt8 指針滥酥,那要怎樣利用起來呢更舞?首先,我們需要一個不同的數據類型坎吻,然后一定要確定該數據的類型缆蝉。我們知道這段數據包含一個 Int32 類型,那該如何正確地解碼呢禾怠?

既然已經有了一個 unsafe pointer(UInt8 類型),那么就能夠輕松地轉換成目標類型 unsafe pointer贝搁。UnsafePointer 有一個 pointee 屬性吗氏,可以返回指針所指向數據的正確類型:

let data = Data(bytes: [0x00, 0x01, 0x00, 0x00])
let result = data.withUnsafeBytes { (pointer: UnsafePointer<Int32>) -> Int32 in
    return pointer.pointee
}
print(result)
//: 256

如你所見,我們創(chuàng)建了一個字節(jié)的 Data 實例雷逆,通過在閉包里定義 UnsafePointer<Int32>弦讽,返回 Int32 類型的數據“蛘埽可以把代碼寫得再精簡一點往产,因為編譯器能夠根據上下文推斷結果類型:

let result: Int32 = data.withUnsafeBytes { $0.pointee }

數據的生命周期

使用 withUnsafeBytes 時,指針(你所訪問的)的生命周期是一個很重要的考慮因素(除了整個操作都是不安全的之外)某宪。指針的生命周期受制于閉包的生命周期仿村。正如文檔所說:

留意:字節(jié)指針參數不應該被存儲,或者在所調用閉包的生命周期以外被使用兴喂。

泛型解決方案

現在蔼囊,我們已經可以讀取原始字節(jié)數據焚志,并把它們轉換成正確的類型了。接下來創(chuàng)建一個通用的方法來更輕松地執(zhí)行操作畏鼓,而不用額外地關心語法酱酬。 另外,我們暫時還無法針對數據的子序列執(zhí)行操作云矫,而只能對整個 Data 實例執(zhí)行操作膳沽。 泛型的解決方法大概是這個樣子的:

extension Data {
    func scanValue<T>(start: Int, length: Int) -> T {
        return self.subdata(in: start..<start+length).withUnsafeBytes { $0.pointee }
    }
}
let data = Data(bytes: [0x01, 0x02, 0x01, 0x02])
let a: Int16 = data.scanValue(start: 0, length: 1)
print(a)
// : 1

與之前的代碼相比,存在兩個顯著的不同點:

  • 我們使用了 subdata 把掃描的字節(jié)限定于所需的特定區(qū)域让禀。
  • 我們使用了泛型來支持提取不同的數據類型挑社。

數據轉換

另一方面,從現有的變量內容里得到 Data 緩沖堆缘, 雖然與下面的 Doom 的例子不相關滔灶,但是非常容易實現,(因此也寫在這里啦)

var variable = 256
let data = Data(buffer: UnsafeBufferPointer(start: &variable, count: 1))
print(data) // : <00010000 00000000>

解析 Doom WAD 文件

我小時候非常熱愛 Doom(毀滅戰(zhàn)士)這個游戲吼肥。也玩到了很高的等級录平,并修改 WAD 文件加入了新的精靈,紋理等缀皱。因此當我想給解析二進制文件找一個合適(和簡單)的例子時斗这,就想起了 WAD 文件的設計。因為它十分直觀且容易實現啤斗。于是我寫了一個簡單的小程序表箭,用于讀取 WAD 文件,然后列出所有存儲地板的紋理名稱 <a id="fnr.4" name="fnr.4" class="footref" href="#fn.4">4</a>钮莲。

我把源代碼 放在了 GitHub 免钻。
以下兩個文件解釋了Doom WAD 文件的設計。

但是對于這個簡單的示例崔拥,只需要了解部分的文件格式就夠了极舔。
首先,每個 WAD 文件都有頭文件:

數據類型 大小 功能
Char 4 字符串 IWAD 或者 PWAD
Int32 4 WAD 中區(qū)塊的數目
Int32 4 指向目錄位置的指針

開頭的 4 字節(jié)用來確定文件格式。 IWAD 表明是官方的 Doom WAD 文件,PWAD 表明是在運行時補充內容到主要 WAD 文件的補丁文件血筑。我們的應用只會讀取 IWAD 文件由缆。接著的 4 字節(jié)確定了 WAD 文件中 區(qū)塊(lump) 的數目。 區(qū)塊(Lump)是與 Doom 引擎合作的個體項目,例如紋理材質、精靈幀(Sprite-Frames),文字內容卖子,模型,等等刑峡。每個紋理都是不同類的區(qū)塊揪胃。最后的 4 字節(jié)定義了目錄的位置璃哟。我們開始解析目錄的時候,會給出相關解釋喊递。首先随闪,讓我們來解析頭文件。

解析頭文件

讀取 WAD 文件的方法非常簡單:

let data = try Data(contentsOf: wadFileURL, options: .alwaysMapped)

我們獲取到數據之后骚勘,首先需要解析頭文件铐伴。這里多次使用了之前創(chuàng)建的 scanValuedata`` 擴展。

public func validateWadFile() throws {
    // 一些 Wad 文件定義
    let wadMaxSize = 12, wadLumpsStart = 4, wadDirectoryStart = 8, wadDefSize = 4
    // WAD 文件永遠以 12 字節(jié)的頭文件開始俏讹。
    guard data.count >= wadMaxSize else { throw WadReaderError.invalidWadFile(reason: "File is too small") }

    // 它包含了三個值:

    // ASCII 字符 "IWAD" 或 "PWAD" 定義了 WAD 是 IWAD 還是 PWAD当宴。
    let validStart = "IWAD".data(using: String.Encoding.ascii)!
    guard data.subdata(in: 0..<wadDefSize) == validStart else
    { throw WadReaderError.invalidWadFile(reason: "Not an IWAD") }

    // 一個聲明了 WAD 中區(qū)塊數目的整數。
    let lumpsInteger: Int32 = data.scanValue(start: wadLumpsStart, length: wadDefSize)

    // 一個整數泽疆,含有指向目錄地址的指針户矢。
    let directoryInteger: Int32 = data.scanValue(start: wadDirectoryStart, length: wadDefSize)

    guard lumpsInteger > 0 && directoryInteger > Int32(wadMaxSize)
    else {
        throw WadReaderError.invalidWadFile(reason: "Empty Wad File")
    }
}

你可以在 GitHub 找到其他的類型(例如 WadReaderError enum)。下一步就是解析目錄來獲取每個區(qū)塊的地址和大小殉疼。

解析目錄

目錄與區(qū)塊的名字梯浪、包含的數據相關聯。它包括了一系列的項目瓢娜,每個項目的長度為 16 字節(jié)挂洛。目錄的長度取決于 WAD 頭文件里給出的數字。

每個 16 字節(jié)的項目按照以下的格式:

數據類型 大小 功能
Int32 4 區(qū)塊數據在文件中的開始
Int32 4 區(qū)塊的字節(jié)大小
Char 4 定義了區(qū)塊名字的 ASCII 字符串

名字的字符定義得比較復雜眠砾。文檔是這么說的:

使用 ASCII 字符串定義區(qū)塊的名字虏劲。區(qū)塊的名字只能使用 A-Z(大寫),0-9褒颈,[ ] - _(Arch-Vile 精靈除外柒巫,它們使用 \)。如果這串字符小于 8 字節(jié)長度谷丸,那么余下字節(jié)要被 null 填滿堡掏。

留意最后一句話。在 C 語言里淤井,字符串由空字符(\0)結束布疼。這向系統表明了該字符串的內存到這里結束摊趾。Doom 用可選的空字符來節(jié)約存儲空間币狠。當字符串小于 8 字節(jié),它會包含一個空字符砾层。如果它達到最大允許長度( 8 字節(jié))漩绵,那么字符串以最后一個字節(jié)結束,而非由空字符結束肛炮。

? 0 1 2 3 4 5 6 7 ?
I M P \0 \0 \0 \0 \0 #
F L O O R 4 _ 5 #

看看上面的表格止吐, 短名字會在字符串最后補空字符(位置 3)宝踪。長名字則沒有空字符,而是以 FLOOR4_5 的最后一個字符 5 作為結束碍扔。#表明了下一個項目/片段在內存中的開始瘩燥。

在我們嘗試支持區(qū)塊的名字字符格式之前,首先處理一下簡單的部分不同。那就是讀取開頭和大小厉膀。

在開始之前,我們應該定義一個數據結構二拐,用于保存從目錄里讀取的內容:

public struct Lump {
    public let filepos: Int32
    public let size: Int32
    public let name: String
}

然后服鹅,從完整的數據實例里取出數據片段,這是這些數據構成我們的目錄百新。

// 定義一個目錄項的默認大小企软。
let wadDirectoryEntrySize = 16
// 從完整數據里提取目錄片段。
let directory = data.subdata(in: Int(directoryLocation)..<(Int(directoryLocation) + Int(numberOfLumps) * wadDirectoryEntrySize))

接著饭望,我們以每段 16 字節(jié)的長度在 Data 中迭代仗哨。 Swift 的 stride 方法能夠很好地實現這個功能:

for currentIndex in stride(from: 0, to: directory.count, by: wadDirectoryEntrySize) {
    let currentDirectoryEntry = directory.subdata(in: currentIndex..<currentIndex+wadDirectoryEntrySize)

    // 一個整數表明區(qū)塊數據的起始在文件中的位置。
    let lumpStart: Int32 = currentDirectoryEntry.scanValue(start: 0, length: 4)

    // 一個表示了區(qū)塊字節(jié)大小的整數杰妓。
    let lumpSize: Int32 = currentDirectoryEntry.scanValue(start: 4, length: 4)
    ...
}

簡單的部分到此結束藻治,下面我們要開始進入秋名山飆車了。

解析 C 字符串

要知道對于每個區(qū)塊的名字巷挥,每當遇到空的結束字符或者達到 8 字節(jié)的時候桩卵,我們都要停止向 Swift 字符串的寫入。首要任務是利用相關數據創(chuàng)建一個數據片段倍宾。

let nameData = currentDirectoryEntry.subdata(in: 8..<16)

Swift 給 C 字符串提供了很好的互操作性雏节。這意味著需要創(chuàng)建一個字符串的時候,我們只需要把數據交給 String 的初始化方法就行了:

let lumpName = String(data: nameData, encoding: String.Encoding.ascii)

這個方法可以執(zhí)行高职,但是結果并不正確钩乍。因為它忽略了空結束符,所以即使是短名字怔锌,也會跟長名字一樣轉換成 8 字節(jié)的字符串寥粹。例如,名字為 IMP 的區(qū)塊會變成 IMP00000埃元。但是由于 String(data:encoding:) 并不知道 Doom 把剩下的 5 字節(jié)都用空字符填滿了涝涤,而是根據 nameData 創(chuàng)建了一個完整 8 字節(jié)的字符串。

如果我們想要支持空字符岛杀, Swift 提供了一個 cString 初始化方法阔拳,用來讀取包含空結束符的有效 cString:

// 根據所給的 C 數組創(chuàng)建字符串
// 根據所給的編碼方式編碼
init?(cString: UnsafePointer<CChar>, encoding enc: String.Encoding)

留意這里的參數不需要傳入 data 實例,而是要求一個指向 CChars 的 unsafePointer类嗤。我們已經熟悉這個方法了糊肠,來寫一下:

let lumpName2 = nameData.withUnsafeBytes({ (pointer: UnsafePointer<UInt8>) -> String? in
    return String(cString: UnsafePointer<CChar>(pointer), encoding: String.Encoding.ascii)
})

以上方法依然不能得到我們想要的結果辨宠。在 Doom 的名字長度小于 8 字符的情況下,這段代碼都能完美運行货裹。但是只要某個名字長度達到 8 字節(jié)而沒有一個空結束符時嗤形,這會繼續(xù)讀取(變成一個 16 字節(jié)片段)弧圆,直到找到下一個有效的空結束符派殷。 這就帶來一些不確定長度的長字符串。

這個邏輯是 Doom 自定義的墓阀,因此我們需要自己來實現相應的代碼毡惜。Data 支持 Swift 的集合和序列操作,因此我們可以直接用 reduce 來解決斯撮。

let lumpName3Bytes = try nameData.reduce([UInt8](), { (a: [UInt8], b: UInt8) throws -> [UInt8] in
    guard b > 0 else { return a }
    guard a.count <= 8 else { return a }
    return a + [b]
})
guard let lumpName3 = String(bytes: lumpName3Bytes, encoding: String.Encoding.ascii)
    else {
    throw WadReaderError.invalidLup(reason: "Could not decode lump name for bytes \(lumpName3Bytes)")
}

這段代碼把數據以 UInt8 字節(jié) reduce经伙,并檢查數據是否含有提前的空結束符。一切工作正常勿锅,雖然數據需要進行幾次抽象帕膜,執(zhí)行速度并不是很快。

不過如果我們能以 Doom 引擎類似的方法來解決的話溢十,效果會更好垮刹。Doom 僅移動了 char* 的指針,并根據字符是否為空結束符判斷是否需要提前跳出张弛。Doom 是用 C 語言寫的荒典,因此它能在裸指針層面上迭代。

那么我們要怎樣在 Swift 里實現這個邏輯呢吞鸭?事實上寺董,可以再次借助 withUnsafeBytes 實現類似的效果。來看看代碼:

let finalLumpName = nameData.withUnsafeBytes({ (pointer: UnsafePointer<CChar>) -> String? in
    var localPointer = pointer
    for _ in 0..<8 {
    guard localPointer.pointee != CChar(0) else { break }
    localPointer = localPointer.successor()
    }
    let position = pointer.distance(to: localPointer)
    return String(data: nameData.subdata(in: 0..<position),
          encoding: String.Encoding.ascii)
})
guard let lumpName4 = finalLumpName else {
    throw WadReaderError.invalidLup(reason: "Could not decode lump name for bytes \(lumpName3Bytes)")
}

withUnsafeBytes 的用法與之前相似刻剥,我們接受一個指向原始內存的指針遮咖。 指針 是一個 let 常數,但是由于我們需要對它做修改造虏,因此我們在第一行創(chuàng)建了一個可變的拷貝<a id="fnr.5" name="fnr.5" class="footref" href="#fn.5">5</a>御吞。

接著,開始我們的主要工作漓藕。從 0 到 8 循環(huán)陶珠,每次循環(huán)都檢測指針指向的字符(pointee)是否為空結束符(CChar(0))。是空結束符的話就表明提前找到了空結束符撵术,需要跳出循環(huán)背率。否則將 localPointer 重載為下一位话瞧,即就是嫩与,當前指針內存中的下一個位置寝姿。這樣,我們就能逐字節(jié)地讀取內存中的所有內容了划滋。

完成之后 饵筑,就計算一下我們原始指針本地指針的距離。如果在找到空結束符之前我們僅前移了三次处坪,那么兩個指針之前的距離為 3根资。最后,這個距離能讓我們通過實際 C 字符串的子數據創(chuàng)建一個新的 String 實例同窘。

最后用得到的數據創(chuàng)建新的 區(qū)塊 結構體:

lumps.append(Lump(filepos: lumpStart, size: lumpSize, name: lumpName4))

如果你觀察源代碼玄帕,會發(fā)現 F_STARTF_END 這種顯著的引用。對于特殊的 區(qū)塊區(qū)域 想邦,Doom 使用特殊名稱的空區(qū)塊標記了區(qū)域的開頭和結尾裤纹。F_START / F_END 圍起了所有地板紋理的區(qū)塊。在本教程中丧没,我們將忽略這額外的一步鹰椒。

應用最終的截圖:

我知道這看起來并不酷炫。之后可能會計劃在博客里寫寫如何展示那些紋理呕童。

橋接 NSData

我發(fā)現新的 DataNSData 使用起來更加方便漆际。然而,如果你需要 NSData 或者 getBytes 方法的話夺饲,這有一個簡單的方法能把 Data 轉換成 NSData奸汇。Swift 文檔是這么寫的:

Data 具有“寫時拷貝”能力,也能與 Objective-C 的 NSData 類型橋接往声。 對于 NSData 的自定義子類茫蛹,你可以使用 myData as Data 把它的一個實例轉換成結構體 Data 。

// 創(chuàng)建一個 Data 結構體
let aDataStruct = Data()
// 獲得底層的引用類型 NSData
let aDataReference = aDataStruct as NSData

無論何時烁挟,如果你覺得 Data 類型難以滿足你的需求婴洼,都能輕松地回到 NSData 類型使用你熟悉的方法。不過總而言之你還是應該盡可能地使用新的 Data 類型(除非你需要引用類型的語法)撼嗓。

<a id="fn.1" name="fn.1" class="footnum" href="#fnr.1">1: 有些類型(例如 Date) 并不是包裹類型柬采,而是全新的實現。</a>

<a id="fn.2" name="fn.2" class="footnum" href="#fnr.2">2: Doom1且警,Doom2粉捻,Hexen,Heretic斑芜,還有 Ultimate Doom肩刃。雖然我只在 Doom1 Shareware 驗證過。</a></sup

<a id="fn.3" name="fn.3" class="footnum" href="#fnr.3">3: 留意,我們并沒有驗證最開頭的 4 個字節(jié)盈包,確保這的確是 ABCD 文件沸呐。但是要添加這個驗證也很簡單。</a></sup

<a id="fn.4" name="fn.4" class="footnum" href="#fnr.4">4: 其實我也想展示 texture 但是不夠時間去實現呢燥。</a></sup

<a id="fn.5" name="fn.5" class="footnum" href="#fnr.5">5: Swift 3 不再在閉包和函數體里支持有用的 var 標注崭添。</a></sup

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權叛氨,最新文章請訪問 http://swift.gg呼渣。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市寞埠,隨后出現的幾起案子屁置,更是在濱河造成了極大的恐慌,老刑警劉巖仁连,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缰犁,死亡現場離奇詭異,居然都是意外死亡怖糊,警方通過查閱死者的電腦和手機帅容,發(fā)現死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來伍伤,“玉大人并徘,你說我怎么就攤上這事∪呕辏” “怎么了麦乞?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長劝评。 經常有香客問我姐直,道長,這世上最難降的妖魔是什么蒋畜? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任声畏,我火速辦了婚禮,結果婚禮上姻成,老公的妹妹穿的比我還像新娘插龄。我一直安慰自己,他們只是感情好科展,可當我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布均牢。 她就那樣靜靜地躺著,像睡著了一般才睹。 火紅的嫁衣襯著肌膚如雪徘跪。 梳的紋絲不亂的頭發(fā)上甘邀,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天,我揣著相機與錄音垮庐,去河邊找鬼松邪。 笑死,一個胖子當著我的面吹牛突硝,可吹牛的內容都是我干的。 我是一名探鬼主播置济,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼解恰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了浙于?” 一聲冷哼從身側響起护盈,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎羞酗,沒想到半個月后腐宋,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡檀轨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年胸竞,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片参萄。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡卫枝,死狀恐怖,靈堂內的尸體忽然破棺而出讹挎,到底是詐尸還是另有隱情校赤,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布筒溃,位于F島的核電站马篮,受9級特大地震影響,放射性物質發(fā)生泄漏怜奖。R本人自食惡果不足惜浑测,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望歪玲。 院中可真熱鬧尽爆,春花似錦、人聲如沸读慎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽夭委。三九已至幅狮,卻和暖如春募强,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背崇摄。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工擎值, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人逐抑。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓鸠儿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親厕氨。 傳聞我的和親對象是個殘疾皇子进每,可洞房花燭夜當晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內容

  • 國家電網公司企業(yè)標準(Q/GDW)- 面向對象的用電信息數據交換協議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,005評論 6 13
  • iPhone的標準推薦是CFNetwork 庫編程,其封裝好的開源庫是 cocoa AsyncSocket庫命斧,用它...
    Ethan_Struggle閱讀 2,247評論 2 12
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結起來就是把...
    Dove_iOS閱讀 27,162評論 30 470
  • 一年豐收麥和Born, 化肥農藥澆水錢田晚; 飯衣治病錢難添, 老農苦守幾分田国葬。 問時間情為何物贤徒,直到歲月蒼老了容顏;...
    李修竹閱讀 184評論 0 0