Swift探索(四): 指針和內存管理

一:指針

1. 指針的定義

Swift 中引用了某個引用類型實例的常量或變量,與 C 語言中的指針類似,不過它并不直接指向某個內存地址几于,也不要求你使用星號(*)來表明你在創(chuàng)建一個引用讳侨。相反,Swift 中引用的定義方式與其它的常量或變量的一樣戳杀。

指針是不安全的:

  • 比如我們在創(chuàng)建一個對象的時候,是需要在堆分配內存空間的。但是這個內存空間的生命周期是有限的桂敛,也就意味著如果我們使用指針指向這塊內存空間功炮,如果當前內存空間的生命周期到了(引用計數(shù)變?yōu)?),那么我們當前的指針就變成了未定義的行為了埠啃,也就變成了野指針死宣。
  • 創(chuàng)建的內存空間是越界的,比如我創(chuàng)建了一個大小為 10 的數(shù)組碴开,這個時候我們通過指針訪問到了 index = 11 的位置毅该,這個時候就數(shù)組越界了,訪問了一個未知的內存空間潦牛。
  • 指針所指向的類型與內存的值類型不一致眶掌,也是不安全的。

2. 指針類型

Swift 中的指針分為兩類

  • typed pointer 指定數(shù)據(jù)類型指針
  • raw pointer 未指定數(shù)據(jù)類型的指針(原生指針)
    基本上我們接觸的指針有以下幾種
    Swift的指針和OC的指針對比.png
2.1 原生指針

首先來了解一下步長信息

struct Person {
    var age: Int = 18
    var sex: Bool = true
}

print(MemoryLayout<Person>.size) // 真實大小
print(MemoryLayout<Person>.stride) // 步長信息
print(MemoryLayout<Person>.alignment) // 對齊信息

// 打印結果
9 // 8(int) + 1(bool)
16 // 8 + 8  bool雖然只占用一個字節(jié)
8

我們可以看到

  • size 的結果是 9 = int8字節(jié) + bool1 字節(jié)
  • stride 的結果是 16, 因為 alignment 的值為 8 巴碗,也就是說是按照 8 字節(jié)對齊朴爬,所以步長信息為 8 + 8 = 16 字節(jié)。

接下來使用原生指針 (Raw Pointer) 存儲4個整型的數(shù)據(jù)
示例代碼

// 首先開辟一塊內存空間 byteCount: 當前總的字節(jié)大小 4 x 8 = 32 alignment: 對齊的大小
let p = UnsafeMutableRawPointer.allocate(byteCount: 4 * 8, alignment: 8)

for i in 0..<4 {
    // 調用 advanced 獲取到每個地址排列的過程中應該距離首地址的大小 i x MemoryLayout<Int>.stride
    // 調用 store 方法存儲當前的整型數(shù)值
    p.advanced(by:i * MemoryLayout<Int>.stride).storeBytes(of: i, as: Int.self)
}

for i in 0..<4 {
    // 調用 load 方法加載當前指針當中對應的內存數(shù)據(jù)
    let value = p.load(fromByteOffset: i * 8, as: Int.self)
    print("index--\(i), value--\(value)")
}

// 釋放創(chuàng)建的連續(xù)的內存空間
p.deallocate()

// 打印結果
index--0, value--0
index--1, value--1
index--2, value--2
index--3, value--3
2.2 類型指針

類型指針相較于原生指針來說橡淆,其實就是指定當前指針已經綁定到了具體的類型召噩,在進行類型指針訪問的過程中,我們不再使用 storeload 方法進行存儲操操作逸爵,而是直接使用類型指針內置的變量 pointee
獲取 UnsafePointer 有兩種方式

  • 通過已有變量獲取
var age = 18

// 通過 withUnsafePointer 來訪問到當前變量的地址
withUnsafePointer(to: &age) { ptr in
    print(ptr)
}

age = withUnsafePointer(to: &age) { ptr in
    //注意這里我們不能直接修改ptr.pointee
    return ptr.pointee + 12
}

var b = 18

// 使用mutable修改ptr.pointee
withUnsafeMutablePointer(to: &b) { ptr in
    ptr.pointee += 10
    print(ptr)
}
  • 直接分配內存
var age = 10

