Swift5.0
稳定版就已经发布了, 感兴趣的小伙伴可看个人这篇博客:Swift 5.0新特性更新Swift
的属性的相关介绍和剖析, 测试环境: Xcode 11.2.1
, Swift 5.1.2
在Swift
中, 严格意义上来说属性能够分为两大类: 实例属性和类型属性html
Instance Property
): 只能经过实例去访问的属性
Stored Instance Property
): 存储在市里的内存中, 每一个实例都只有一份Computed Instance Property
)Type Property
): 只能经过类型去访问的属性
Stored Type Property
): 整个程序运行过程当中就只有一分内存(相似全局变量)Computed Type Property
)static
关键字定义; 若是是类也能够经过class
关键字定义上面提到Swift
中跟市里相关的属性能够分为两大类:存储属性和计算属性swift
Stored Property
)
var
定义),也能够是常量存储属性(用关键字let
定义)Computed Property
)
getter
和一个可选的setter
,来间接获取和设置其余属性或变量的值Swift
中存储属性能够是var
修饰的变量, 也能够是let
修饰的常量init
初始化器里为存储实行设置一个初始值struct Person { // 定义的时候设置初始值 var age: Int = 24 var weight: Int } // 使用init初始化器设置初始值 var person1 = Person(weight: 75) var person2 = Person(age: 25, weight: 80) 复制代码
MemoryLayout
获取数据类型占用的内存大小// Person结构体实际占用的内存大小 MemoryLayout<Person>.size // 16 // 系统为Person分配的内存大小 MemoryLayout<Person>.stride // 16 // 内存对其参数 MemoryLayout<Person>.alignment // 8 复制代码
还有一种使用方式, 输出结果一致sass
var person = Person(weight: 75) MemoryLayout.size(ofValue: person) MemoryLayout.stride(ofValue: person) MemoryLayout.alignment(ofValue: person) 复制代码
getter
和一个可选的setter
,来间接获取和设置其余属性或变量的值struct Square { var side: Int var girth: Int { set { side = newValue / 4 } get { return side * 4 } } } // 其中set也可使用下面方式 set(newGirth) { side = newGirth / 4 } 复制代码
下面咱们先看一下Square
所占用的内存大小, 这里方便查看都去掉了print
函数安全
var squ = Square(side: 4) MemoryLayout.size(ofValue: squ) // 8 MemoryLayout.stride(ofValue: squ) // 8 MemoryLayout.alignment(ofValue: squ) // 8 复制代码
从上面输出结果能够看出, Square
只占用8个内存大小, 也就是一个Int
占用的内存大小, 若是仍是看不出来, 能够看一下下面这个bash
struct Square { var girth: Int { get { return 4 } } } // 输出结果0 print(MemoryLayout<Square>.size) // 0 复制代码
get、set
方法来取值或赋值set
方法修改其余相关联的属性的值; 若是该计算属性是只读的, 则不须要set
方法, 传入的新值默认值newValue
, 也能够自定义get
方法获取该计算属性的值, 即便是只读的, 计算属性的值也是可能发生改变的var
, 不能使用let
Always Show Disassembly
, 右断点时Xcode
就会在运行过程当中自动跳到断电的汇编代码中var squ = Square(side: 4) var c = squ.girth // 在此处加上断点时 复制代码
上述代码的执行流程, 经过汇编的方式看, 核心代码以下所示微信
下面是在iOS模拟器环境下一些汇编经常使用的指令markdown
// 将rax的值赋值给rdi movq %rax, %rdi // 将rbp-0x18这个地址值赋值给rsi leaq -0x18(%rbp), %rsi // 函数跳转指令 callq 0x100005428 复制代码
从上图能够看到上面代码对应的汇编代码, 其核心代码大概能够分为四部分闭包
Square
调用init
初始化器, 即Square
的初始化(详细汇编代码可进入callq 0x100001300
中查看)Square
的对象的内存地址赋值给一个全局变量, 即squ
Square
对象里面girth
计算属性的getter
方法, 获取girth
的值girth
的值赋值给一个全局变量如上图中中断点位置, 当断电执行到此处时, 执行
si
命令便可查看getter
函数的的执行过程, 以下图所示, 其中imulq
是执行乘法指令ide
// 把rdx和rax的相乘的结果在赋值给rax imulq %rdx, %rax 复制代码
下面再看一下, 计算属性的赋值操做, 代码以下函数
var squ = Square(side: 4) squ.girth = 12; print(squ.side) // 3 复制代码
对应的汇编代码以下, 执行流程和上面的取值操做相似, 不一样的是赋值操做最后执行的是girth
的setter
方法
0x1000010c9 <+25>: callq 0x100001300 ; SwiftLanguage.Square.init(side: Swift.Int) -> SwiftLanguage.Square at main.swift:11 0x1000010ce <+30>: leaq 0x6123(%rip), %rsi ; SwiftLanguage.squ : SwiftLanguage.Square 0x1000010d5 <+37>: xorl %ecx, %ecx 0x1000010d7 <+39>: movq %rax, 0x611a(%rip) ; SwiftLanguage.squ : SwiftLanguage.Square 0x1000010de <+46>: movq %rsi, %rdi 0x1000010e1 <+49>: leaq -0x20(%rbp), %rsi 0x1000010e5 <+53>: movl $0x21, %edx 0x1000010ea <+58>: callq 0x10000540a ; symbol stub for: swift_beginAccess 0x1000010ef <+63>: movl $0xc, %edi 0x1000010f4 <+68>: leaq 0x60fd(%rip), %r13 ; SwiftLanguage.squ : SwiftLanguage.Square 0x1000010fb <+75>: callq 0x100001200 ; SwiftLanguage.Square.girth.setter : Swift.Int at main.swift:14 复制代码
get
没有set
// 你能够这样写 struct Square { var side: Int var girth: Int { get { return side * 4 } } } // 也能够这样写 var girth: Int { return side * 4 } // 还能够这样写 var girth: Int { side * 4 } var squ = Square(side: 4) // 不可赋值修改 //squ.girth = 12; print(squ.girth) 复制代码
枚举的rawValue
的本质就是计算属性, 并且是只读的计算属性
enum Test: Int { case test1 = 1 case test2 = 2 } var c = Test.test1.rawValue print(c) // 1 复制代码
至于如何肯定, 那么久简单粗暴点, 看汇编
rawValue
的值, 其实就是调用的rawValue
的getter
方法rawValue
进行从新赋值, 会报错Test.test1.rawValue = 2 // 这里报错: Cannot assign to property: 'rawValue' is immutable 复制代码
那么咱们就能够根据rawValue
的计算属性修改rawValue
的值
enum Test: Int { case test1 = 1 case test2 = 2 var rawValue: Int { switch self { case .test1: return 10 case .test2: return 20 } } } var c = Test.test1.rawValue // 10 复制代码
lazy
能够定义一个延迟存储属性(Lazy Stored Property
), 延迟存储属性只有在第一次使用的时候才会进行初始化lazy
属性修饰必须是var
, 不能是let
let
修饰的常量必须在实例的初始化方法完成以前就拥有值class Car { init() { print("Car init") } func run() { print("Car is runing") } } class Person { lazy var car = Car() init() { print("Person init") } func goOut() { car.run() } } let person = Person() print("--------") person.goOut() // 输出结果 // Person init // -------- // Car init // Car is runing 复制代码
上述代码, 在初始化car
的时候若是没有lazy
, 则输出结果以下
/* Car init Person init -------- Car is runing */ 复制代码
OC
中的懒加载class Preview { lazy var image: Image = { let url = "https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/12/20/16f238c1c709a6a9~tplv-t2oaga2asx-image.image" let data = Data.init(contentsOf: url) return Image(data: data) }() } 复制代码
在Swift
中能够为非lazy
的而且只能是var
修饰的存储属性设置属性观察器, 形式以下
struct Person { var age: Int { willSet { print("willSet", newValue) } didSet { print("didSet", oldValue, age) } } init() { self.age = 3 print("Person init") } } var p = Person() p.age = 10 print(p.age) /* 输出结果 Person init willSet 10 didSet 3 10 10 */ 复制代码
willSet
或didSet
观察者,来观察和响应属性值的变化, 从上述输出结果咱们也能够看到
willSet
会传递新值, 在存储值以前被调用, 其默认的参数名是newValue
didSet
会传递旧值, 在存储新值以后当即被调用, 其默认的参数名是oldValue
willSet
或didSet
Stored Type Property
): 整个程序运行过程当中就只有一分内存(相似全局变量)Computed Type Property
): 不占用系统内存static
关键字定义; 若是是类也能够经过class
关键字定义init
初始化器去设置初始值的方式lazy
), 不须要使用lazy
修饰符标记, 只会在第一次使用的时候初始化, 即便是被多个线程访问, 也能保证只会被初始化一次// 在结构体中只能使用static struct Person { static var weight: Int = 30 static let height: Int = 100 } // 取值 let a = Person.weight let b = Person.height // 赋值 Person.weight = 12 // let修饰的不可被赋值 //Person.height = 10 复制代码
在类中可使用static
和class
class Animal { static var name: String = "name" class var age: Int { return 10 } } // 取值 let a1 = Animal.name let a2 = Animal.age // 赋值 Animal.name = "animal" // class定义的属性是只读的 // Animal.age = 20 复制代码
static
class
、struct
、enum
类型的属性或者方法class
中的属性和方法不能够在子类中被重写, 重写会报错struct Person { // 存储属性 static var weight: Int = 30 // 计算属性 static var height: Int { get { 140 } } // 类型方法 static func goShoping() { print("Person shoping") } } 复制代码
class
class Animal { // 计算属性 class var height: Int { get { 140 } } // 类型方法 class func running() { print("Person running") } } 复制代码
先看下下面这行代码的内存地址
var num1 = 3 var num2 = 5 var num3 = 7 复制代码
rip
做为指令指针,rip
中存储着CPU
下一条要执行的指令的地址CPU
读取一条指令, rip
会自动指向下一条指令(存储下一条指令的地址)rip
存储的地址就是第三条指令的地址0x10000138c
0x10000137f <+15>: xorl %ecx, %ecx // $0x3赋值给num1, 则num1的地址值就是: 0x10000138c + 0x5e6c = 0x1000071F8 0x100001381 <+17>: movq $0x3, 0x5e6c(%rip) ; lazy cache variable for type metadata for Swift.Array<Swift.UInt8> + 4 // $0x5赋值给num2, 则num2的地址值就是: 0x100001397 + 0x5e69 = 0x100007200 0x10000138c <+28>: movq $0x5, 0x5e69(%rip) ; SwiftLanguage.num1 : Swift.Int + 4 // $0x7赋值给num3, 则num3的地址值就是: 0x1000013a2 + 0x5e66 = 0x100007208 0x100001397 <+39>: movq $0x7, 0x5e66(%rip) ; SwiftLanguage.num2 : Swift.Int + 4 0x1000013a2 <+50>: movl %edi, -0x1c(%rbp) 复制代码
从上面三个内存地址能够看出三个全局变量的内存地址是相邻的, 而且彼此相差8个字节, 由于每个
Int
就占用8个字节; 下面再看一下类型属性和全局变量的内存地址
class Animal { static var age: Int = 10 } var num1 = 3 Animal.age = 7 var num2 = 5 复制代码
相关汇编代码如图所示
根据图中的相关核心代码, 分别计算出num1
, age
和num2
的内存地址以下
// $0x3赋值给num1, 则num1的地址值就是: 0x100000fd3 + 0x6785 = 0x100007330 // 经过register命令获得rax的地址为0x100007338, 即为age所在的内存地址 // $0x5赋值给num2, 则num2的地址值就是: 0x100001027 + 0x6319 = 0x100007340 /* 0x100007330 0x100007338 0x100007340 */ // 上述三个内存地址一样也是相邻, 而且彼此相差8个字节 复制代码
因此, 类型属性也能够理解为全局变量, 不一样的是全局变量能够直接访问, 类型属性必须经过类名访问, 有必定的访问限制而已
lazy
), 不须要使用lazy
修饰符标记, 只会在第一次使用的时候初始化age
赋值以前, 执行了不少汇编代码callq
// 进入查看具体执行的那些操做 0x100000fda <+26>: callq 0x1000010d0 ; SwiftLanguage.Animal.age.unsafeMutableAddressor : Swift.Int at main.swift 复制代码
将断点加在此处, 执行si
指令便可进入该模块
swift_once
, 天然就可以联想到dispatch_once
和OC
中的单例模式swift_once
里面究竟是如何操做的, 仍是在swift_once
加上断点, 并执行si
指令, 以下图所示dispatch_once
实现的dispatch_once
里面执行的, 保证age
的初始化操做永远只被执行一次欢迎您扫一扫下面的微信公众号,订阅个人博客!