struct Circle {
//存储属性
var radius: Double
//计算属性
var diamiter: Double {
set {
radius = newValue / 2
}
get {
radius * 2
}
}
}
复制代码
Stored Property
)
不能够
定义存储属性 咱们知道枚举的内存里面能够存放的是全部的case
以及关联值
,并无所谓的成员变量概念,可所以也不存在所谓的存储属性 * 计算属性(Computed Property
) + 本质就是方法(函数)这个也能够经过汇编来证实一下
+ 不占用实例的内存
+ 枚举、结构体、类均可以定义计算属性swift
Swift
有个明确的规定
set
传入的新值默认叫作newValue
,也能够自定义var
, 不能用let
let
表明常量,也就是值是一成不变的get
, 没有set
rawValue
的本质是:只读计算属性,直接看汇编就能够证实 看现这段代码数组
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
修饰markdown
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顺滑度,改善卡顿状况。app
- 使用
lazy
能够定义一个延迟存储属性,在第一次用到属性的时候才会进行初始化lazy
属性必须是var
, 不能是let
- 这个要求很容易理解,
let
必须在实例
的初始化方法完成以前就拥有值,而lazy
刚好是为了在实例建立并初始化以后的某个时刻对其某个属性进行初始化赋值,因此lazy
只能做用域var
属性- 若是多线程同时第一次访问
lazy
属性,没法保证属性只被初始化1
次
var
才能访问延迟存储属性由于延迟属性初始化时须要改变结构体的内存 案例中,由于
p
是常量,因此内存的内容初始化以后不能够变化,可是p.z会使得结构体Point
的lazy var z
属性进行初始化,由于结构体的成员是在结构体的内存里面的,所以就须要改变结构体的内存,所以便产生了后面的报错。ide
非lazy
的var
存储属性设置属性观察器willSet
会传递新值,默认叫作newValue
didSet
会传递旧值,默认叫作oldValue
willSet
和didSet
willSet
和didSet
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
调用以前,参数的传递状况以下 对于上述比较简单的状况,咱们知道
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
,它的内存放的是两个存储属性width
和side
,其中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
复制代码
参数传递流程以下图
因此对于普通的存储属性
,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
复制代码
这一次从汇编代码量就能够判断,对于计算属性的处理确定比存储属性要复杂,仍是经过图例来展现一下整个过程
能够看出,因为计算属性在实例内部没有对应的内存空间,编译器经过在函数栈里面开辟一个局部变量的方法,利用它做为计算属性的值的临时宿主,而且将该局部变量的地址做为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
复制代码
此次,咱们发现跟计算属性有些相似,这里也用到了函数栈的局部变量,它的做用是用来承载计算属性的值,而后被传入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
复制代码
原来,这个
side
的两个属性观察器willSet
和didSet
被包裹在了这个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**):只能经过类型去访问
+ 存储类型属性(**Stored Type Property**):整个程序的运行过程当中,就只有一分内存,它的本质就是全局变量
+ 计算类型属性(**Computed Type Property**)
复制代码
static
定义类型属性,对于类来讲,还能够用关键字class
由于类型没有像实例那样的init
初始化器来初始化存储属性
存储类型属性默认就是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存储属性的时候,提到了它其实是全局变量,如今来证实一下,首先咱们看看普通的全局变量是怎么样的
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
复制代码
很明显,下图的这三句分别对应的就是num1
、num2
、num3
咱们来算一下他们的实际内存地址
&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
复制代码
如上图所示,首先咱们能够快速定位
num1
和num3
,咱们能够先记录一下他们的内存地址
&num1 = 0x6595 + 0x100000d93 = 0x100007328
&num3 = 0x655e + 0x100000dda = 0x100007338
在num1
和num2
中间,咱们发现了一个叫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
复制代码
能够看到,这个地址正好是
num1
和num3
之间的那段空间,所以虽然num2
做为Car
的static
存储属性,可是从它在内存中的位置来看,跟普通的全局变量没有区别,所以能够说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
复制代码
这一次咱们从
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
函数里面确实是调用了GCD的dispatch_once_f
,那么dispatch_once
里面的block
是什么呢,直觉告诉咱们应该就是Car.num2
的初始化代码,也就是这句代码static var num2 = 1
如何证实呢?我先咱们将汇编运行到callq 0x7fff7349c19c ; symbol stub for: dispatch_once_f
处,由于此时,dispatch_once_f
函数所需的参数按照汇编的惯例,已经放到了rsi
、rdx
等寄存起里面了,咱们能够查看一下此时这两个寄存器的内容
(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源码,在以下处加一个断点
那么咱们继续运行程序,断点会停在上面这句代码上,若是咱们猜想正确的话,那么此时的汇编应该就在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
进行初始化的,由于使用了GCD
的dispatch_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
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
}
}
}
复制代码
在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
的内部包含了get
和set
,很像计算属性。
咱们简化一下代码
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
复制代码
运行程序至断点处,汇编以下
咱们咱们根据当即数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!