目录
【返回目录】Swift底层原理探索----属性 & 方法
属性
struct Circle {
//存储属性
var radius: Double
//计算属性
var diamiter: Double {
set {
radius = newValue / 2
}
get {
radius * 2
}
}
}
- Swift中跟实例相关的属性可以分为2大类
- 存储属性(
Stored Property
)- 类似于成员变量这个概念
- 存储在实例的内存中
- 结构体、类可以定义存储属性
- 枚举
不可以
定义存储属性
我们知道枚举的内存里面可以存放的是所有的case
以及关联值
,并没有所谓的成员变量概念,可因此也不存在所谓的存储属性
- 计算属性(
Computed Property
)- 本质就是方法(函数)这个也可以通过汇编来证明一下
- 不占用实例的内存
- 枚举、结构体、类都可以定义计算属性
- 存储属性(
存储属性
- 关于存储属性,
Swift
有个明确的规定- 在创建类 或 结构体的时候,必须为所有的存储属性设置一个合适的初始值,也就是要求类/结构体创建实例后,它的全部内存要得到初始化,而存储属性正好就是放在实例的内存里面的,所以需要将所有的存储属性设置初始值。
- 可以在初始化器里为存储属性设置一个初始值
- 可以分配一个默认的属性值作为属性定义的一部分
- 在创建类 或 结构体的时候,必须为所有的存储属性设置一个合适的初始值,也就是要求类/结构体创建实例后,它的全部内存要得到初始化,而存储属性正好就是放在实例的内存里面的,所以需要将所有的存储属性设置初始值。
计算属性
set
传入的新值默认叫做newValue
,也可以自定义- 定义计算属性只能用
var
, 不能用let
let
代表常量,也就是值是一成不变的- 计算属性的值是可能发生变化的(即使是只读计算属性)
- 只读计算属性:只有
get
, 没有set
枚举rawValue原理
- 枚举原始值
rawValue
的本质是:只读计算属性,直接看汇编就可以证明
延迟存储属性(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
才能访问延迟存储属性
因为延迟属性初始化时需要改变结构体的内存
案例中,因为p
是常量,所以内存的内容初始化之后不可以变化,但是p.z会使得结构体Point
的lazy var z
属性进行初始化,因为结构体的成员是在结构体的内存里面的,因此就需要改变结构体的内存,因此便产生了后面的报错。
属性观察器(Property Observer)
- 可以为
非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,