// 分配一塊int類型內存空間, 注意當前內存空間還沒被初始化
let tPtr = UnsafeMutablePointer<Int>.allocate(capacity: 1)

// 初始化分配內存空間
tPtr.initialize(to: age)

// 訪問當前內存的值, 直接通過pointee進行訪問
print(tPtr.pointee)`

類型指針主要涉及到的api主要有


類型指針api.png

示例

struct Person {
    var age = 18
    var name = "小明"
}

// 方式一
// capacity 內存空間 5個連續(xù)的內存空間
var tptr = UnsafeMutablePointer<Person>.allocate(capacity: 5)

// tptr就是當前分配的內存空間的首地址
tptr[0] = Person.init(age: 18, name: "小明")
tptr[1] = Person.init(age: 19, name: "小強")

// 這兩個是成對出現(xiàn)的
// 清除內存空間中內容
tptr.deinitialize(count: 5)
// 回收內存空間
tptr.deallocate()

// 方式二
// 開辟2個連續(xù)的內存空間
let p = UnsafeMutablePointer<Person>.allocate(capacity: 2)
p.initialize(to: Person())
p.advanced(by: MemoryLayout<Person>.stride).initialize(to: Person(age: 18, name: "小明"))

// 當前程序運行完成后 執(zhí)行defer
defer {
    // 這兩個是成對出現(xiàn)的
    p.deinitialize(count: 2)
    p.deallocate()
}
2.3 內存指針的使用-內存綁定

Swift 提供了三種不同的API來綁定/重新綁定指針:

  • assumingMemoryBound(to:)
    有些時候我們處理代碼的過程中只有原生指針(沒有報錯指針類型)具滴,但此刻對于處理代碼的的我們來說明確知道指針的類型,我們就可以使用 assumingMemoryBound(to:) 來告訴編譯器預期的類型师倔。
    (注意:這里只是讓編譯器繞過類型檢查构韵,并沒有發(fā)生實際的類型轉換)
func testPointer(_ p: UnsafePointer<Int>) {
    print(p[0])
    print(p[1])
}

// 這里的元祖是值類型,本質上這塊內存空間中存放的就是Int類型的數(shù)據(jù)
let tuple = (10, 20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
    // 先將tuplePtr 轉換成原生指針趋艘, 在調用assumingMemoryBound(to:) 告訴編譯器當前內存已經綁定過Int了疲恢,這個時候編譯器就不會進行檢查
    testPointer(UnsafeRawPointer(tuplePtr).assumingMemoryBound(to: Int.self))
}
  • bindMemory(to: capacity:)
    用于更改內存綁定的類型,如果當前內存還沒有類型綁定瓷胧,則將首次綁定為該類型显拳,否則重新綁定該類型,并且內存中所有的值都會變成該類型
func testPointer(_ p: UnsafePointer<Int>) {
    print(p[0])
    print(p[1])
}

// 這里的元祖是值類型抖单,本質上這塊內存空間中存放的就是Int類型的數(shù)據(jù)
let tuple = (10, 20)
withUnsafePointer(to: tuple) { (tuplePtr: UnsafePointer<(Int, Int)>) in
    // 先將tuplePtr 轉換成原生指針萎攒, 將原生指針轉換成UnsafePointer<Int>類型
    testPointer(UnsafeRawPointer(tuplePtr).bindMemory(to: Int.self, capacity: 1))
}
  • withMemoryRebound(to: capacity: body:)
    當我們在給外部函數(shù)傳遞參數(shù)時,不免會有一些數(shù)據(jù)類型上的差距矛绘,如果我們進行類型轉換耍休,必然要來會復制數(shù)據(jù),這個時候就可以調用 withMemoryRebound(to: capacity: body:) 來臨時更改內存綁定類型货矮。
func testPointer(_ p: UnsafePointer<Int8>) {
    print(p[0])
    print(p[1])
}

let uint8Ptr = UnsafePointer<uint8>.init(bitPattern: 10)
// 減少代碼復雜度
uint8Ptr?.withMemoryRebound(to: Int8.self, capacity: 1) { (int8Ptr: UnsafePointer<Int8>) in
    testPointer(int8Ptr)
    
}

3.利用指針還原Macho文件中的屬性和函數(shù)表

class Person {
    var age: Int = 18
    var name: String = "小明"
}

var size: UInt = 0
//__swift5_types section 的pFile
var typesPtr = getsectdata("__TEXT", "__swift5_types", &size)

// 獲取當前程序運行地址 相當于 LLDB 中 image list 命令
var mhHeaderPtr = _dyld_get_image_header(0)

// 獲取 __LINKEDIT 中的內容 其中 getsegbyname 返回的是 UnsafePointer<segment_command_64>,  segment_command_64 就包含了 vmaddr(虛擬內存地址) 和 fileoff(偏移量)
var setCommond64LinkeditPtr = getsegbyname("__LINKEDIT")

// 計算鏈接的基地址
var linkBaseAddress: UInt64 = 0
if let vmaddr = setCommond64LinkeditPtr?.pointee.vmaddr, let fileOff = setCommond64LinkeditPtr?.pointee.fileoff{
    linkBaseAddress = vmaddr - fileOff
}

// 或者 直接去 LC_SEGMENT_64(__PAGEZERO)中的VM Size
var setCommond64PageZeroPtr = getsegbyname("__PAGEZERO")
if let vmsize = setCommond64PageZeroPtr?.pointee.vmsize {
    linkBaseAddress = vmsize
}

// 獲取__TEXT, __swift5_types 在Macho中的偏移量
var typesOffSet: UInt64 = 0
if let unwrappedPtr = typesPtr {
    // 將當前的地址信息轉換成UInt64
    let intRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: unwrappedPtr)))
    typesOffSet = intRepresentation - linkBaseAddress
}

// 程序運行的首地址 轉換成UInt64類型
let mhHeaderPtr_IntRepresentation = UInt64(bitPattern: Int64(Int(bitPattern: mhHeaderPtr)))

// DataLo的內存地址
var dataLoAddress = mhHeaderPtr_IntRepresentation + typesOffSet

// 轉換成指針類型
var dataLoAddressPtr = withUnsafePointer(to: &dataLoAddress){return $0}

// 獲取dataLo指針指向的內容
var dataLoContent = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: dataLoAddress) ?? 0)?.pointee

// 獲取typeDescriptor的偏移量
let typeDescOffset = UInt64(dataLoContent!) + typesOffSet - linkBaseAddress

// 獲取typeDescriptor在程序運行中的地址
var typeDescAddress = typeDescOffset + mhHeaderPtr_IntRepresentation

// typeDescriptor結構體
struct TargetClassDescriptor{
    var flags: UInt32
    var parent: UInt32
    var name: Int32
    var accessFunctionPointer: Int32
    var fieldDescriptor: Int32
    var superClassType: Int32
    var metadataNegativeSizeInWords: UInt32
    var metadataPositiveSizeInWords: UInt32
    var numImmediateMembers: UInt32
    var numFields: UInt32
    var fieldOffsetVectorOffset: UInt32
    var Offset: UInt32
    var methods: UInt32
}

// 將 typeDescriptor 的內存地址直接轉換成指向 TargetClassDescriptor 結構體的指針
let classDescriptor = UnsafePointer<TargetClassDescriptor>.init(bitPattern: Int(exactly: typeDescAddress) ?? 0)?.pointee

if let name = classDescriptor?.name {
    // 獲取name的偏移量地址
    let nameOffset = Int64(name) + Int64(typeDescOffset) + 8
    // 獲取name在運行中的內存地址
    let nameAddress = nameOffset + Int64(mhHeaderPtr_IntRepresentation)
    if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(nameAddress)){
        print(String(cString: cChar))
    }
}

// 獲取屬性
// 獲取屬性相關的filedDescriptor 在運行中的內存地址
let filedDescriptorRelaticveAddress = typeDescOffset + 4 * 4 + mhHeaderPtr_IntRepresentation

struct FieldDescriptor  {
    var mangledTypeName: Int32
    var superclass: Int32
    var Kind: UInt16
    var fieldRecordSize: UInt16
    var numFields: UInt32
    var fieldRecords: [FieldRecord]
}

struct FieldRecord{
    var Flags: UInt32
    var mangledTypeName: Int32
    var fieldName: UInt32
}

// 獲取fieldDescriptor 指針在的內容 就是FieldDescriptor 的偏移量
let fieldDescriptorOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: filedDescriptorRelaticveAddress) ?? 0)?.pointee

// 獲取 FieldDescriptor 的在運行中的內存地址
let fieldDescriptorAddress = filedDescriptorRelaticveAddress + UInt64(fieldDescriptorOffset!)

// 將 FieldDescriptor 的內存地址直接轉換成指向 FieldDescriptor 結構體的指針
let fieldDescriptor = UnsafePointer<FieldDescriptor>.init(bitPattern: Int(exactly: fieldDescriptorAddress) ?? 0)?.pointee

// 循環(huán)遍歷屬性
for i in 0..<fieldDescriptor!.numFields{
    // FieldRecord 結構體由 3個 4字節(jié)組成羊精,并且保持3 * 4 = 12字節(jié)對齊
    let stride: UInt64 = UInt64(i * 3 * 4)
    // 計算 fieldRecord 的地址
    let fieldRecordAddress = fieldDescriptorAddress + stride + 16
    // 計算 fieldRecord 結構體中的 name 在程序運行中的內存地址
    let fieldNameRelactiveAddress = UInt64(2 * 4) + fieldRecordAddress - linkBaseAddress + mhHeaderPtr_IntRepresentation
    // 將上面地址的地址轉換成指針,并且獲取指向的內容 (偏移量)
    let nameOffset = UnsafePointer<UInt32>.init(bitPattern: Int(exactly: fieldNameRelactiveAddress) ?? 0)?.pointee
    
    // 獲取 name 的地址
    let fieldNameAddress = fieldNameRelactiveAddress + UInt64(nameOffset!) - linkBaseAddress
    // 將 name 地址轉換成指針
    if let cChar = UnsafePointer<CChar>.init(bitPattern: Int(fieldNameAddress)){
        // 打印指針內容
        print(String(cString: cChar))
    }
}


// 獲取v-table
// 函數(shù)的結構體
struct TargetMethodDescriptor {
    var kind: UInt32
    var offset: UInt32
}

// 獲取方法的數(shù)量
if let methods = classDescriptor?.methods {
    for i in 0..<methods {
        // 獲取v-table的的首地址
        let VTableRelaticveAddress = typeDescOffset + 4 * 13 + mhHeaderPtr_IntRepresentation
        // 獲取當前函數(shù)的地址
        let currentMethodAddress = VTableRelaticveAddress + UInt64(i) * UInt64(MemoryLayout<TargetMethodDescriptor>.size)
        // 將 當前函數(shù) 的內存地址直接轉換成指向 TargetMethodDescriptor 結構體的指針
        let currentMethod = UnsafePointer<TargetMethodDescriptor>.init(bitPattern: Int(exactly: currentMethodAddress) ?? 0)?.pointee
        // 獲取到imp的地址
        let impAddress = currentMethodAddress + 4 + UInt64(currentMethod!.offset) - linkBaseAddress
        print(impAddress);
    }
}

注意: 在 Xcode 13_dyld_get_image_header(0) 對比在 LLDB 中輸入命令 image list,發(fā)現(xiàn)沒有正確獲取到程序運行的基地址喧锦,但是在 Xcode 12 中不會出現(xiàn)這樣的問題读规。

_dyld_get_image_header(0)對比image list.png

發(fā)現(xiàn) _dyld_get_image_header(0) 獲取到的地址是 image list 中第三個元素的地址,目前還沒找到解決辦法燃少,如果您正好知道請留意或者私信我束亏,萬分感謝。

  • 經過后面的研究這里找到一個方式獲取當前程序運行的基地址
var mhHeaderPtr: UnsafePointer<mach_header>?
let count = _dyld_image_count()
for i in 0..<count {
    var excute_header = _dyld_get_image_header(i)
    if excute_header!.pointee.filetype == MH_EXECUTE {
        mhHeaderPtr = excute_header
        break
    }
}

就是循環(huán)遍歷 _dyld_get_image_header 中的元素判斷是不是 mach-o 的執(zhí)行地址阵具。

二:內存管理

Swift 中使用自動引用計數(shù)(ARC)機制來追蹤和管理內存碍遍,通常情況下,Swift 內存管理機制會一直起作用阳液,你無須自己來考慮內存的管理怕敬。ARC 會在類的實例不再被使用時,自動釋放其占用的內存帘皿。

1. 強引用

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

var p1: Person?
var p2: Person?
var p3: Person?

p1 = Person(name: "小明")

// 打印結果
小明 is being initialized

由于 Person 類的新實例被賦值給了 p1 變量东跪,所以 p1Person 類的新實例之間建立了一個強引用。正是因為這一個強引用鹰溜,ARC 會保證 Person 實例被保持在內存中不被銷毀虽填。
我們接著添加代碼

p2 = p1
p3 = p1

現(xiàn)在這一個 Person 實例已經有三個強引用了。
將其中兩個變量賦值 nil 的方式斷開兩個強引用(包括最先的那個強引用)曹动,只留下一個強引用卤唉,Person 實例不會被銷毀

p1 = nil
p2 = nil

只有當最后一個引用被斷開時 ARC 才會銷毀它

p3 = nil

// 打印結果
小明 is being deinitialized

2. 弱引用

弱引用不會對其引用的實例保持強引用,因而不會阻止 ARC 銷毀被引用的實例仁期。這個特性阻止了引用變?yōu)檠h(huán)強引用。聲明屬性或者變量時竭恬,在前面加上 weak 關鍵字表明這是一個弱引用跛蛋。
因為弱引用不會保持所引用的實例,即使引用存在痊硕,實例也有可能被銷毀赊级。因此,ARC 會在引用的實例被銷毀后自動將其弱引用賦值為 nil岔绸。并且因為弱引用需要在運行時允許被賦值為 nil理逊,所以它們一定是可選類型。

class Person {
    var age: Int = 18
    var name: String = "小明"
}

weak var t = Person()

進入?yún)R編代碼

weak的匯編代碼.png

我們可以看到這里的實質是調用了 swift_weakInit 函數(shù)盒揉,根據(jù) Swift 源碼的分析晋被,其內部實現(xiàn)其實就是:一個對象在初始化的時候后是沒有 SideTable (散列表)的,當我們創(chuàng)建一個弱引用的時候刚盈,系統(tǒng)會創(chuàng)建一個 SideTable
實質上 Swift 存在兩種引用計算的布局方式

HeapObject {
    isa
    InlineRefCounts {
      atomic<InlineRefCountBits> {
        strong RC + unowned RC + flags
        OR
        HeapObjectSideTableEntry*
      }
    }
  }

  HeapObjectSideTableEntry {
    SideTableRefCounts {
      object pointer
      atomic<SideTableRefCountBits> {
        strong RC + unowned RC + weak RC + flags
      }
    }   
  }

其中

  • InlineRefCountsSideTableRefCounts 共享當前模板類 RefCounts<T>.的實現(xiàn)羡洛。
  • InlineRefCountBitsSideTableRefCountBits 共享當前模板類 RefCountBitsT<bool>
  • InlineRefCounts 其實是一個 uint64_t 可以當引用計數(shù)也可以當Side Table 的指針
  • SideTableRefCounts 是一種名為 HeapObjectSideTableEntry 的結構體,里面也有 RefCounts 成員藕漱,內部是 SideTableRefCountBits 欲侮,其實就是原來的 uint64_t 加上一個存儲弱引用數(shù)的 uint32_t

3. 無主引用

和弱引用類似崭闲,無主引用不會牢牢保持住引用的實例。和弱引用不同的是威蕉,無主引用在其他實例有相同或者更長的生命周期時使用刁俭。你可以在聲明屬性或者變量時,在前面加上關鍵字 unowned 表示這是一個無主引用韧涨。
但和弱引用不同牍戚,無主引用通常都被期望擁有值。所以氓奈,將值標記為無主引用不會將它變?yōu)榭蛇x類型翘魄,ARC 也不會將無主引用的值設置為 nil ∫蹋總之一句話就是暑竟,無主引用假定是永遠有值的。

  • 如果兩個對象的生命周期完全和對方沒關系(其中一方什么時候賦值為 nil 育勺,對對方沒有影響)但荤,使用 weak
  • 如果能確保:其中一個對象銷毀,另一個對象也要跟著銷毀涧至,這時候可以(謹慎)使用 unowned

4. 閉包循環(huán)引用

閉包會一般默認捕獲外部的變量

var age = 18

let closure = {
    age += 1
}
closure()
print(age)

// 打印結果
19

可以看出 閉包的內部對變量的修改將會改變外部原始變量的值

class Person {
    var age: Int = 18
    var name: String = "小明"
    
    var testClosure:(() -> ())?
    
    deinit {
        print("Person deinit")
    }
}

func testARC() {
    let t = Person()
    
    t.testClosure = {
        print(t.age)
    }
    
    print("end")
}

testARC()

// 打印結果
end

我們發(fā)現(xiàn)沒有打印 Person deinit 腹躁,也就意味著 t 并沒有被銷毀,此時出現(xiàn)了循環(huán)引用南蓬。解決辦法:就是使用捕獲列表

func testARC() {
    let t = Person()
    
    t.testClosure = { [weak t] in
        t?.age += 1
    }
//    t.testClosure = { [unowned t] in
//        t.age += 1
//    }
}

5. 捕獲列表

默認情況下纺非,閉包表達式從起周圍的范圍捕獲常量和變量,并強引用這些值赘方∩沼保可以使用捕獲列表來顯式控制如何在閉包中捕獲值。
在參數(shù)列表之前窄陡,捕獲列表被寫為用逗號括起來的表達式列表炕淮,并用方括號括起來。如果使用捕獲列表跳夭,則即使省略參數(shù)名稱涂圆,參數(shù)類型和返回類型,也必須使用 in 關鍵字币叹。
創(chuàng)建閉包時润歉,將初始化捕獲列表中的條目。對于捕獲列表中的每個條目颈抚,將常量初始化為在周圍范圍內具有相同名稱的常量或變量的值卡辰。

var age = 0
var height = 0.0
let closure = { [age] in
    print(age)
    print(height)
}
age = 10
height = 1.85
closure()

// 打印結果
0
1.85

創(chuàng)建閉包時,內部作用域中的 age 會用外部作用域中的 age 的值進行初始化,但他們的值未以任何特殊方式連接九妈。這意味著更改外部作用域中的 age 的值不會影響內部作用域中的 age 的值反砌,也不會更改封閉內部的值,也不會影響封閉外的值萌朱。先比之下宴树,只有一個名為 height 的變量-外部作用域中的 height - 因此,在閉包內部或外部進行的更改在兩個均可見晶疼。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末酒贬,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子翠霍,更是在濱河造成了極大的恐慌锭吨,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寒匙,死亡現(xiàn)場離奇詭異零如,居然都是意外死亡,警方通過查閱死者的電腦和手機锄弱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門考蕾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人会宪,你說我怎么就攤上這事肖卧。” “怎么了掸鹅?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵塞帐,是天一觀的道長。 經常有香客問我巍沙,道長壁榕,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任赎瞎,我火速辦了婚禮,結果婚禮上颊咬,老公的妹妹穿的比我還像新娘务甥。我一直安慰自己,他們只是感情好喳篇,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布敞临。 她就那樣靜靜地躺著,像睡著了一般麸澜。 火紅的嫁衣襯著肌膚如雪挺尿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機與錄音编矾,去河邊找鬼熟史。 笑死,一個胖子當著我的面吹牛窄俏,可吹牛的內容都是我干的蹂匹。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼凹蜈,長吁一口氣:“原來是場噩夢啊……” “哼限寞!你這毒婦竟也來了?” 一聲冷哼從身側響起仰坦,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤履植,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后悄晃,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體玫霎,經...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年传泊,在試婚紗的時候發(fā)現(xiàn)自己被綠了鼠渺。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡眷细,死狀恐怖拦盹,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情溪椎,我是刑警寧澤普舆,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站校读,受9級特大地震影響沼侣,放射性物質發(fā)生泄漏。R本人自食惡果不足惜歉秫,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一蛾洛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧雁芙,春花似錦轧膘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至洞焙,卻和暖如春蟆淀,著一層夾襖步出監(jiān)牢的瞬間拯啦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工熔任, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留褒链,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓笋敞,卻偏偏與公主長得像碱蒙,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子夯巷,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內容