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

一:指針

1. 指針的定義

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

指針是不安全的:

  • 比如我們在創建一個對象的時候,是需要在堆分配內存空間的。但是這個內存空間的生命周期是有限的,也就意味著如果我們使用指針指向這塊內存空間,如果當前內存空間的生命周期到了(引用計數變為0),那么我們當前的指針就變成了未定義的行為了,也就變成了野指針。
  • 創建的內存空間是越界的,比如我創建了一個大小為 10 的數組,這個時候我們通過指針訪問到了 index = 11 的位置,這個時候就數組越界了,訪問了一個未知的內存空間。
  • 指針所指向的類型與內存的值類型不一致,也是不安全的。

2. 指針類型

Swift 中的指針分為兩類

  • typed pointer 指定數據類型指針
  • raw pointer 未指定數據類型的指針(原生指針)
    基本上我們接觸的指針有以下幾種
    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雖然只占用一個字節
8

我們可以看到

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

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

// 首先開辟一塊內存空間 byteCount: 當前總的字節大小 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 方法存儲當前的整型數值
    p.advanced(by:i * MemoryLayout<Int>.stride).storeBytes(of: i, as: Int.self)
}

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

// 釋放創建的連續的內存空間
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個連續的內存空間
var tptr = UnsafeMutablePointer<Person>.allocate(capacity: 5)

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

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

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

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

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

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

// 這里的元祖是值類型,本質上這塊內存空間中存放的就是Int類型的數據
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類型的數據
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:)
    當我們在給外部函數傳遞參數時,不免會有一些數據類型上的差距,如果我們進行類型轉換,必然要來會復制數據,這個時候就可以調用 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文件中的屬性和函數表

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

// 循環遍歷屬性
for i in 0..<fieldDescriptor!.numFields{
    // FieldRecord 結構體由 3個 4字節組成,并且保持3 * 4 = 12字節對齊
    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
// 函數的結構體
struct TargetMethodDescriptor {
    var kind: UInt32
    var offset: UInt32
}

// 獲取方法的數量
if let methods = classDescriptor?.methods {
    for i in 0..<methods {
        // 獲取v-table的的首地址
        let VTableRelaticveAddress = typeDescOffset + 4 * 13 + mhHeaderPtr_IntRepresentation
        // 獲取當前函數的地址
        let currentMethodAddress = VTableRelaticveAddress + UInt64(i) * UInt64(MemoryLayout<TargetMethodDescriptor>.size)
        // 將 當前函數 的內存地址直接轉換成指向 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,發現沒有正確獲取到程序運行的基地址,但是在 Xcode 12 中不會出現這樣的問題。

_dyld_get_image_header(0)對比image list.png

發現 _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
    }
}

就是循環遍歷 _dyld_get_image_header 中的元素判斷是不是 mach-o 的執行地址。

二:內存管理

Swift 中使用自動引用計數(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

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

p1 = nil
p2 = nil

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

p3 = nil

// 打印結果
小明 is being deinitialized

2. 弱引用

弱引用不會對其引用的實例保持強引用,因而不會阻止 ARC 銷毀被引用的實例。這個特性阻止了引用變為循環強引用。聲明屬性或者變量時,在前面加上 weak 關鍵字表明這是一個弱引用。
因為弱引用不會保持所引用的實例,即使引用存在,實例也有可能被銷毀。因此,ARC 會在引用的實例被銷毀后自動將其弱引用賦值為 nil。并且因為弱引用需要在運行時允許被賦值為 nil,所以它們一定是可選類型。

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

weak var t = Person()

進入匯編代碼

weak的匯編代碼.png

我們可以看到這里的實質是調用了 swift_weakInit 函數,根據 Swift 源碼的分析,其內部實現其實就是:一個對象在初始化的時候后是沒有 SideTable (散列表)的,當我們創建一個弱引用的時候,系統會創建一個 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>.的實現。
  • InlineRefCountBitsSideTableRefCountBits 共享當前模板類 RefCountBitsT<bool>
  • InlineRefCounts 其實是一個 uint64_t 可以當引用計數也可以當Side Table 的指針
  • SideTableRefCounts 是一種名為 HeapObjectSideTableEntry 的結構體,里面也有 RefCounts 成員,內部是 SideTableRefCountBits ,其實就是原來的 uint64_t 加上一個存儲弱引用數的 uint32_t

3. 無主引用

和弱引用類似,無主引用不會牢牢保持住引用的實例。和弱引用不同的是,無主引用在其他實例有相同或者更長的生命周期時使用。你可以在聲明屬性或者變量時,在前面加上關鍵字 unowned 表示這是一個無主引用。
但和弱引用不同,無主引用通常都被期望擁有值。所以,將值標記為無主引用不會將它變為可選類型,ARC 也不會將無主引用的值設置為 nil 。總之一句話就是,無主引用假定是永遠有值的。

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

4. 閉包循環引用

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

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

我們發現沒有打印 Person deinit ,也就意味著 t 并沒有被銷毀,此時出現了循環引用。解決辦法:就是使用捕獲列表

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

5. 捕獲列表

默認情況下,閉包表達式從起周圍的范圍捕獲常量和變量,并強引用這些值。可以使用捕獲列表來顯式控制如何在閉包中捕獲值。
在參數列表之前,捕獲列表被寫為用逗號括起來的表達式列表,并用方括號括起來。如果使用捕獲列表,則即使省略參數名稱,參數類型和返回類型,也必須使用 in 關鍵字。
創建閉包時,將初始化捕獲列表中的條目。對于捕獲列表中的每個條目,將常量初始化為在周圍范圍內具有相同名稱的常量或變量的值。

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

// 打印結果
0
1.85

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,460評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,067評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,467評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,468評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,184評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,582評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,616評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,794評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,343評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,096評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,291評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,863評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,513評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,941評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,190評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,026評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,253評論 2 375

推薦閱讀更多精彩內容