Swift底層原理探索6----屬性 & 方法

屬性

struct Circle {
    //存儲屬性
    var radius: Double
    //計算屬性
    var diamiter: Double {
        set { 
            radius = newValue / 2
        }
        get {
            radius * 2
        }
    }
}
  • Swift中跟實例相關的屬性可以分為2大類
    • 存儲屬性(Stored Property
      • 類似于成員變量這個概念
      • 存儲在實例的內存中
        image
      • 結構體、類可以定義存儲屬性
        image
      • 枚舉不可以定義存儲屬性
        image

        我們知道枚舉的內存里面可以存放的是所有的case以及關聯值,并沒有所謂的成員變量概念,可因此也不存在所謂的存儲屬性
    • 計算屬性(Computed Property
      • 本質就是方法(函數)這個也可以通過匯編來證明一下
        image
        image
        image
        image
      • 不占用實例的內存
        image
      • 枚舉、結構體、類都可以定義計算屬性

存儲屬性

  • 關于存儲屬性, Swift有個明確的規定
    • 在創建結構體的時候,必須為所有的存儲屬性設置一個合適的初始值,也就是要求類/結構體創建實例后,它的全部內存要得到初始化,而存儲屬性正好就是放在實例的內存里面的,所以需要將所有的存儲屬性設置初始值。
      1. 可以在初始化器里為存儲屬性設置一個初始值
        image

        image
      2. 可以分配一個默認的屬性值作為屬性定義的一部分

計算屬性

  • set傳入的新值默認叫做newValue,也可以自定義
  • 定義計算屬性只能用var, 不能用let
    • let代表常量,也就是值是一成不變的
    • 計算屬性的值是可能發生變化的(即使是只讀計算屬性)
  • 只讀計算屬性:只有get, 沒有set

枚舉rawValue原理

  • 枚舉原始值rawValue的本質是:只讀計算屬性,直接看匯編就可以證明
    image
    image
    image

延遲存儲屬性(Lazy Stored Property)

看現這段代碼

class Car {
    init() {
        print("Car init")
    }
    func run() {
        print("Car is running!")
    }
}

class Person {
    var car = Car()
    init() {
        print("Person init")
    }
    func  goOut() {
        car.run()
    }
}

let p = Person()
print("-----------")
p.goOut()

運行結果如下

Car init
Person init
-----------
Car is running!
Program ended with exit code: 0

我們給上面代碼的car屬性增加一個關鍵字lazy修飾

class Car {
    init() {
        print("Car init")
    }
    func run() {
        print("Car is running!")
    }
}

class Person {
    lazy var car = Car()
    init() {
        print("Person init")
    }
    func  goOut() {
        car.run()
    }
}

let p = Person()
print("-----------")
p.goOut()

再看下現在的運行結果

Person init
-----------
Car init
Car is running!
Program ended with exit code: 0

可以看出,lazy的作用,是將屬性var car的初始化延遲到了它首次使用的時候進行,例子中也就是p.goOut()這句代碼執行的時候,才回去初始化屬性car

通過lazy 關鍵字修飾的存儲屬性就要做延遲存儲屬性,這個功能的好處是顯而易見的,因為有些屬性可能需要花費很多資源進行初始化,而很可能在某些極少情況下才會被觸發使用,所以lazy關鍵字就可以用在這種情況下,讓核心對象的初始化變得快速而輕量。比如下面這個例子

class PhotoView {
    lazy var image: Image = {
        let url = "https://www.520it.com/xx.png"
        let data = Data(url: url)
        return Image(dada: data)
    }()
}

網絡圖片的加載往往是需要一些時間的,上面例子里面圖片的加載過程封裝在閉包表達式里面,并且將其返回值作為了image屬性的初始化賦值,通過lazy,就講這個加載的過程推遲到了image在實際被用到的時候去執行,這樣就可以提升app順滑度,改善卡頓情況。

  • 使用lazy可以定義一個延遲存儲屬性,在第一次用到屬性的時候才會進行初始化
  • lazy屬性必須是var, 不能是let
    • 這個要求很容易理解,let必須在實例的初始化方法完成之前就擁有值,而lazy恰好是為了在實例創建并初始化之后的某個時刻對其某個屬性進行初始化賦值,所以lazy只能作用域var屬性
  • 如果多線程同時第一次訪問lazy屬性,無法保證屬性只被初始化1

延遲存儲屬性注意點

  • 當結構體包含一個延遲存儲屬性時,只有var才能訪問延遲存儲屬性
    因為延遲屬性初始化時需要改變結構體的內存
    image

    案例中,因為p是常量,所以內存的內容初始化之后不可以變化,但是p.z會使得結構體Pointlazy var z屬性進行初始化,因為結構體的成員是在結構體的內存里面的,因此就需要改變結構體的內存,因此便產生了后面的報錯。

屬性觀察器(Property Observer)

  • 可以為非lazyvar存儲屬性設置屬性觀察器
  • willSet會傳遞新值,默認叫做newValue
  • didSet會傳遞舊值,默認叫做oldValue
  • 在初始化器中設置屬性值不會出發willSetdidSet
  • 在屬性定義時設置初始值也不會出發willSetdidSet
struct Circle {
    var radius: Double {
        willSet {
            print("willSet", newValue)
        }

        didSet {
            print("didSet", oldValue, radius)
        }
    }

    init() {
        self.radius = 1.0
        print("Circle init!")
    }
}

var circle = Circle()
circle.radius = 10.5
print(circle.radius)

運行結果

Circle init!
willSet 10.5
didSet 1.0 10.5
10.5
Program ended with exit code: 0

全局變量、局部變量

屬性觀察器、計算屬性的功能,同樣可以應用在全局變量、局部變量身上

var num: Int {
   get {
       return 10
   }
   set {
       print("setNum", newValue)
   }
}
num = 12
print(num)


func test() {
   var age = 10 {
       willSet {
           print("willSet", newValue)
       }
       didSet {
           print("didSet", oldValue, age)
       }
   }

   age = 11
}
test()

inout的再次研究

首先看下面的代碼

func test(_ num: inout Int) {
    num = 20
}

var age = 10
test(&age) // 此處加斷點

將程序運行至斷點處,觀察匯編

SwiftTest`main:
    0x1000010b0 <+0>:  pushq  %rbp
    0x1000010b1 <+1>:  movq   %rsp, %rbp
    0x1000010b4 <+4>:  subq   $0x30, %rsp
    0x1000010b8 <+8>:  leaq   0x6131(%rip), %rax        ; SwiftTest.age : Swift.Int
    0x1000010bf <+15>: xorl   %ecx, %ecx
    0x1000010c1 <+17>: movq   $0xa, 0x6124(%rip)        ; demangling cache variable for type metadata for Swift.Array<Swift.UInt8> + 4
    0x1000010cc <+28>: movl   %edi, -0x1c(%rbp)
->  0x1000010cf <+31>: movq   %rax, %rdi
    0x1000010d2 <+34>: leaq   -0x18(%rbp), %rax
    0x1000010d6 <+38>: movq   %rsi, -0x28(%rbp)
    0x1000010da <+42>: movq   %rax, %rsi
    0x1000010dd <+45>: movl   $0x21, %edx
    0x1000010e2 <+50>: callq  0x10000547c               ; symbol stub for: swift_beginAccess
    0x1000010e7 <+55>: leaq   0x6102(%rip), %rdi        ; SwiftTest.age : Swift.Int
    0x1000010ee <+62>: callq  0x100001110               ; SwiftTest.test(inout Swift.Int) -> () at main.swift:658
    0x1000010f3 <+67>: leaq   -0x18(%rbp), %rdi
    0x1000010f7 <+71>: callq  0x10000549a               ; symbol stub for: swift_endAccess
    0x1000010fc <+76>: xorl   %eax, %eax
    0x1000010fe <+78>: addq   $0x30, %rsp
    0x100001102 <+82>: popq   %rbp
    0x100001103 <+83>: retq  

我們可以看到函數test調用之前,參數的傳遞情況如下

image

對于上述比較簡單的情況,我們知道inout的本質就是進行引用傳遞,接下來,我們考慮一些更加復雜的情況

struct Shape {
    var width: Int
    var side: Int {
        willSet {
            print("willSetSide", newValue)
        }
        didSet {
            print("didSetSide", oldValue, side)
        }
    }

    var girth: Int {
        set {
            width = newValue / side
            print("setGirth", newValue)
        }
        get {
            print("getGirth")
            return width * side
        }
    }

    func show() {
        print("width= \(width), side= \(side), girth= \(girth)")
    }
}


func test(_ num: inout Int) {
    num = 20
}



var s = Shape(width: 10, side: 4)
test(&s.width)  // 斷點1
s.show()
print("-------------")
test(&s.side)   //斷點2
s.show()
print("-------------")
test(&s.girth)  //斷點3
s.show()
print("-------------")

上述案例里面,全局變量s的類型是結構體 Struct Shape,它的內存放的是兩個存儲屬性widthside,其中side帶有屬性觀察器,另外Shape還有一個計算屬性girth,我們首先不加斷點運行一下程序,觀察一下運行結果

getGirth
width= 20, side= 4, girth= 80
-------------
willSetSide 20
didSetSide 4 20
getGirth
width= 20, side= 20, girth= 400
-------------
getGirth
setGirth 20
getGirth
width= 1, side= 20, girth= 20
-------------
Program ended with exit code: 0

看得出來,inout對于三種屬性都產生了作用,那么它的底層到底是如何處理和實現的呢?我們還是要通過匯編來一探究竟。便于匯編分析,我們截取部分代碼進行編譯運行




首先看普通的屬性

struct Shape {
    var width: Int
    var side: Int {
        willSet {
            print("willSetSide", newValue)
        }
        didSet {
            print("didSetSide", oldValue, side)
        }
    }

    var girth: Int {
        set {
            width = newValue / side
            print("setGirth", newValue)
        }
        get {
            print("getGirth")
            return width * side
        }
    }

    func show() {
        print("width= \(width), side= \(side), girth= \(girth)")
    }
}


func test(_ num: inout Int) {
    num = 20
}

var s = Shape(width: 10, side: 4)
test(&s.width) // 斷點處,傳入普通屬性width作為test的inout參數

匯編結果如下

SwiftTest`main:
    0x100001310 <+0>:   pushq  %rbp
    0x100001311 <+1>:   movq   %rsp, %rbp
    0x100001314 <+4>:   subq   $0x30, %rsp
    0x100001318 <+8>:   movl   $0xa, %eax
    0x10000131d <+13>:  movl   %edi, -0x1c(%rbp)
    0x100001320 <+16>:  movq   %rax, %rdi
    0x100001323 <+19>:  movl   $0x4, %eax
    0x100001328 <+24>:  movq   %rsi, -0x28(%rbp)
    0x10000132c <+28>:  movq   %rax, %rsi
    0x10000132f <+31>:  callq  0x100001d60               ; SwiftTest.Shape.init(width: Swift.Int, side: Swift.Int) -> SwiftTest.Shape at main.swift:630
    0x100001334 <+36>:  leaq   0x6ebd(%rip), %rcx        ; SwiftTest.s : SwiftTest.Shape
    0x10000133b <+43>:  xorl   %r8d, %r8d
    0x10000133e <+46>:  movl   %r8d, %esi
    0x100001341 <+49>:  movq   %rax, 0x6eb0(%rip)        ; SwiftTest.s : SwiftTest.Shape
    0x100001348 <+56>:  movq   %rdx, 0x6eb1(%rip)        ; SwiftTest.s : SwiftTest.Shape + 8
->  0x10000134f <+63>:  movq   %rcx, %rdi
    0x100001352 <+66>:  leaq   -0x18(%rbp), %rax
    0x100001356 <+70>:  movq   %rsi, -0x30(%rbp)
    0x10000135a <+74>:  movq   %rax, %rsi
    0x10000135d <+77>:  movl   $0x21, %edx
    0x100001362 <+82>:  movq   -0x30(%rbp), %rcx
    0x100001366 <+86>:  callq  0x100006312               ; symbol stub for: swift_beginAccess
    0x10000136b <+91>:  leaq   0x6e86(%rip), %rdi        ; SwiftTest.s : SwiftTest.Shape
    0x100001372 <+98>:  callq  0x100001d70               ; SwiftTest.test(inout Swift.Int) -> () at main.swift:658
    0x100001377 <+103>: leaq   -0x18(%rbp), %rdi
    0x10000137b <+107>: callq  0x100006330               ; symbol stub for: swift_endAccess
    0x100001380 <+112>: xorl   %eax, %eax
    0x100001382 <+114>: addq   $0x30, %rsp
    0x100001386 <+118>: popq   %rbp
    0x100001387 <+119>: retq

參數傳遞流程如下圖


image

所以對于普通的存儲屬性test函數是直接將它的地址值傳入。



接下來便于直觀的對比,我們再看一下計算屬性的情況

struct Shape {
    var width: Int
    var side: Int {
        willSet {
            print("willSetSide", newValue)
        }
        didSet {
            print("didSetSide", oldValue, side)
        }
    }

    var girth: Int {
        set {
            width = newValue / side
            print("setGirth", newValue)
        }
        get {
            print("getGirth")
            return width * side
        }
    }

    func show() {
        print("width= \(width), side= \(side), girth= \(girth)")
    }
}

func test(_ num: inout Int) {
    print("開始test函數")
    num = 20
}

var s = Shape(width: 10, side: 4)
test(&s.girth)

斷點處匯編如下

SwiftTest`main:
    0x1000012f0 <+0>:   pushq  %rbp
    0x1000012f1 <+1>:   movq   %rsp, %rbp
    0x1000012f4 <+4>:   pushq  %r13
    0x1000012f6 <+6>:   subq   $0x38, %rsp
    0x1000012fa <+10>:  movl   $0xa, %eax
    0x1000012ff <+15>:  movl   %edi, -0x2c(%rbp)
    0x100001302 <+18>:  movq   %rax, %rdi
    0x100001305 <+21>:  movl   $0x4, %eax
    0x10000130a <+26>:  movq   %rsi, -0x38(%rbp)
    0x10000130e <+30>:  movq   %rax, %rsi
    0x100001311 <+33>:  callq  0x100001d60               ; SwiftTest.Shape.init(width: Swift.Int, side: Swift.Int) -> SwiftTest.Shape at main.swift:630
    0x100001316 <+38>:  leaq   0x6edb(%rip), %rcx        ; SwiftTest.s : SwiftTest.Shape
    0x10000131d <+45>:  xorl   %r8d, %r8d
    0x100001320 <+48>:  movl   %r8d, %esi
    0x100001323 <+51>:  movq   %rax, 0x6ece(%rip)        ; SwiftTest.s : SwiftTest.Shape
    0x10000132a <+58>:  movq   %rdx, 0x6ecf(%rip)        ; SwiftTest.s : SwiftTest.Shape + 8
->  0x100001331 <+65>:  movq   %rcx, %rdi
    0x100001334 <+68>:  leaq   -0x20(%rbp), %rax
    0x100001338 <+72>:  movq   %rsi, -0x40(%rbp)
    0x10000133c <+76>:  movq   %rax, %rsi
    0x10000133f <+79>:  movl   $0x21, %edx
    0x100001344 <+84>:  movq   -0x40(%rbp), %rcx
    0x100001348 <+88>:  callq  0x100006312               ; symbol stub for: swift_beginAccess
    0x10000134d <+93>:  movq   0x6ea4(%rip), %rdi        ; SwiftTest.s : SwiftTest.Shape
    0x100001354 <+100>: movq   0x6ea5(%rip), %rsi        ; SwiftTest.s : SwiftTest.Shape + 8
    0x10000135b <+107>: callq  0x1000016d0               ; SwiftTest.Shape.girth.getter : Swift.Int at main.swift:646
    0x100001360 <+112>: movq   %rax, -0x28(%rbp)
    0x100001364 <+116>: leaq   -0x28(%rbp), %rdi
    0x100001368 <+120>: callq  0x100001d70               ; SwiftTest.test(inout Swift.Int) -> () at main.swift:658
    0x10000136d <+125>: movq   -0x28(%rbp), %rdi
    0x100001371 <+129>: leaq   0x6e80(%rip), %r13        ; SwiftTest.s : SwiftTest.Shape
    0x100001378 <+136>: callq  0x100001820               ; SwiftTest.Shape.girth.setter : Swift.Int at main.swift:642
    0x10000137d <+141>: leaq   -0x20(%rbp), %rdi
    0x100001381 <+145>: callq  0x100006330               ; symbol stub for: swift_endAccess
    0x100001386 <+150>: xorl   %eax, %eax
    0x100001388 <+152>: addq   $0x38, %rsp
    0x10000138c <+156>: popq   %r13
    0x10000138e <+158>: popq   %rbp
    0x10000138f <+159>: retq 

這一次從匯編代碼量就可以判斷,對于計算屬性的處理肯定比存儲屬性要復雜,還是通過圖例來展示一下整個過程


image

image

可以看出,由于計算屬性在實例內部沒有對應的內存空間,編譯器通過在函數棧里面開辟一個局部變量的方法,利用它作為計算屬性的值的臨時宿主,并且將該局部變量的地址作為test函數的inout參數傳入函數,所以本質上,仍然是引用傳遞

test函數調用前,計算屬性值給復制到局部變量上,以及test函數調用之后,局部變量的值傳遞給setter函數的這兩個過程,被蘋果成為 Copy In Copy Out,上面案例代碼的運行結果也驗證了這個結論

getGirth
開始test函數
setGirth 20
Program ended with exit code: 0




最后,我們來看對于帶有屬性觀察器的存儲屬性,處理過程會有哪些獨到之處

struct Shape {
    var width: Int
    var side: Int {
        willSet {
            print("willSetSide", newValue)
        }
        didSet {
            print("didSetSide", oldValue, side)
        }
    }

    var girth: Int {
        set {
            width = newValue / side
            print("setGirth", newValue)
        }
        get {
            print("getGirth")
            return width * side
        }
    }

    func show() {
        print("width= \(width), side= \(side), girth= \(girth)")
    }
}


func test(_ num: inout Int) {
    num = 20
}

var s = Shape(width: 10, side: 4)
test(&s.side) //side是帶屬性觀察期的存儲屬性, 斷點在這里

斷點處匯編結果如下

SwiftTest`main:
    0x100001230 <+0>:   pushq  %rbp
    0x100001231 <+1>:   movq   %rsp, %rbp
    0x100001234 <+4>:   pushq  %r13
    0x100001236 <+6>:   subq   $0x38, %rsp
    0x10000123a <+10>:  movl   $0xa, %eax
    0x10000123f <+15>:  movl   %edi, -0x2c(%rbp)
    0x100001242 <+18>:  movq   %rax, %rdi
    0x100001245 <+21>:  movl   $0x4, %eax
    0x10000124a <+26>:  movq   %rsi, -0x38(%rbp)
    0x10000124e <+30>:  movq   %rax, %rsi
    0x100001251 <+33>:  callq  0x100001ca0               ; SwiftTest.Shape.init(width: Swift.Int, side: Swift.Int) -> SwiftTest.Shape at main.swift:630
    0x100001256 <+38>:  leaq   0x6f9b(%rip), %rcx        ; SwiftTest.s : SwiftTest.Shape
    0x10000125d <+45>:  xorl   %r8d, %r8d
    0x100001260 <+48>:  movl   %r8d, %esi
    0x100001263 <+51>:  movq   %rax, 0x6f8e(%rip)        ; SwiftTest.s : SwiftTest.Shape
    0x10000126a <+58>:  movq   %rdx, 0x6f8f(%rip)        ; SwiftTest.s : SwiftTest.Shape + 8
->  0x100001271 <+65>:  movq   %rcx, %rdi
    0x100001274 <+68>:  leaq   -0x20(%rbp), %rax
    0x100001278 <+72>:  movq   %rsi, -0x40(%rbp)
    0x10000127c <+76>:  movq   %rax, %rsi
    0x10000127f <+79>:  movl   $0x21, %edx
    0x100001284 <+84>:  movq   -0x40(%rbp), %rcx
    0x100001288 <+88>:  callq  0x100006302               ; symbol stub for: swift_beginAccess
    0x10000128d <+93>:  movq   0x6f6c(%rip), %rax        ; SwiftTest.s : SwiftTest.Shape + 8
    0x100001294 <+100>: movq   %rax, -0x28(%rbp)
    0x100001298 <+104>: leaq   -0x28(%rbp), %rdi
    0x10000129c <+108>: callq  0x100001cb0               ; SwiftTest.test(inout Swift.Int) -> () at main.swift:658
    0x1000012a1 <+113>: movq   -0x28(%rbp), %rdi
    0x1000012a5 <+117>: leaq   0x6f4c(%rip), %r13        ; SwiftTest.s : SwiftTest.Shape
    0x1000012ac <+124>: callq  0x100001350               ; SwiftTest.Shape.side.setter : Swift.Int at main.swift:632
    0x1000012b1 <+129>: leaq   -0x20(%rbp), %rdi
    0x1000012b5 <+133>: callq  0x100006320               ; symbol stub for: swift_endAccess
    0x1000012ba <+138>: xorl   %eax, %eax
    0x1000012bc <+140>: addq   $0x38, %rsp
    0x1000012c0 <+144>: popq   %r13
    0x1000012c2 <+146>: popq   %rbp
    0x1000012c3 <+147>: retq   
image

這次,我們發現跟計算屬性有些類似,這里也用到了函數棧的局部變量,它的作用是用來承載計算屬性的值,然后被傳入test函數的同樣是這個局部變量的地址(引用),但是我很好奇為何要多此一舉,計算屬性因為本身沒有固定的內存,所以很好理解必須借助局部變臉作為臨時宿主,但是計算屬性是有固定內存的,可以猜的到,這么設計的原因肯定跟屬性觀察器有關,但是目前的代碼還不足以解釋這么設計的意圖,但是我們看到這里最后一步,調用了side.setter函數,???side是存儲屬性,怎么會有setter函數呢?那我們就進入它內部看看嘍,它的匯編如下

SwiftTest`Shape.side.setter:
->  0x100001350 <+0>:  pushq  %rbp
    0x100001351 <+1>:  movq   %rsp, %rbp
    0x100001354 <+4>:  pushq  %r13
    0x100001356 <+6>:  subq   $0x28, %rsp
    0x10000135a <+10>: movq   $0x0, -0x10(%rbp)
    0x100001362 <+18>: movq   $0x0, -0x18(%rbp)
    0x10000136a <+26>: movq   %rdi, -0x10(%rbp)
    0x10000136e <+30>: movq   %r13, -0x18(%rbp)
    0x100001372 <+34>: movq   0x8(%r13), %rax
    0x100001376 <+38>: movq   %rax, %rcx
    0x100001379 <+41>: movq   %rdi, -0x20(%rbp)
    0x10000137d <+45>: movq   %r13, -0x28(%rbp)
    0x100001381 <+49>: movq   %rax, -0x30(%rbp)
    0x100001385 <+53>: callq  0x1000013b0               ; SwiftTest.Shape.side.willset : Swift.Int at main.swift:633
    0x10000138a <+58>: movq   -0x28(%rbp), %rax
    0x10000138e <+62>: movq   -0x20(%rbp), %rcx
    0x100001392 <+66>: movq   %rcx, 0x8(%rax)
    0x100001396 <+70>: movq   -0x30(%rbp), %rdi
    0x10000139a <+74>: movq   %rax, %r13
    0x10000139d <+77>: callq  0x1000014d0               ; SwiftTest.Shape.side.didset : Swift.Int at main.swift:636
    0x1000013a2 <+82>: movq   -0x30(%rbp), %rax
    0x1000013a6 <+86>: addq   $0x28, %rsp
    0x1000013aa <+90>: popq   %r13
    0x1000013ac <+92>: popq   %rbp
    0x1000013ad <+93>: retq 

image

原來,這個side的兩個屬性觀察器willSetdidSet被包裹在了這個setter函數里面,而且,對于屬性side的賦值真正發生在這個setter函數里面。

因此我們看出了一個細節,屬性side內存里的值被修改的時間點,是在test函數之后,也就是這個setter函數里,也就是test函數其實并沒有修改side的值。

因為test函數的功能拿到一段內存,并且修改里面的值,如果當前我們將side的地址提交給test,除了能夠修改side內存里值以外,它是無法觸發side的屬性觀察器的。所以看得出局部變量以及setter函數出現在這里的意義就是為了能夠去觸發屬性side的屬性觀察器。因為我們使用了局部變量,因此對于帶有屬性觀察器的存儲屬性,也可以說inout對其采用了Copy In Copy Out的做法。

通過程序運行之后的輸出結果,也可以驗證我們已上的結論

開始test函數
willSetSide 20
didSetSide 4 20
Program ended with exit code: 0


inout的本質總結

  • 如果實參有物理內存地址,且沒有設置屬性觀察器
    則直接將實參的內存地址傳入函數(實參進行引用傳遞

  • 如果實參是計算屬性 或者 設置了屬性觀察器
    則采取了 Copy In Copy Out的做法

    • 調用該函數時,先復制實參的值,產生副本【可以理解成get操作】
    • 將副本的內存地址傳入函數(副本進行引用傳遞),在函數內部可以修改副本的值
    • 函數返回后,再將副本的值覆蓋實參的值【可以理解成set操作】

總結:inout的本質就是引用傳遞(地址傳遞)


類型屬性(Type Property)

  • 嚴格來說,屬性可以劃分為:
    • 實例屬性(Instance Property):只能通過實例去訪問

      • 存儲實例屬性(Stored Instance Property):存儲在實例的內存中,每個實例都有一份
      • 計算實例屬性(Computed Instance Property):
    • 類型屬性(Type Property):只能通過類型去訪問

      • 存儲類型屬性(Stored Type Property):整個程序的運行過程中,就只有一份內存,它的本質就是全局變量
      • 計算類型屬性(Computed Type Property
  • 可以通過static定義類型屬性,對于類來說,還可以用關鍵字class

類型屬性細節

  • 不同于存儲實例屬性,你必須給存儲類型屬性設定初始值
    因為類型沒有像實例那樣的init初始化器來初始化存儲屬性

    image

  • 存儲類型屬性默認就是lazy, 會在第一次使用的時候才初始化

    • 就算被多個線程同時訪問,保證只會初始化一次,可以保證線程安全(系統底層會有加鎖處理)
    • 存儲類型屬性可以時let,因為這里壓根不存在實例初始化的過程
  • 枚舉類型也可以定義類型屬性(存儲類型屬性計算類型屬性

單例模式

public class FileManager {
    
    public static let shared = FileManager()
    
    private init(){
        
    }
}
  • public static let shared = FileManager()
    • 通過static定義了一個類型存儲屬性
    • public確保在任何場景下,外界都能訪問,
    • let保證了FileManager()只會被賦值給shared一次,并且確保了線程安全,也就是說init()方法只會被調用一次,這樣就確保FileManager只會存在唯一一個實例,這就是Swift中的單例
  • private init()private確保了外界是無法手動調用FileManager()來創建實例,因此通過shared屬性得到的FileManager實例永遠是相同的一份,這也符合了我們對與單例的要求。

類型(static)存儲屬性的本質

前面我們介紹static存儲屬性的時候,提到了它實際上是全局變量,現在來證明一下,首先我們看看普通的全局變量是怎么樣的

var num1 = 10 // 此處加斷點
var num2 = 11
var num3 = 12

運行至斷點處,匯編如下

SwiftTest`main:
    0x100001120 <+0>:  pushq  %rbp
    0x100001121 <+1>:  movq   %rsp, %rbp
    0x100001124 <+4>:  xorl   %eax, %eax
->  0x100001126 <+6>:  movq   $0xa, 0x60af(%rip)        ; demangling cache variable for type metadata for Swift.Array<Swift.UInt8> + 4
    0x100001131 <+17>: movq   $0xb, 0x60ac(%rip)        ; SwiftTest.num1 : Swift.Int + 4
    0x10000113c <+28>: movq   $0xc, 0x60a9(%rip)        ; SwiftTest.num2 : Swift.Int + 4
    0x100001147 <+39>: popq   %rbp
    0x100001148 <+40>: retq

很明顯,下圖的這三句分別對應的就是num1num2num3

image

我們來算一下他們的實際內存地址

  • &num1 = 0x60af + 0x100001131 = 0x1000071E0
  • &num2 = 0x60ac + 0x10000113c = 0x1000071E8
  • &num3 = 0x60a9 + 0x100001147 = 0x1000071F0

它們就是全局數據段上的3段連續內存空間。接下來我們加入static存儲屬性如下

var num1 = 10 // 斷點處

class Car {
    static var num2 = 1
}

Car.num2 = 11

var num3 = 12

打開斷點處的匯編

SwiftTest`main:
    0x100000d80 <+0>:  pushq  %rbp
    0x100000d81 <+1>:  movq   %rsp, %rbp
    0x100000d84 <+4>:  subq   $0x30, %rsp
->  0x100000d88 <+8>:  movq   $0xa, 0x6595(%rip)        ; demangling cache variable for type metadata for Swift.Array<Swift.UInt8> + 4
    0x100000d93 <+19>: movl   %edi, -0x1c(%rbp)
    0x100000d96 <+22>: movq   %rsi, -0x28(%rbp)
    0x100000d9a <+26>: callq  0x100000e40               ; SwiftTest.Car.num2.unsafeMutableAddressor : Swift.Int at main.swift
    0x100000d9f <+31>: xorl   %ecx, %ecx
    0x100000da1 <+33>: movq   %rax, %rdx
    0x100000da4 <+36>: movq   %rdx, %rdi
    0x100000da7 <+39>: leaq   -0x18(%rbp), %rsi
    0x100000dab <+43>: movl   $0x21, %edx
    0x100000db0 <+48>: movq   %rax, -0x30(%rbp)
    0x100000db4 <+52>: callq  0x1000053a2               ; symbol stub for: swift_beginAccess
    0x100000db9 <+57>: movq   -0x30(%rbp), %rax
    0x100000dbd <+61>: movq   $0xb, (%rax)
    0x100000dc4 <+68>: leaq   -0x18(%rbp), %rdi
    0x100000dc8 <+72>: callq  0x1000053c6               ; symbol stub for: swift_endAccess
    0x100000dcd <+77>: xorl   %eax, %eax
    0x100000dcf <+79>: movq   $0xc, 0x655e(%rip)        ; static SwiftTest.Car.num2 : Swift.Int + 4
    0x100000dda <+90>: addq   $0x30, %rsp
    0x100000dde <+94>: popq   %rbp
    0x100000ddf <+95>: retq 

image

如上圖所示,首先我們可以快速定位num1num3,我們可以先記錄一下他們的內存地址

  • &num1 = 0x6595 + 0x100000d93 = 0x100007328
  • &num3 = 0x655e + 0x100000dda = 0x100007338

num1num2中間,我們發現了一個叫Car.num2.unsafeMutableAddressor的函數被調用,并且通過將它的返回值作為地址訪問了一段內存空間,并向其賦值11,從Car.num2.unsafeMutableAddressor這個名字,我們可以看出,這個函數返回出來的地址,就是Car.num2的地址,首先我們運行到0x100000dbd <+61>: movq $0xb, (%rax)這句匯編,記錄一下這個地址的值

(lldb) register read rax
     rax = 0x0000000100007330  SwiftTest`static SwiftTest.Car.num2 : Swift.Int

可以看到,這個地址正好是num1num3之間的那段空間,因此雖然num2作為Carstatic存儲屬性,但是從它在內存中的位置來看,跟普通的全局變量沒有區別,因此可以說static存儲屬性的本質就是全局變量。

代碼稍微調整一下

var num1 = 10

class Car {
    static var num2 = 1
}
//Car.num2 = 11   //將這一句注釋掉
var num3 = 12


**********************對應匯編***********************
SwiftTest`main:
    0x100000dc0 <+0>:  pushq  %rbp
    0x100000dc1 <+1>:  movq   %rsp, %rbp
    0x100000dc4 <+4>:  xorl   %eax, %eax
->  0x100000dc6 <+6>:  movq   $0xa, 0x6557(%rip)        ; demangling cache variable for type metadata for Swift.Array<Swift.UInt8> + 4
    0x100000dd1 <+17>: movq   $0xc, 0x655c(%rip)        ; static SwiftTest.Car.num2 : Swift.Int + 4
    0x100000ddc <+28>: popq   %rbp
    0x100000ddd <+29>: retq

可以看出,匯編里Car.num2相關的代碼就消失了,也就是說如果沒有用到Car.num2,那么它是不會被初始化的,因此我們說static存儲屬性是默認lazy延遲)的。

我們將代碼恢復,再次更深入的跟蹤一下匯編過程

var num1 = 10 // 斷點處
class Car {
    static var num2 = 1
}
Car.num2 = 11
var num3 = 12


**********************對應匯編***********************
SwiftTest`main:
    0x100000d80 <+0>:  pushq  %rbp
    0x100000d81 <+1>:  movq   %rsp, %rbp
    0x100000d84 <+4>:  subq   $0x30, %rsp
->  0x100000d88 <+8>:  movq   $0xa, 0x6595(%rip)        ; demangling cache variable for type metadata for Swift.Array<Swift.UInt8> + 4
    0x100000d93 <+19>: movl   %edi, -0x1c(%rbp)
    0x100000d96 <+22>: movq   %rsi, -0x28(%rbp)
    0x100000d9a <+26>: callq  0x100000e40               ; SwiftTest.Car.num2.unsafeMutableAddressor : Swift.Int at main.swift
    0x100000d9f <+31>: xorl   %ecx, %ecx
    0x100000da1 <+33>: movq   %rax, %rdx
    0x100000da4 <+36>: movq   %rdx, %rdi
    0x100000da7 <+39>: leaq   -0x18(%rbp), %rsi
    0x100000dab <+43>: movl   $0x21, %edx
    0x100000db0 <+48>: movq   %rax, -0x30(%rbp)
    0x100000db4 <+52>: callq  0x1000053a2               ; symbol stub for: swift_beginAccess
    0x100000db9 <+57>: movq   -0x30(%rbp), %rax
    0x100000dbd <+61>: movq   $0xb, (%rax)
    0x100000dc4 <+68>: leaq   -0x18(%rbp), %rdi
    0x100000dc8 <+72>: callq  0x1000053c6               ; symbol stub for: swift_endAccess
    0x100000dcd <+77>: xorl   %eax, %eax
    0x100000dcf <+79>: movq   $0xc, 0x655e(%rip)        ; static SwiftTest.Car.num2 : Swift.Int + 4
    0x100000dda <+90>: addq   $0x30, %rsp
    0x100000dde <+94>: popq   %rbp
    0x100000ddf <+95>: retq

image

這一次我們從unsafeMutableAddressor這個函數跟進去看看

SwiftTest`Car.num2.unsafeMutableAddressor:
->  0x100000e40 <+0>:  pushq  %rbp
    0x100000e41 <+1>:  movq   %rsp, %rbp
    0x100000e44 <+4>:  cmpq   $-0x1, 0x64f4(%rip)       ; SwiftTest.num3 : Swift.Int + 7
    0x100000e4c <+12>: sete   %al
    0x100000e4f <+15>: testb  $0x1, %al
    0x100000e51 <+17>: jne    0x100000e55               ; <+21> at main.swift:719:16
    0x100000e53 <+19>: jmp    0x100000e5e               ; <+30> at main.swift
    0x100000e55 <+21>: leaq   0x64d4(%rip), %rax        ; static SwiftTest.Car.num2 : Swift.Int
    0x100000e5c <+28>: popq   %rbp
    0x100000e5d <+29>: retq   
    0x100000e5e <+30>: leaq   -0x45(%rip), %rax         ; globalinit_33_B9B0E304FD1668A20F6C95C54E9E2F7A_func0 at main.swift
    0x100000e65 <+37>: leaq   0x64d4(%rip), %rdi        ; globalinit_33_B9B0E304FD1668A20F6C95C54E9E2F7A_token0
    0x100000e6c <+44>: movq   %rax, %rsi
    0x100000e6f <+47>: callq  0x1000053fc               ; symbol stub for: swift_once
    0x100000e74 <+52>: jmp    0x100000e55               ; <+21> at main.swift:719:16

看到在最后,調用了swift_once函數,GCD里面我們知道有個dispatch_once,是否有關聯呢,我們進入這個函數

libswiftCore.dylib`swift_once:
->  0x7fff73447820 <+0>:  pushq  %rbp
    0x7fff73447821 <+1>:  movq   %rsp, %rbp
    0x7fff73447824 <+4>:  cmpq   $-0x1, (%rdi)
    0x7fff73447828 <+8>:  jne    0x7fff7344782c            ; <+12>
    0x7fff7344782a <+10>: popq   %rbp
    0x7fff7344782b <+11>: retq   
    0x7fff7344782c <+12>: movq   %rsi, %rax
    0x7fff7344782f <+15>: movq   %rdx, %rsi
    0x7fff73447832 <+18>: movq   %rax, %rdx
    0x7fff73447835 <+21>: callq  0x7fff7349c19c            ; symbol stub for: dispatch_once_f
    0x7fff7344783a <+26>: popq   %rbp
    0x7fff7344783b <+27>: retq   
    0x7fff7344783c <+28>: nop    
    0x7fff7344783d <+29>: nop    
    0x7fff7344783e <+30>: nop    
    0x7fff7344783f <+31>: nop

真相出現了,原來swift_once函數里面確實是調用了GCDdispatch_once_f,那么dispatch_once里面的block是什么呢,直覺告訴我們應該就是Car.num2的初始化代碼,也就是這句代碼static var num2 = 1

如何證明呢?我先我們將匯編運行到callq 0x7fff7349c19c ; symbol stub for: dispatch_once_f處,因為此時,dispatch_once_f函數所需的參數按照匯編的慣例,已經放到了rsirdx等寄存起里面了,我們可以查看一下此時這兩個寄存器的內容

在這里插入圖片描述

(lldb) register read rsi
     rsi = 0x00007ffeefbff598
(lldb) register read rdx
     rdx = 0x0000000100000e20  SwiftTest`globalinit_33_B9B0E304FD1668A20F6C95C54E9E2F7A_func0 at main.swift
(lldb) 

可以看到rdx此時存放的是一個跟globalinit(全局初始化)相關的函數func0,地址為0x0000000100000e20,該函數就是dispatch_once_f所接受的block。接下來我們回到Swift源碼,在如下處加一個斷點

image

那么我們繼續運行程序,斷點會停在上面這句代碼上,如果我們猜測正確的話,那么此時的匯編應該就在globalinit_33_B9B0E304FD1668A20F6C95C54E9E2F7A_func0這個函數里面,我們運行程序后,匯編如下

SwiftTest`globalinit_33_B9B0E304FD1668A20F6C95C54E9E2F7A_func0:
    0x100000e20 <+0>:  pushq  %rbp
    0x100000e21 <+1>:  movq   %rsp, %rbp
->  0x100000e24 <+4>:  movq   $0x1, 0x6501(%rip)        ; SwiftTest.num1 : Swift.Int + 4
    0x100000e2f <+15>: popq   %rbp
    0x100000e30 <+16>: retq   

確實是處在globalinit_33_B9B0E304FD1668A20F6C95C54E9E2F7A_func0函數內部,并且這里進行初始化的內存地址是 0x100000e2f + 0x6501 = 0x100007330,從初始值很明顯看出這段內存就是num2,并且跟我們在unsafeMutableAddressor函數返回處記錄的返回值相同,結果正如預期,證明完畢。

Swift底層,是通過
unsafeMutableAddressor ->
libswiftCore.dylib-swift_once ->
libswiftCore.dylib-dispatch_once_f: ---------->
static var num2 = 1
來對num2進行初始化的,因為使用了GCDdispatch_once,因此我們說static存儲屬性是線程安全的,并且只能被初始化一次。

方法

方法

class Car {
    static var count = 0  
    init() {
        Car.count += 1
    }
    // Type Method
    static func getCount() -> Int {
        //以下幾種訪問count的方法是等價的
        count += 1
        self.count += 1
        Car.self.count += 1
        Car.count += 1
        return count 
     }
}

let c0 = Car()
let c1 = Car()
let c2 = Car()
print(Car.getCount()) // 通過類名進行調用

枚舉、結構體、類都可以定義實例方法、類型方法

  • 實例方法Instance Method):通過實例對象進行調用
  • 類型方法Type Method):通過類型調用,用static或者class關鍵字來定義

