Swift底层原理探索6----属性 & 方法

1,430 阅读19分钟

属性

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!