一、概述
本系列文章旨在复习Swift5
核心语法且适当进行底层原理探索,属于阶段性复习和巩固,以供日后进一步探索Swift
语言的底层原理做铺垫。
整个系列文章如下,每一文章知识点独立成篇,欢迎各位按需或按兴趣点击阅读:
- 01-📝Swift5常用核心语法|了解Swift【Swift简介、Swift的版本、Swift编译原理】
- 02-📝Swift5常用核心语法|基础语法【Playground、常量与变量、常见数据类型、字面量、元组、流程控制、函数、枚举、可选项、guard语句、区间】
- 03-📝Swift5常用核心语法|面向对象【闭包、结构体、类、枚举】
- 04-📝Swift5常用核心语法|面向对象【属性、inout、类型属性、单例模式、方法、下标、继承、初始化】
- 05-📝Swift5常用核心语法|高级语法【可选链、协议、错误处理、泛型、String与Array、高级运算符、扩展、访问控制、内存管理、字面量、模式匹配】
- 06-📝Swift5常用核心语法|编程范式与Swift源码【从OC到Swift、函数式编程、面向协议编程、响应式编程、Swift源码分析】
二、属性
1. 属性的基本概念
Swift中跟实例相关的属性可以分为2大类:
存储属性
(Stored Property)- 类似于成员变量的概念
- 存储在实例的内存中
- 结构体、类可以定义存储属性
- 枚举
不可以
定义存储属性
计算属性
(Computed Property)- 本质就是方法(函数)
- 不占用实例的内存
- 枚举、结构体、类都可以定义计算属性
1.1 存储属性
关于存储属性,Swift有个明确的规定:
- 在创建类或结构体的实例时,必须为所有的存储属性设置一个合适的初始值
- 可以在初始化器里为存储属性设置一个初始值
struct Point { // 存储属性 var x: Int var y: Int } let p = Point(x: 10, y: 10)
- 可以分配一个默认的属性值作为属性定义的一部分
struct Point { // 存储属性 var x: Int = 10 var y: Int = 10 } let p = Point()
1.2 计算属性
定义计算属性只能用var
,不能用let
let
代表常量,值是一直不变的- 计算属性的值是可能发生变化的(即使是只读计算属性)
struct Circle { // 存储属性 var radius: Double // 计算属性 var diameter: Double { set { radius = newValue / 2 } get { radius * 2 } } } var circle = Circle(radius: 5) print(circle.radius) // 5.0 print(circle.diameter) // 10.0 circle.diameter = 12 print(circle.radius) // 6.0 print(circle.diameter) // 12.0
- set传入的新值默认叫做
newValue
,也可以自定义struct Circle { // 存储属性 var radius: Double // 计算属性 var diameter: Double { set(newDiameter) { radius = newDiameter / 2 } get { radius * 2 } } } var circle = Circle(radius: 5) circle.diameter = 12 print(circle.diameter)
- 只读计算属性,只有
get
,没有set
struct Circle { // 存储属性 var radius: Double // 计算属性 var diameter: Double { get { radius * 2 } } }
struct Circle { // 存储属性 var radius: Double // 计算属性 var diameter: Double { radius * 2 } } }
- 打印
Circle结构体
的内存大小,其占用才8个字节
,其本质是因为计算属性相当于函数var circle = Circle(radius: 5) print(Mems.size(ofVal: &circle)) // 8
我们可以通过反汇编来查看其内部做了什么
- 可以看到内部会调用
set方法
去计算 - 然后我们在往下执行,还会看到
get方法
的调用 - 所以可以用此证明:计算属性只会生成
getter
和setter
,不会开辟内存空间
注意:
- 一旦将存储属性变为计算属性,初始化构造器就会报错,只允许传入存储属性的值
- 因为存储属性是直接存储在结构体内存中的,如果改成计算属性则不会分配内存空间来存储
- 如果只有
setter
也会报错 - 只读计算属性:只有
get
,没有set
struct Circle { var radius: Double var diameter: Double { get { radius * 2 } } } //可以简写成 struct Circle { var radius: Double var diameter: Double { radius * 2 } }
2. 枚举rawValue原理(计算属性)
-
- 枚举原始值
rawValue
的本质也是计算属性,而且是只读的计算属性
enum TestEnum: Int { case test1, test2, test3 var rawValue: Int { switch self { case .test1: return 10 case .test2: return 20 case .test3: return 30 } } } print(TestEnum.test1.rawValue)//10
- 枚举原始值
-
- 下面我们去掉自己写的
rawValue
,然后转汇编看下本质是什么样的
- 可以看到底层确实是调用了
getter
enum TestEnum: Int { case test1, test2, test3 } print(TestEnum.test1.rawValue)
- 下面我们去掉自己写的
3. 延迟存储属性(Lazy Stored Property)
-
- 使用
lazy
可以定义一个延迟存储属性,在第一次用到属性的时候才会进行初始化
- 看下面的示例代码,如果不加
lazy
,那么Person初始化之后就会进行Car的初始化 - 加上
lazy
,只有调用到属性的时候才会进行Car的初始化
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!
- 使用
-
lazy
属性必须是var
,不能是let
let
必须在实例的初始化方法完成之前就拥有值
class PhotoView { lazy var image: UIImage = { let url = "http://www.***.com/logo.png" let data = Data(url: url) return UIImage(data: data) }() }
-
- 注意:
lazy
属性和普通的存储属性内存布局是一样的,不同的只是什么分配内存的时机,而且lazy属性可以通过闭包进行初始化
- 注意:
-
- 延迟存储属性的注意点
- 1.如果多条线程同时第一次访问
lazy
属性,无法保证属性只被初始化一次 - 2.当结构体包含一个延迟存储属性时,只有
var
才能访问延迟存储属性
因为延迟存储属性初始化时需要改变结构体的内存
4. 属性观察器(Property Observer)
-
- 可以为非
lazy
的var存储属性
设置属性观察器
- 只有存储属性可以设置属性观察器
willSet
会传递新值,默认叫newValue
didSet
会传递旧值,默认叫oldValue
struct Circle { // 存储属性 var radius: Double { willSet { print("willSet", newValue) } didSet { print("didSet", oldValue, radius) } } init() { radius = 1.0 print("Circle init!") } } var circle = Circle() circle.radius = 10.5 // 打印 // willSet 10.5 // didSet 1.0 10.5
- 可以为非
-
- 在初始化器中设置属性值不会触发
willSet
和didSet
struct Circle { // 存储属性 var radius: Double { willSet { print("willSet", newValue) } didSet { print("didSet", oldValue, radius) } } init() { radius = 1.0 print("Circle init!") } } var circle = Circle()
- 在初始化器中设置属性值不会触发
-
- 在属性定义时设置初始值也不会触发
willSet
和didSet
struct Circle { // 存储属性 var radius: Double = 1.0 { willSet { print("willSet", newValue) } didSet { print("didSet", oldValue, radius) } } } var circle = Circle()
- 在属性定义时设置初始值也不会触发
-
- 计算属性设置属性观察器会报错
5. 全局变量和局部变量
-
- 属性观察器、计算属性的功能,同样可以应用在全局变量和局部变量身上
5.1 全局变量
var num: Int {
get {
return 10
}
set {
print("setNum", newValue)
}
}
num = 11 // setNum 11
print(num) // 10
5.2 局部变量
func test() {
var age = 10 {
willSet {
print("willSet", newValue)
}
didSet {
print("didSet", oldValue, age)
}
}
age = 11
// willSet 11
// didSet 10 11
}
test()
二、inout
1. inout对属性的影响
看下面的示例代码,分别输出什么,为什么?
struct Shape {
var width: Int
var side: Int {
willSet {
print("willSet", newValue)
}
didSet {
print("didSet", 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)
s.show()
print("--------------------")
test(&s.side)
s.show()
print("--------------------")
test(&s.girth)
s.show()
// 打印:
//getGirth
//width=20, side=4, girth=80
//--------------------
//willSet 20
//didSet 4 20
//getGirth
//width=20, side=20, girth=400
//--------------------
//getGirth
//setGirth 20
//getGirth
//width=1, side=20, girth=20
第一段打印
初始化的时候会给width赋值为10,side赋值为4,并且不会调用side的属性观察器
然后调用test方法
,并传入width的地址值,width变成20
然后调用show方法
,会调用girth的getter,然后先执行打印,再计算,girth为80
下面我们通过反汇编来进行分析
第二段打印
现在width的值是20,side的值是4,girth的值是80
然后调用test方法
,并传入side的地址值,side变成20,并且触发属性观察器,执行打印
然后调用show方法
,会调用girth的getter,然后先执行打印,再计算,girth为400
下面我们通过反汇编来进行分析
将地址值存储到rdi
中,并带入到test
函数中进行计算
setter
中才会真正的调用willSet
和didSet
方法willSet
和didSet
之间的计算才是真正的将改变了的值覆盖了全局变量里的side- 真正改变了side的值的时候是调用完
test函数
之后,在内部的setter
里进行的
第三段打印
现在width的值是20,side的值是20,girth的值是400
然后调用test方法
,并传入girth的getter的返回值为400,然后将20赋值给girth的setter计算,width变为1
然后调用show方法
,,会调用girth的getter,然后先执行打印,再计算,girth为20
下面我们通过反汇编来进行分析
再后面都是计算的过程了,这里就不详细跟进了
我们主要了解inout
是怎么给计算属性进行关联调用的,从上面分析可以看出:
- 从调用girth的
getter
开始,都会将计算的结果放入一个寄存器中 - 然后通过这个寄存器的地址再进行传递
inout
影响的也是修改这个寄存器中存储的值,然后再进一步传递到setter
里进行计算
2. inout的本质总结
对于没有属性观察器的存储属性
来说:
inout
的本质就是传进来一个地址值
,然后将值存储到这个地址对应的存储空间内
对于设置了属性观察器和计算属性
来说:
-
inout
会先将传进来的地址值放到一个局部变量中,然后改变局部变量地址值对应的存储空间 -
再将改变了的局部变量值覆盖最初传进来的参数的值
- 这时会对应触发属性观察器
willSet、didSet
和计算属性的setter、getter
的调用
- 这时会对应触发属性观察器
-
如果不这么做,直接就改变了传进来的地址值的存储空间的话,就不会调用属性观察器了,而计算属性因为没有分配内存来存储值,也就没办法更改了
-
总结:
inout
的本质就是引用传递(地址传递)
三、类型属性(Type Property)
1. 两类属性
严格来说,属性可以分为两大类:
- 实例属性(Instance Property):只能通过实例去访问
- 存储实例属性(Stored Instance Property):存储在实例的内存中,每个实例都有一份
- 计算实例属性(Computed Instance Property)
- 类型属性(Type Property):只能通过类去访问
- 存储类型属性(Stored Type Property):整个程序运行过程中,就只有一份内存(类似于全局变量)
- 计算类型属性(Computed Type Property)
-
- 可以通过
static
定义类型属性
struct Car { static var count: Int = 0 init() { Car.count += 1 } }
- 可以通过
-
- 如果是类,也可以用关键字
class
修饰计算属性类型
class Car { class var count: Int { return 10 } } print(Car.count)
- 如果是类,也可以用关键字
-
- 类里面不能用
class
修饰存储属性类型
- 类里面不能用
2. 类型属性细节
- 不同于
存储实例属性
,存储类型属性
必须设定初始值,不然会报错 - 因为类型没有像实例那样的
init初始化器
来初始化存储属性 存储类型属性
可以用let
struct Car { static let count: Int = 0 } print(Car.count)
- 枚举类型也可以定义类型属性(
存储类型属性
、计算类型属性
)enum Shape { static var width: Int = 0 case s1, s2, s3, s4 } var s = Shape.s1 Shape.width = 5
存储类型属性
默认就是lazy
,会在第一次使用的时候进行初始化- 就算被多个线程同时访问,保证只会初始化一次
通过反汇编来分析类型属性的底层实现
我们先通过打印下面两组代码来做对比,发现存储类型属性的内存地址和前后两个全局变量正好相差8个字节,所以可以证明存储类型属性的本质就是类似于全局变量,只是放在了结构体或者类里面控制了访问权限:
var num1 = 5
var num2 = 6
var num3 = 7
print(Mems.ptr(ofVal: &num1)) // 0x000000010000c1c0
print(Mems.ptr(ofVal: &num2)) // 0x000000010000c1c8
print(Mems.ptr(ofVal: &num3)) // 0x000000010000c1d0
var num1 = 5
class Car {
static var count = 1
}
Car.count = 6
var num3 = 7
print(Mems.ptr(ofVal: &num1)) // 0x000000010000c2f8
print(Mems.ptr(ofVal: &Car.count)) // 0x000000010000c300
print(Mems.ptr(ofVal: &num3)) // 0x000000010000c308
然后我们通过反汇编来观察:
通过调用我们可以发现最后会调用到GCD
的dispatch_once
,所以存储类型属性才会说是线程安全的,并且只执行一次
并且dispatch_once
里面执行的代码就是static var count = 1
四、单例模式
public class FileManager {
public static let shared = FileManager()
private init() { }
public func openFile() {
}
}
FileManager.shared.openFile()
五、方法(Method)
1. 基本概念
枚举、结构体、类都可以定义实例方法
、类型方法
- 实例方法(
Instance Method
): 通过实例对象调用 - 类型方法(
Type Method
): 通过类型调用- 实例方法调用
class Car { var count = 0 func getCount() -> Int { count } } let car = Car() car.getCo
- 类型方法用
static
或者class
关键字定义class Car { static var count = 0 static func getCount() -> Int { count } } Car.getCount()
- 类型方法中不能调用实例属性,反之实例方法中也不能调用类型属性
- 实例方法调用
- 不管是类型方法还是实例方法,都会传入隐藏参数
self
self
在实例方法中代表实例对象self
在类型方法中代表类型// count等于self.count、Car.self.count、Car.count static func getCount() -> Int { self.count }
2. mutating
结构体
和枚举
是值类型
,默认情况下,值类型的属性不能被自身的实例方法修改- 在
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 } } }
3. @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)
1. 基本概念
-
- 使用
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
中定义的返回值类型决定了getter
中返回值类型和setter
中newValue
的类型
-
subscript
可以接收多个参数,并且类型任意
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
-
subscript
可以没有setter
,但必须要有getter
,同计算属性
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 } } }
-
subscript
如果只有getter
,可以省略getter
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 } }
-
subscript
可以设置参数标签
只有设置了自定义标签的调用才需要写上参数标签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]) // 22.2
- 8.
subscript
可以是类型方法class Sum { static subscript(v1: Int, v2: Int) -> Int { v1 + v2 } } print(Sum[10, 20]) // 30
通过反汇编来分析
看下面的示例代码,我们将断点打到图上的位置,然后观察反汇编
看到其内部是会调用setter
来进行计算
然后再将断点打到这里来看
看到其内部是会调用getter
来进行计算
经上述分析就可以证明subscript
本质就是方法调用
2. 结构体和类作为返回值对比
看下面的示例代码
struct Point {
var x = 0, y = 0
}
class PointManager {
var point = Point()
subscript(index: Int) -> Point {
set { point = newValue }
get { point }
}
}
var pm = PointManager()
pm[0].x = 11 // 等价于pm[0] = Point(x: 11, y: pm[0].y)
pm[0].y = 22 // 等价于pm[0] = Point(x: pm[0].x, y: 22)
如果我们注释掉setter
,那么调用会报错
但是我们将结构体换成类,就不会报错了
- 原因还是在于结构体是
值类型
,通过getter
得到的Point
结构体只是临时的值
(可以想成计算属性),并不是真正的存储属性point,所以会报错- 通过打印也可以看出来要修改的并不是同一个地址值的point
- 但换成了类,那么通过
getter
得到的Point
类是一个指针变量,而修改的是指向堆空间中的Point
的属性,所以不会报错
3.接收多个参数的下标
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)
七、继承(Inheritance)
1. 基本概念
继承:
值类型(结构体、枚举)不支持继承,只有引用类型的类支持继承基类:
没有父类的类,叫做基类Swift
并没有像OC、Java
那样的规定,任何类最终都要继承自某个基类子类
可以重写从父类
继承过来的下标
、方法
、属性
。重写必须加上override
class Car { func run() { print("run") } } class Truck: Car { override func run() { } }
2.内存结构
看下面几个类的内存占用是多少
class Animal {
var age = 0
}
class Dog: Animal {
var weight = 0
}
class ErHa: Dog {
var iq = 0
}
let a = Animal()
a.age = 10
print(Mems.size(ofRef: a)) // 32
print(Mems.memStr(ofRef: a))
//0x000000010000c3c8
//0x0000000000000003
//0x000000000000000a
//0x000000000000005f
let d = Dog()
d.age = 10
d.weight = 20
print(Mems.size(ofRef: d)) // 32
print(Mems.memStr(ofRef: d))
//0x000000010000c478
//0x0000000000000003
//0x000000000000000a
//0x0000000000000014
let e = ErHa()
e.age = 10
e.weight = 20
e.iq = 30
print(Mems.size(ofRef: e)) // 48
print(Mems.memStr(ofRef: e))
//0x000000010000c548
//0x0000000000000003
//0x000000000000000a
//0x0000000000000014
//0x000000000000001e
//0x0000000000000000
-
- 首先类内部会有16个字节:存储
类信息
和引用计数
- 首先类内部会有16个字节:存储
-
- 然后才是成员变量/常量的内存(
存储属性
)
- 然后才是成员变量/常量的内存(
-
- 又由于堆空间分配内存,存在内存对齐的概念,其原则分配的内存大小为16的倍数且刚好大于或等于初始化一个该数据类型变量所需的字节数
-
- 基于前面的规则,最终得出结论:所分配的内存空间分别占用为
32
、32
、48
- 基于前面的规则,最终得出结论:所分配的内存空间分别占用为
-
- Tips:子类会继承自父类的属性,所以内存会算上父类的属性存储空间
3. 重写实例方法、下标
class Animal {
func speak() {
print("Animal speak")
}
subscript(index: Int) -> Int {
index
}
}
var ani: Animal
ani = Animal()
ani.speak()
print(ani[6])
class Cat: Animal {
override func speak() {
super.speak()
print("Cat speak")
}
override subscript(index: Int) -> Int {
super[index] + 1
}
}
ani = Cat()
ani.speak()
print(ani[7])
-
- 被
class
修饰的类型方法
、下标
,允许被子类重写
class Animal { class func speak() { print("Animal speak") } class subscript(index: Int) -> Int { index } } Animal.speak() print(Animal[6]) class Cat: Animal { override class func speak() { super.speak() print("Cat speak") } override class subscript(index: Int) -> Int { super[index] + 1 } } Cat.speak() print(Cat[7])
- 被
-
- 被
static
修饰的类型方法、下标,不允许被子类重写
- 被
-
- 但是被
class
修饰的类型方法、下标,子类重写时允许使用static
修饰
但再后面的子类就不被允许了
class Animal { class func speak() { print("Animal speak") } class subscript(index: Int) -> Int { index } } Animal.speak() print(Animal[6]) class Cat: Animal { override static func speak() { super.speak() print("Cat speak") } override static subscript(index: Int) -> Int { super[index] + 1 } } Cat.speak() print(Cat[7])
- 但是被
4. 重写属性
-
- 子类可以将父类的属性(存储、计算)重写为计算属性
class Animal { var age = 0 } class Dog: Animal { override var age: Int { set { } get { 10 } } var weight = 0 }
-
- 但子类
不可以
将父类的属性重写为存储属性
- 但子类
-
- 只能重写
var
属性,不能重新let
属性
- 只能重写
-
- 重写时,属性名、类型要一致
-
- 子类重写后的属性权限不能小于父类的属性权限
- 如果父类属性是
只读的
,那么子类重写后的属性可以是只读的
,也可以是可读可写的
- 如果父类属性是
可读可写的
,那么子类重写后的属性也必须是可读可写的
4.1 重写实例属性
class Circle {
// 存储属性
var radius: Int = 0
// 计算属性
var diameter: Int {
set(newDiameter) {
print("Circle setDiameter")
radius = newDiameter / 2
}
get {
print("Circle getDiameter")
return radius * 2
}
}
}
class SubCircle: Circle {
override var radius: Int {
set {
print("SubCircle setRadius")
super.radius = newValue > 0 ? newValue : 0
}
get {
print("SubCircle getRadius")
return super.radius
}
}
override var diameter: Int {
set {
print("SubCircle setDiameter")
super.diameter = newValue > 0 ? newValue : 0
}
get {
print("SubCircle getDiameter")
return super.diameter
}
}
}
var c = SubCircle()
c.radius = 6
print(c.diameter)
c.diameter = 20
print(c.radius)
//SubCircle setRadius
//SubCircle getDiameter
//Circle getDiameter
//SubCircle getRadius
//12
//SubCircle setDiameter
//Circle setDiameter
//SubCircle setRadius
//SubCircle getRadius
//10
-
- 从父类继承过来的
存储属性
,都会分配内存空间,不管
之后会不会被重写为计算属性
- 从父类继承过来的
-
- 如果重写的方法里的
setter
和getter
不写super
,那么就会死循环
class SubCircle: Circle { override var radius: Int { set { print("SubCircle setRadius") radius = newValue > 0 ? newValue : 0 } get { print("SubCircle getRadius") return radius } } }
- 如果重写的方法里的
4.2 重写类型属性
-
- 被
class
修饰的计算类型属性,可以
被子类重写
- 被
class Circle {
// 存储属性
static var radius: Int = 0
// 计算属性
class var diameter: Int {
set(newDiameter) {
print("Circle setDiameter")
radius = newDiameter / 2
}
get {
print("Circle getDiameter")
return radius * 2
}
}
}
class SubCircle: Circle {
override static var diameter: Int {
set {
print("SubCircle setDiameter")
super.diameter = newValue > 0 ? newValue : 0
}
get {
print("SubCircle getDiameter")
return super.diameter
}
}
}
Circle.radius = 6
print(Circle.diameter)
Circle.diameter = 20
print(Circle.radius)
SubCircle.radius = 6
print(SubCircle.diameter)
SubCircle.diameter = 20
print(SubCircle.radius)
//Circle getDiameter
//12
//Circle setDiameter
//10
//SubCircle getDiameter
//Circle getDiameter
//12
//SubCircle setDiameter
//Circle setDiameter
//10
-
- 被
static
修饰的类型属性(计算、存储),不可以
被子类重写
- 被
5. 属性观察器
-
- 可以在子类中为父类属性(除了只读计算属性、
let
属性)增加属性观察器
重写后还是存储属性,不是变成了计算属性
class Circle { var radius: Int = 1 } class SubCircle: Circle { override var radius: Int { willSet { print("SubCircle willSetRadius", newValue) } didSet { print("SubCircle didSetRadius", oldValue, radius) } } } var circle = SubCircle() circle.radius = 10 //SubCircle willSetRadius 10 //SubCircle didSetRadius 1 10
- 可以在子类中为父类属性(除了只读计算属性、
-
- 如果父类里也有属性观察器:
- 那么子类赋值时,会先调用自己的属性观察器
willSet
,然后调用父类的属性观察器willSet
; - 并且在父类里面才是真正的进行赋值
- 然后先父类的
didSet
,最后再调用自己的didSet
class Circle { var radius: Int = 1 { willSet { print("Circle willSetRadius", newValue) } didSet { print("Circle didSetRadius", oldValue, radius) } } } class SubCircle: Circle { override var radius: Int { willSet { print("SubCircle willSetRadius", newValue) } didSet { print("SubCircle didSetRadius", oldValue, radius) } } } var circle = SubCircle() circle.radius = 10 //SubCircle willSetRadius 10 //Circle willSetRadius 10 //Circle didSetRadius 1 10 //SubCircle didSetRadius 1 10
-
- 可以给父类的
计算属性
增加属性观察器
class Circle { var radius: Int { set { print("Circle setRadius", newValue) } get { print("Circle getRadius") return 20 } } } class SubCircle: Circle { override var radius: Int { willSet { print("SubCircle willSetRadius", newValue) } didSet { print("SubCircle didSetRadius", oldValue, radius) } } } var circle = SubCircle() circle.radius = 10 //Circle getRadius //SubCircle willSetRadius 10 //Circle setRadius 10 //Circle getRadius //SubCircle didSetRadius 20 20
- 可以给父类的
上面打印会先调用一次Circle getRadius
是因为在设置值之前会先拿到它的oldValue
,所以需要调用getter
一次
为了测试,我们将oldValue
的获取去掉后,再打印发现就没有第一次的getter
的调用了
6. final
-
- 被
final
修饰的方法
、下标
、属性
,禁止被重写
- 被
-
- 被
final
修饰的类,禁止被继承
- 被
7. 方法调用的本质
-
- 我们先看下面的示例代码,分析
结构体
和类
的调用方法区别是什么
struct Animal { func speak() { print("Animal speak") } func eat() { print("Animal eat") } func sleep() { print("Animal sleep") } } var ani = Animal() ani.speak() ani.eat() ani.sleep()
- 我们先看下面的示例代码,分析
-
- 反汇编之后,发现结构体的方法调用就是直接找到方法所在地址直接调用
结构体的方法地址都是固定的
- 反汇编之后,发现结构体的方法调用就是直接找到方法所在地址直接调用
-
- 接下来 我们在看换成
类
之后反汇编的实现是怎样的
class Animal { func speak() { print("Animal speak") } func eat() { print("Animal eat") } func sleep() { print("Animal sleep") } } var ani = Animal() ani.speak() ani.eat() ani.sleep()
- 接下来 我们在看换成
-
- 反汇编之后,会发现需要
调用的方法地址
是不确定的
所以凡是调用固定地址的都不会是类的方法
的调用
- 而且上述的几个调用的方法地址都是从
rcx
往高地址偏移8
个字节来调用的,也就说明几个方法地址都是连续的 - 我们再来分析下方法调用前做了什么:
- 通过反汇编我们可以看到:
- 会从
全局变量
的指针找到其指向的堆内存
中的类的存储空间 - 然后
再根据类的前8个字节
里的类信息
知道需要调用的方法地址 - 从类信息的地址进行偏移找到
方法地址
,然后调用
- 会从
- 然后我们将示例代码修改一下,再观察其本质是什么
class Animal { func speak() { print("Animal speak") } func eat() { print("Animal eat") } func sleep() { print("Animal sleep") } } class Dog: Animal { override func speak() { print("Dog speak") } override func eat() { print("Dog eat") } func run() { print("Dog run") } } var ani = Animal() ani.speak() ani.eat() ani.sleep() ani = Dog() ani.speak() ani.eat() ani.sleep()
增加了子类后
,Dog的类信息里的方法列表会存有重写后的父类方法,以及自己新增的方法
class Dog: Animal { func run() { print("Dog run") } }
- 如果子类里
没有重写父类方法
,那么类信息里的方法列表会有父类的方法,以及自己新增的方法
- 反汇编之后,会发现需要
八、多态及实现原理
- 面向对象语言三大特性:封装、继承、多态。
- 在OC中多态是用
Runtime
实现的,在C++中用虚表实现多态,今天我们了解一下Swift中的多态及其原理- 和 C++ 类似,都是使用虚表
- 什么是多态?父类指针指向子类对象就是多态
1. 函数调用比较
1.1 结构体的函数
通过汇编分析可以看到:
- 因为
不存在继承重写行为
,调用的函数地址都是在编译时期确定
的。
1.2. 类的函数
speak
函数调用栈:eat
函数调用栈:sleep
函数调用栈- 类生成的汇编代码非常多,相比结构体复杂了很多,并且通过函数调用发现:
- 函数地址是动态变化的
- 所以,如果没有继承行为或简单的类,建议使用结构体,效率更高。(机器指令越少,意味着要执行的代码越高效)
- 类的函数调用地址之所以变化是为因为
- 子类继承父类 会导致
函数实际调用地址
发生变化 - 这也是多态的体现。
- 子类继承父类 会导致
2、汇编分析类的继承
示例代码:
class Animal {
func speak() {
print("Animal speak")
}
func eat() {
print("Animal eat")
}
func sleep() {
print("Animal sleep")
}
}
class Dog: Animal {
override func speak() {
print("Dog speak")
}
override func eat() {
print("Dog eat")
}
func run() {
print("Dog run")
}
}
var animal = Animal()
animal.speak()
animal.eat()
animal.sleep()
/*
输出:
Animal speak
Animal eat
Animal sleep
*/
animal = Dog()
animal.speak()
animal.eat()
animal.sleep()
/*
输出:
Dog speak
Dog eat
Animal sleep
*/
汇编分析:
分析:
- 类的实例前8个字节保存的是类的信息,所以上面的汇编代码会一值围绕着实例
animal
的前8个字节去查找函数地址。 - 而
animal
最后一次指向的是对象Dog
在堆空间的内存,所以最终调用的是Dog
中的speak
函数。 - 其实就是虚表:
callq *0x50(%rcx)
中的0x50
就是偏移量,跳过0x50
就是函数speak
的地址。
总结起来其实很简单:
- 先找到全局变量
animal
的地址; animal
地址保存的是堆空间Dog
对象的内存地址;Dog
对象前8个字节保存的是对象类型信息地址;- 对象类型信息地址保存着类中函数的地址。
注意: 无论创建多少个同类型对象,对象的类型信息都指向同一块内存地址。对象类型信息保存在全局区。
九、初始化
1. 类的初始化器
-
类
、结构体
、枚举
都可以定义初始化器
- 类有两种初始化器:
- 指定初始化器(
designated initializer
) - 便捷初始化器(
convenience initializer
)// 指定初始化器 init(parameters) { statements } // 便捷初始化器 convenience init(parameters) { statements }
-
- 每个类
至少有一个
指定初始化器
指定初始化器是类的主要初始化器
- 每个类
-
- 默认初始化器总是类的指定初始化器
class Size { init() { } init(age: Int) { } convenience init(height: Double) { self.init() } } var s = Size() s = Size(height: 180) s = Size(age: 10)
-
- 类本身会自带一个指定初始化器
class Size { } var s = Size()
-
- 如果有自定义的指定初始化器,默认的指定初始化器就不存在了
-
- 类偏向于
少量指定初始化器
一个类通常只有一个指定初始化器
class Size { var width: Double = 0 var height: Double = 0 init(height: Double, width: Double) { self.width = width self.height = height } convenience init(height: Double) { self.init(height: height, width: 0) } convenience init(width: Double) { self.init(height: 0,width: width) } } let size = Size(height: 180, width: 70)
- 类偏向于
2. 初始化器的相互调用
初始化器的相互调用规则
指定初始化器
必须从它的直系父类调用指定初始化器
便捷初始化器
必须从相同的类里调用另一个初始化器
便捷初始化器
最终必须调用一个指定初始化器
class Person { var age: Int init(age: Int) { self.age = age } convenience init() { self.init(age: 0) self.age = 10 } } class Student: Person { var score: Int init(age: Int, score: Int) { self.score = score super.init(age: age) self.age = 30 } convenience init(score: Int) { self.init(age: 0, score: score) self.score = 100 } }
这一套规则保证了:
使用任何初始化器,都可以完整地初始化实例
3. 两段式初始化和安全检查
Swift在编码安全方面煞费苦心,为了保证初始化过程的安全,设定了两段式初始化
和安全检查
3.1 两段式初始化
第一阶段: 初始化所有存储属性
- 外层调用
指定/便捷
初始化器 - 分配内存给实例,但未初始化
指定初始化器
确保当前类定义的存储属性都初始化指定初始化器
调用父类的初始化器,不断向上调用,形成初始化器链
第二阶段: 设置新的存储属性值
- 从顶部初始化器往下,链中的每一个指定初始化器都有机会进一步定制实例
- 初始化器现在能够使用
self
(访问、修改它的属性、调用它的实例方法等) - 最终,链中任何便捷初始化器都有机会定制实例以及使用
self
3.2 安全检查
指定初始化器
必须保证在调用父类初始化器之前, 其所在类定义的所有存储属性
都要初始化完成指定初始化器
必须先调用父类初始化器,然后才能为继承的属性设置新值便捷初始化器
必须先调用同类中的其他初始化器,然后再为任意属性设置新值初始化器
在第一阶段初始化完成之前,不能调用任何实例方法,不能读取任何实例属性的值,也不能引用self
- 直到第一阶段结束,实例才算完全合法
3.3 重写
-
- 当重写父类的
指定初始化器
时,必须加上override
(即使子类的实现的便捷初始化器
)
- 当重写父类的
-
指定初始化器
只能纵向调用
,可以被子类调用
class Person { var age: Int init(age: Int) { self.age = age } convenience init() { self.init(age: 0) self.age = 10 } } class Student: Person { var score: Int override init(age: Int) { self.score = 0 super.init(age: age) } }
class Person { var age: Int init(age: Int) { self.age = age } convenience init() { self.init(age: 0) self.age = 10 } } class Student: Person { var score: Int init(age: Int, score: Int) { self.score = score super.init(age: age) } override convenience init(age: Int) { self.init(age: age, score: 0) } }
-
- 如果子类写了一个匹配父类
便捷初始化器
的初始化器,不用加override
class Person { var age: Int init(age: Int) { self.age = age } convenience init() { self.init(age: 0) } } class Student: Person { var score: Int init(age: Int, score: Int) { self.score = score super.init(age: age) } convenience init() { self.init(age: 0, score: 0) } }
因为父类的便捷初始化器永远不会通过子类直接调用
因此,严格来说,子类无法重写父类的便捷初始化器
- 如果子类写了一个匹配父类
-
便捷初始化器
只能横向调用,不能被子类调用
子类没有权利更改父类的便捷初始化器
,所以不能叫重写
class Person { var age: Int init(age: Int) { self.age = age } convenience init() { self.init(age: 0) } } class Student: Person { var score: Int init(age: Int, score: Int) { self.score = score super.init(age: age) } init() { self.score = 0 super.init(age: 0) } }
4. 自动继承
-
- 如果子类没有自定义任何指定初始化器,它会自动继承父类所有的指定初始化器
class Person { var age: Int init(age: Int) { self.age = age } } class Student: Person { } var s = Student(age: 20)
class Person { var age: Int init(age: Int) { self.age = age } } class Student: Person { convenience init(name: String) { self.init(age: 0) } } var s = Student(name: "ray") s = Student(age: 20)
-
- 如果子类提供了父类所有
指定初始化器
的实现(要不通过上一种方式继承,要不重新)
class Person { var age: Int init(age: Int) { self.age = age } convenience init(sex: Int) { self.init(age: 0) } } class Student: Person { override init(age: Int) { super.init(age: 20) } } var s = Student(age: 30)
class Person { var age: Int init(age: Int) { self.age = age } convenience init(sex: Int) { self.init(age: 0) } } class Student: Person { init(num: Int) { super.init(age: 0) } override convenience init(age: Int) { self.init(num: 200) } } var s = Student(age: 30)
- 如果子类提供了父类所有
-
- 如果子类自定义了
指定初始化器
,那么父类的指定初始化器
便不会被继承
子类自动继承所有的父类便捷初始化器
- 如果子类自定义了
-
- 就算子类添加了更多的
便捷初始化器
,这些规则仍然适用
class Person { var age: Int init(age: Int) { self.age = age } convenience init(sex: Int) { self.init(age: 0) } } class Student: Person { convenience init(isBoy: Bool) { self.init(age: 20) } convenience init(num: Int) { self.init(age: 20) } } var s = Student(age: 30) s = Student(sex: 24) s = Student(isBoy: true) s = Student(num: 6)
- 就算子类添加了更多的
-
- 子类以
便捷初始化器
的形式重新父类的指定初始化器
,也可以作为满足第二条规则的一部分
class Person { var age: Int init(age: Int) { self.age = age } convenience init(sex: Int) { self.init(age: 0) } } class Student: Person { convenience init(sex: Int) { self.init(age: 20) } } var s = Student(age: 30) s = Student(sex: 24)
- 子类以
5. required
-
- 用
required
修饰指定初始化器
,表明其所有子类都必须实现
该初始化器(通过继承或者重写实现)
- 用
class Person {
var age: Int
init(age: Int) {
self.age = age
}
required init() {
self.age = 0
}
}
class Student: Person {
}
var s = Student(age: 30)
-
- 如果子类重写了
required
初始化器,也必须加上required
,不用加override
- 如果子类重写了
class Person {
var age: Int
init(age: Int) {
self.age = age
}
required init() {
self.age = 0
}
}
class Student: Person {
init(num: Int) {
super.init(age: 0)
}
required init() {
super.init()
}
}
var s = Student(num: 30)
s = Student()
6. 属性观察器
-
- 父类的属性在它自己的初始化器中赋值不会触发
属性观察器
但在子类的初始化器中赋值会触发属性观察器
class Person { var age: Int { willSet { print("willSet", newValue) } didSet { print("didSet", oldValue, age) } } init() { self.age = 0 } } class Student: Person { override init() { super.init() age = 1 } } var s = Student()
- 父类的属性在它自己的初始化器中赋值不会触发
7. 可失败初始化器
-
类
、结构体
、枚举
都可以使用init?
定义可失败初始化器
class Person { var name: String init?(name: String) { if name.isEmpty { return nil } self.name = name } } let p = Person(name: "Jack") print(p)
- 下面这几个也是使用了可失败初始化器
var num = Int("123") enum Answer: Int { case wrong, right } var an = Answer(rawValue: 1)
-
- 不允许同时定义
参数标签
、参数个数
、参数类型相同
的可失败初始化器
和非可失败初始化器
- 不允许同时定义
-
- 可以用
init!
定义隐式解包的可失败初始化器
class Person { var name: String init!(name: String) { if name.isEmpty { return nil } self.name = name } } let p = Person(name: "Jack") print(p)
- 可以用
-
可失败初始化器
可以调用非可失败初始化器
非可失败初始化器
调用可失败初始化器
需要进行解包
class Person { var name: String convenience init?(name: String) { self.init() if name.isEmpty { return nil } self.name = name } init() { self.name = "" } }
class Person { var name: String init?(name: String) { if name.isEmpty { return nil } self.name = name } convenience init() { // 强制解包有风险 self.init(name: "")! self.name = "" } }
-
- 如果初始化器调用一个
可失败初始化器
导致初始化失败
,那么整个初始化过程都失败,并且之后的代码都停止执行
class Person { var name: String init?(name: String) { if name.isEmpty { return nil } self.name = name } convenience init?() { // 如果这一步返回为nil,那么后面的代码就不会继续执行了 self.init(name: "")! self.name = "" } } let p = Person() print(p)
- 如果初始化器调用一个
-
- 可以用一个
非可失败初始化器
重写一个可失败初始化器
,但反过来是不行的
class Person { var name: String init?(name: String) { if name.isEmpty { return nil } self.name = name } } class Student: Person { override init(name: String) { super.init(name: name)! } }
- 可以用一个
7. 反初始化器(deinit)
-
deinit
叫做反初始化器,类似于C++的析构函数
,OC中的dealloc方法
-
- 当类的实例对象被释放内存时,就会调用实例对象的
deinit
方法
class Person { var name: String init(name: String) { self.name = name } deinit { print("Person对象销毁了") } }
- 当类的实例对象被释放内存时,就会调用实例对象的
-
- 父类的
deinit
能被子类继承
- 父类的
-
- 子类的
deinit
实现执行完毕后会调用父类的deinit
class Person { var name: String init(name: String) { self.name = name } deinit { print("Person对象销毁了") } } class Student: Person { deinit { print("Student对象销毁了") } } func test() { let stu = Student(name: "Jack") } test() // 打印 // Student对象销毁了 // Person对象销毁了
- 子类的
-
deinit
不接受任何参数,不能写小括号,不能自行调用
专题系列文章
1. 前知识
- 01-探究iOS底层原理|综述
- 02-探究iOS底层原理|编译器LLVM项目【Clang、SwiftC、优化器、LLVM】
- 03-探究iOS底层原理|LLDB
- 04-探究iOS底层原理|ARM64汇编
2. 基于OC语言探索iOS底层原理
- 05-探究iOS底层原理|OC的本质
- 06-探究iOS底层原理|OC对象的本质
- 07-探究iOS底层原理|几种OC对象【实例对象、类对象、元类】、对象的isa指针、superclass、对象的方法调用、Class的底层本质
- 08-探究iOS底层原理|Category底层结构、App启动时Class与Category装载过程、load 和 initialize 执行、关联对象
- 09-探究iOS底层原理|KVO
- 10-探究iOS底层原理|KVC
- 11-探究iOS底层原理|探索Block的本质|【Block的数据类型(本质)与内存布局、变量捕获、Block的种类、内存管理、Block的修饰符、循环引用】
- 12-探究iOS底层原理|Runtime1【isa详解、class的结构、方法缓存cache_t】
- 13-探究iOS底层原理|Runtime2【消息处理(发送、转发)&&动态方法解析、super的本质】
- 14-探究iOS底层原理|Runtime3【Runtime的相关应用】
- 15-探究iOS底层原理|RunLoop【两种RunloopMode、RunLoopMode中的Source0、Source1、Timer、Observer】
- 16-探究iOS底层原理|RunLoop的应用
- 17-探究iOS底层原理|多线程技术的底层原理【GCD源码分析1:主队列、串行队列&&并行队列、全局并发队列】
- 18-探究iOS底层原理|多线程技术【GCD源码分析1:dispatch_get_global_queue与dispatch_(a)sync、单例、线程死锁】
- 19-探究iOS底层原理|多线程技术【GCD源码分析2:栅栏函数dispatch_barrier_(a)sync、信号量dispatch_semaphore】
- 20-探究iOS底层原理|多线程技术【GCD源码分析3:线程调度组dispatch_group、事件源dispatch Source】
- 21-探究iOS底层原理|多线程技术【线程锁:自旋锁、互斥锁、递归锁】
- 22-探究iOS底层原理|多线程技术【原子锁atomic、gcd Timer、NSTimer、CADisplayLink】
- 23-探究iOS底层原理|内存管理【Mach-O文件、Tagged Pointer、对象的内存管理、copy、引用计数、weak指针、autorelease
3. 基于Swift语言探索iOS底层原理
关于函数
、枚举
、可选项
、结构体
、类
、闭包
、属性
、方法
、swift多态原理
、String
、Array
、Dictionary
、引用计数
、MetaData
等Swift基本语法和相关的底层原理文章有如下几篇:
- 01-📝Swift5常用核心语法|了解Swift【Swift简介、Swift的版本、Swift编译原理】
- 02-📝Swift5常用核心语法|基础语法【Playground、常量与变量、常见数据类型、字面量、元组、流程控制、函数、枚举、可选项、guard语句、区间】
- 03-📝Swift5常用核心语法|面向对象【闭包、结构体、类、枚举】
- 04-📝Swift5常用核心语法|面向对象【属性、inout、类型属性、单例模式、方法、下标、继承、初始化】
- 05-📝Swift5常用核心语法|高级语法【可选链、协议、错误处理、泛型、String与Array、高级运算符、扩展、访问控制、内存管理、字面量、模式匹配】
- 06-📝Swift5常用核心语法|编程范式与Swift源码【从OC到Swift、函数式编程、面向协议编程、响应式编程、Swift源码分析】
其它底层原理专题
1. 底层原理相关专题
2. iOS相关专题
- 01-iOS底层原理|iOS的各个渲染框架以及iOS图层渲染原理
- 02-iOS底层原理|iOS动画渲染原理
- 03-iOS底层原理|iOS OffScreen Rendering 离屏渲染原理
- 04-iOS底层原理|因CPU、GPU资源消耗导致卡顿的原因和解决方案