self

  • 在實例方法中就代表實例對象
  • 在類型方法中就代表類型

在類型方法static func getCount中,以下幾種寫法等價

  • count
  • self.count
  • Car.count
  • Car.self.count

mutating

Swift語法規定,對于結構體和枚舉這兩種值類型,默認情況下,他們的屬性是不能被自身的實例方法所修改的(對于類沒有這個規定)

  • func關鍵字前面加mutating就可以允許這種修改行為,如下
struct Point {
    var x = 0.0, y = 0.0
    mutating func moveBy(deltaX: Double, deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}

enum StateSwitch {
    case low, middle, high
    mutating func next() {
        switch self {
        case .low:
            self = .middle
        case .middle:
            self = .high
        case .high:
            self = .low
        }
    }
}

@discardableResult

在func前面加上@discardableResult,可以消除:函數調用后的返回值未被使用的警告信息?

struct Point {
    var x = 0.0, y = 0.0
    @discardableResult mutating
    func moveX(deltaX: Double) -> Double {
        x += deltaX
        return x
    }
}
var p = Point()
p.moveX(deltaX: 10)

下標

使用subscript可以給任意類型(枚舉、類、結構體)增加下表功能。subscript的語法類似于實例方法、計算屬性,它的本質就是方法(函數)

class Point {
    var x = 0.0, y = 0.0
    subscript(index: Int) -> Double {
        set {
            if index == 0 {
                x = newValue
            } else if index == 1 {
                y = newValue
            }
        }

        get {
            if index == 0 {
                return x
            } else if index == 1 {
                return y
            }
            return 0
        }
    }
}

var p = Point()
p[0] = 11.1
p[1] = 22.2
print(p.x)  // 11.1
print(p.y)  // 22.2
print(p[0]) // 11.1
print(p[1]) // 22.2

從上面的案例來看,subscript為我們提供了通過[i]的方式去訪問成員變量,就像數組/字典那樣去使用。下標與函數的表面區別,只是在定義的時候,用subscript代替了func funcName,在調用的時候通過[arg]代替了funcName(arg)。而subscript的內部包含了getset,很像計算屬性。

我們簡化一下代碼

class Point {
    var x = 0, y = 0
    subscript(index: Int) -> Int {
        set {
            if index == 0 {
                x = newValue
            } else if index == 1 {
                y = newValue
            }
        }

        get {
            if index == 0 {
                return x
            } else if index == 1 {
                return y
            }
            return 0
        }
    }
}

var p = Point()
p[0] = 10 // 0xa   在這里放一個斷點?
p[1] = 11 // 0xb

運行程序至斷點處,匯編如下


image

我們我們根據立即數10和11,找到綠框處代碼,紅色標記處的函數顯然不是下標的調用,我們從兩個綠框處的間接函數調用跟進去看看

0x1000016b1 <+145>: callq  *0x98(%rcx) ---進入該函數-->

SwiftTest`Point.subscript.setter:
->  0x100001c10 <+0>:   pushq  %rbp
    0x100001c11 <+1>:   movq   %rsp, %rbp
    0x100001c14 <+4>:   pushq  %r13
    0x100001c16 <+6>:   subq   $0x48, %rsp
    0x100001c1a <+10>:  xorl   %eax, %eax
    0x100001c1c <+12>:  leaq   -0x10(%rbp), %rcx
    0x100001c20 <+16>:  movq   %rdi, -0x28(%rbp)
    ..........
    ..........
    ..........
0x100001715 <+245>: callq  *0x98(%rcx) ---進入該函數-->

SwiftTest`Point.subscript.setter:
->  0x100001c10 <+0>:   pushq  %rbp
    0x100001c11 <+1>:   movq   %rsp, %rbp
    0x100001c14 <+4>:   pushq  %r13
    0x100001c16 <+6>:   subq   $0x48, %rsp
    0x100001c1a <+10>:  xorl   %eax, %eax
    0x100001c1c <+12>:  leaq   -0x10(%rbp), %rcx
    0x100001c20 <+16>:  movq   %rdi, -0x28(%rbp)
     ..........
    ..........
    ..........

上面的結果說明callq *0x98(%rcx) = Point.subscript.setter 等價于 p[i] =
因此,證明了下標的本質就是函數。

這里為什么是 callq *[內存地址]來間接調用函數呢,因為p不是一個函數名,而是一個變量,所以想要調用下標函數,所以肯定是通過間接調用的方式來操作的。
直接調用callq 函數地址
間接調用callq *內存地址

注意點?

  • subscript中定義的返回值類型可以決定:
    • get方法的返回值類型
    • set方法中國呢newValue的類型
  • subscript可以接受多個參數,并且是任意類型

下標的細節

subscript可以沒有set方法,但是必須要有get方法,如果只有get方法,可以理解為只讀

class Point {
    var x = 0.0, y = 0.0
    subscript(index: Int) -> Double {
        get {
            if index == 0 {
                return x
            } else if index == 1 {
                return y
            }
            return 0
        }
    }
}

如果只有get方法,還可以省略get

class Point {
    var x = 0.0, y = 0.0
    subscript(index: Int) -> Double {
        if index == 0 {
            return x
        } else if index == 1 {
            return y
        }
        return 0
    }
}

還可以設置參數標簽

class Point {
    var x = 0.0, y = 0.0
    subscript(index i: Int) -> Double {
        if i == 0 {
            return x
        } else if i == 1 {
            return y
        }
        return 0
    }
}

var p = Point()
p.y = 22.2
print(p[index: 1]) // 如果有標簽的話,在使用的時候,就一定要帶上標簽才行

上面我們看到的subscript都是相當于實例方法(默認),下標也可以是類型方法

class Sum {
    static subscript(v1: Int, v2: Int) -> Int {
        return v1 + v2
    }
}

print(Sum[10,20])

結構體、類作為返回值的對比

struct Point {
    var x = 0
    var y = 0
}

class PointManager {
    var point = Point()
    subscript(index: Int) -> Point {
        set { point = newValue }  // 如果后面有堆point進行賦值,則必須要加上set方法。
        get { point }
    }
}

var pm = PointManager()
pm[0].x = 11
pm[0].y = 22
print(pm[0])
print(pm.point)

上面的案例中,PointManager這個類有一個下標,返回類型是結構體struct Point,并且注意這個下標的特點,無論下標值傳什么,它返回的都是結構體變量point,我們需要注意的是,下標里面的set的寫法應該如下

set { point = newValue }

這樣你可能會好奇,pm[0].x = 11 或者 pm[0].y = 22時,在set方法里面我們怎么知道這個newValue的值到底是給.x還是給.y的。其實你應該注意到,這里的newValue應該是struct Point類型的,如果這樣,其實設計者的思路就不難猜到
pm[0].x = 11 ---> newValue = (11, pm[0].y) ---> set { point = newValue = (11, pm[0].y) }
pm[0].y = 22 ---> newValue = (pm[0].x, 22) ---> set { point = newValue = (pm[0].x, 22) }

如果把strtct Point 換成 class Point, 這個set方法就可以不用寫了

class Point {
    var x = 0
    var y = 0
}

class PointManager {
    var point = Point()
    subscript(index: Int) -> Point {
        get { point }
    }
}

var pm = PointManager()
pm[0].x = 11
pm[0].y = 22
print(pm[0])
print(pm.point)

因為我們通過pm[0]拿到的是point這個對象實例指針,那么pm[0].x等價于point.x,所以point.x = 11是符合規范的。

下標接受多個參數

class Grid {
    var data = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8]
    
    ]
    
    subscript( row: Int, column: Int) -> Int {
        set {
            guard row >= 0 && row < 3 && column >= 0 && column < 3 else {
                return
            }
            data[row][column] = newValue
        }
        
        get  {
            guard row >= 0 && row < 3 && column >= 0 && column < 3 else {
                return 0
            }
            return data[row][column]
        }
    }
    
    
    
}

var grid = Grid()
grid[0, 1] = 77
grid[1, 2] = 88
grid[2, 0] = 99
print(grid.data)

*********************運行結果
[[0, 77, 2], [3, 4, 88], [99, 7, 8]]
Program ended with exit code: 0

好了,屬性和方法,暫時梳理到這里,period!

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

推薦閱讀更多精彩內容