在 Swift 中跟实例相关的属性可以分为两大类:存储属性(Stored Property),计算属性(Computed Property)。接下来我们将去了解 Swift 中的属性。
一、存储属性(Stored Property)
存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性 (由 var 关键字引入)要么是常量存储属性(由 let 关键字引入)。
关于存储属性,Swift 有个明确的规定,在创建类或结构体的实例时,必须为所有的存储属性设置一个初始值。可以在初始化器里为存储属性设置一个初始值,可以分配一个默认的属性值作为定义的一部分。
struct SHPoint {
var x: Double = 10
let y: Double = 20
}
class SHPerson {
var age: Int
let name: String
init(_ age: Int, name: String) {
self.age = age
self.name = name
}
}
let point = SHPoint()
let person = SHPerson(18, name: "Coder_张三")
类似于成员变量这个概念,存储在实例的内存中,结构体、类可以定义存储属性,但枚举不可以定义存储属性。
二、计算属性(Computed Property)
除了存储属性,类、结构体和枚举也能够定义计算属性,计算属性并不存储值,他们提供 getter 和 setter 来修改和获取值。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量。于此同时我们书写计算属性时候必须包含类型,因为编译器需 要知道期望返回值是什么。
1. 计算属性写法
在 Swift 中,重写一个实例的 setter 和 getter 可以用 set、get 来修饰,setter 传入新的默认值叫做 newValue,也可以自定义。Swift 中,在拥有返回值的方法里,如果方法内部只有 return,那么可以直接省略 return。
struct SHCircle {
var radius: Double
var diameter: Double {
// set(newDiameter) {
// radius = newDiameter / 2
// }
set {
radius = newValue / 2
}
get {
radius * 2
}
}
}
2. 只读计算属性
只读计算属性:只有 get,没有 set。
struct SHCircle {
var radius: Double
var diameter: Double {
get {
radius * 2
}
}
}
或者我们可以这样写:
struct SHCircle {
var radius: Double
private (set) var diameter: Double
init(_ radius: Double) {
self.radius = 10
diameter = radius * 2
}
}
那这两种只读计算有什么区别呢?前者不管是内部访问还是外部访问,都不能进行赋值。而后者是内部可以进行赋值,外部不能进行赋值。
3. 计算属性的本质
那么计算属性和存储属性的本质有什么区别呢?我们可以查看 .sil 文件中的代码是如何实现的。了解 .sil 文件可以去看看前面的两篇文章 《结构体与类》 和 《方法》。
ViewController.swift 文件的代码如下:
import UIKit
struct SHCircle {
var radius: Double
var diameter: Double {
set {
radius = newValue / 2
}
get {
radius * 2
}
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var c = SHCircle(radius: 10)
c.radius = 20
c.diameter = 30
let _ = c.diameter
}
}
3.1. SHCircle 结构体在 sil 中的声明
我们来看一下 SHCircle 结构体在 sil 中如何声明的:
struct SHCircle {
@_hasStorage var radius: Double { get set }
var diameter: Double { get set }
init(radius: Double)
}
radius 和 diameter 后面虽然有 { get set },但前面的修饰符却是又区别的。radius 有 @_hasStorage 修饰,diameter 没有,说明 radius 属性是一个可存储的值,这点和第一大点就相呼应了。而 diameter 没有 @_hasStorage 修饰,那么是不是说明,diameter 只有 setter 和 getter 方法呢。
3.2. radius 属性的 getter 和 setter 在 sil 的内部实现
我们先来看一下 radius 属性的 getter 和 setter 在 .sil 的内部实现:
// SHCircle.radius.getter
sil hidden [transparent] @$s14ViewController8SHCircleV6radiusSdvg : $@convention(method) (SHCircle) -> Double {
// %0 "self" // users: %2, %1
bb0(%0 : $SHCircle):
debug_value %0 : $SHCircle, let, name "self", argno 1 // id: %1
%2 = struct_extract %0 : $SHCircle, #SHCircle.radius // user: %3
return %2 : $Double // id: %3
} // end sil function '$s14ViewController8SHCircleV6radiusSdvg'
// SHCircle.radius.setter
sil hidden [transparent] @$s14ViewController8SHCircleV6radiusSdvs : $@convention(method) (Double, @inout SHCircle) -> () {
// %0 "value" // users: %6, %2
// %1 "self" // users: %4, %3
bb0(%0 : $Double, %1 : $*SHCircle):
debug_value %0 : $Double, let, name "value", argno 1 // id: %2
debug_value_addr %1 : $*SHCircle, var, name "self", argno 2 // id: %3
%4 = begin_access [modify] [static] %1 : $*SHCircle // users: %7, %5
%5 = struct_element_addr %4 : $*SHCircle, #SHCircle.radius // user: %6
store %0 to %5 : $*Double // id: %6
end_access %4 : $*SHCircle // id: %7
%8 = tuple () // user: %9
return %8 : $() // id: %9
} // end sil function '$s14ViewController8SHCircleV6radiusSdvs'
可以看到,在调用 radius 的 getter 的时候,底层的 sil 拿到 SHCircle 中 radius 的值直接返回。在调用 radius 的 setter 的时候,会取 SHCircle 实例的地址,对 radius 进行修改。
3.3. diameter 属性的 getter 和 setter 在 sil 的内部实现
接下来我们看一下 diameter 属性的 setter 和 getter 在 sil 的内部实现:
// SHCircle.diameter.setter
sil hidden @$s14ViewController8SHCircleV8diameterSdvs : $@convention(method) (Double, @inout SHCircle) -> () {
// %0 "newValue" // users: %5, %2
// %1 "self" // users: %8, %3
bb0(%0 : $Double, %1 : $*SHCircle):
debug_value %0 : $Double, let, name "newValue", argno 1 // id: %2
debug_value_addr %1 : $*SHCircle, var, name "self", argno 2 // id: %3
%4 = float_literal $Builtin.FPIEEE64, 0x4000000000000000 // 2 // user: %6
%5 = struct_extract %0 : $Double, #Double._value // user: %6
%6 = builtin "fdiv_FPIEEE64"(%5 : $Builtin.FPIEEE64, %4 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %7
%7 = struct $Double (%6 : $Builtin.FPIEEE64) // user: %10
%8 = begin_access [modify] [static] %1 : $*SHCircle // users: %11, %9
%9 = struct_element_addr %8 : $*SHCircle, #SHCircle.radius // user: %10
store %7 to %9 : $*Double // id: %10
end_access %8 : $*SHCircle // id: %11
%12 = tuple () // user: %13
return %12 : $() // id: %13
} // end sil function '$s14ViewController8SHCircleV8diameterSdvs'
注意到,diameter 属性 setter 的内部实现会自动生成一个名为 newValue 的常量,并且会把外部传进来的值赋值给 newValue。
// SHCircle.diameter.getter
sil hidden @$s14ViewController8SHCircleV8diameterSdvg : $@convention(method) (SHCircle) -> Double {
// %0 "self" // users: %2, %1
bb0(%0 : $SHCircle):
debug_value %0 : $SHCircle, let, name "self", argno 1 // id: %1
%2 = struct_extract %0 : $SHCircle, #SHCircle.radius // user: %4
%3 = float_literal $Builtin.FPIEEE64, 0x4000000000000000 // 2 // user: %5
%4 = struct_extract %2 : $Double, #Double._value // user: %5
%5 = builtin "fmul_FPIEEE64"(%4 : $Builtin.FPIEEE64, %3 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %6
%6 = struct $Double (%5 : $Builtin.FPIEEE64) // user: %7
return %6 : $Double // id: %7
} // end sil function '$s14ViewController8SHCircleV8diameterSdvg'
从观察 diameter 属性 的 setter 和 getter 中,并未发现有 diameter 相关的存储变量。所以其实,计算属性根本不会有存储在实例的成员变量,那也就意味着计算属性不占内存。
3.4. 计算属性汇编实现
为了更加直观的感受,我们直接看最终编译成的汇编代码,我们将断点打在 c.diameter = 30 处,汇编代码如下:
通过汇编观察,发现,c.diameter = 30 这句代码的本质就是去调用 setter ,let _ = c.diameter 这句代码的本质就是去调用 getter。
由此可以得出:计算属性的本质就是方法(函数),并且计算属性并不占用实例的内存。
三、延迟存储属性(Lazy Stored Property)
1. lazy 的使用
- 使用
lazy可以定义一个延迟存储属性,在第一次用到属性的时候才会进行初始化。 lazy属性必须是var,不能是let,因为let必须在实例的初始化方法完成之前就拥有值。- 如果多条线程同时第一次访问
lazy属性,无法保证属性只被初始化 1 次。
// 定义:
class SHCar {
init() {
print("SHCar init!")
}
func run() {
print("SHCar is running!")
}
}
class SHPerson {
lazy var car = SHCar()
init() {
print("SHPerson init!")
}
func goOut() { car.run() }
}
// 调用:
let p = SHPerson()
print("--------")
p.goOut()
打印结果:
SHPerson init!
--------
SHCar init!
SHCar is running!
2. lazy 的注意点
需要注意的是:
- 当结构体包含一个延迟存储属性时,只有
var才能访问延迟存储属性,因为延迟属性初始化时需要改变结构体的内存。
struct SHPoint {
var x = 0.0
var y = 0.0
lazy var z = 0.0
}
3. lazy 在底层 sil 的调用实现
代码还是用第 1 点 的代码,我们将其生成 sil 文件之后,SHPerson 的声明定义如下:
class SHPerson {
lazy var car: SHCar { get set }
@_hasStorage @_hasInitialValue final var $__lazy_storage_$_car: SHCar? { get set }
init()
func goOut()
@objc deinit
}
存储属性在添加了 lazy 修饰后,除了拥有存储属性的特性之外,在底层的 sil 代码还生成了一行代码。我们注意观察,这行代码拥有 final 修饰符,说明 lazy 修饰的属性不能被重写。并且,它是一个可选项。拥有可选项就意味着,其实在初始的时候是有值的,只是这个值是一个 nil。
我们再来看 car 属性 getter 的实现:
// SHPerson.car.getter
sil hidden [lazy_getter] [noinline] @$s14ViewController8SHPersonC3carAA5SHCarCvg : $@convention(method) (@guaranteed SHPerson) -> @owned SHCar {
// %0 "self" // users: %17, %2, %1
bb0(%0 : $SHPerson):
debug_value %0 : $SHPerson, let, name "self", argno 1 // id: %1
%2 = ref_element_addr %0 : $SHPerson, #SHPerson.$__lazy_storage_$_car // user: %3
%3 = begin_access [read] [dynamic] %2 : $*Optional<SHCar> // users: %4, %6
%4 = load %3 : $*Optional<SHCar> // users: %7, %5
retain_value %4 : $Optional<SHCar> // id: %5
end_access %3 : $*Optional<SHCar> // id: %6
switch_enum %4 : $Optional<SHCar>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %7
// %8 // users: %10, %9
bb1(%8 : $SHCar): // Preds: bb0
debug_value %8 : $SHCar, let, name "tmp1" // id: %9
br bb3(%8 : $SHCar) // id: %10
bb2: // Preds: bb0
%11 = metatype $@thick SHCar.Type // user: %13
// function_ref SHCar.__allocating_init()
%12 = function_ref @$s14ViewController5SHCarCACycfC : $@convention(method) (@thick SHCar.Type) -> @owned SHCar // user: %13
%13 = apply %12(%11) : $@convention(method) (@thick SHCar.Type) -> @owned SHCar // users: %16, %15, %23, %14
debug_value %13 : $SHCar, let, name "tmp2" // id: %14
strong_retain %13 : $SHCar // id: %15
%16 = enum $Optional<SHCar>, #Optional.some!enumelt, %13 : $SHCar // user: %20
%17 = ref_element_addr %0 : $SHPerson, #SHPerson.$__lazy_storage_$_car // user: %18
%18 = begin_access [modify] [dynamic] %17 : $*Optional<SHCar> // users: %20, %19, %22
%19 = load %18 : $*Optional<SHCar> // user: %21
store %16 to %18 : $*Optional<SHCar> // id: %20
release_value %19 : $Optional<SHCar> // id: %21
end_access %18 : $*Optional<SHCar> // id: %22
br bb3(%13 : $SHCar) // id: %23
// %24 // user: %25
bb3(%24 : $SHCar): // Preds: bb2 bb1
return %24 : $SHCar // id: %25
} // end sil function '$s14ViewController8SHPersonC3carAA5SHCarCvg'
代码稍微有点长,注意看 bb0 这一段代码,其中有一行代码如下:
switch_enum %4 : $Optional<SHCar>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %7
-
它是根据可选项来判断 car 属性是否有值,如果有值,走
bb1,否则走bb2。 -
在
bb2中调用了SHCar.__allocating_init()方法,所以bb2是对 car 属性进行初始化。 -
最后都走
bb3,将 car 的值返回。
用 lazy 修饰在底层只是用一个可选项来判断修饰的属性是否有值,并未看见有线程安全相关的代码。所以其实 lazy 并不是线程安全的,感兴趣的靓仔可以自己在多线程环境下,测试 lazy 修饰属性的线程安全问题。
四、属性观察器(Property Observer)
1. willSet 和 didSet
-
属性观察者会观察用来观察属性值的变化,一个
willSet当属性将被改变调用,即使这个值与原有的值相同,而didSet在属性已经改变之后调用。 -
在使用上有点像在 OC 中使用 setter,比如设置 cell 的数据源的时候传的 Model。但属性观察器更多的是像 OC 的 KVO,都是为了监听某个属性值的改变,并且都可以监听到
newValue和oldValue。 -
willSet会传递新值,默认叫newValue,didSet会传递旧值,默认叫oldValue。 -
在初始化器中设置属性值不会触发
willSet和didSet。在属性定义时设置初始值也不会触发willSet和didSet。
代码如下:
class SHPoint {
var x: Double {
willSet {
print("x willSet newValue: \(newValue)")
}
didSet {
print("x didSet oldValue: \(oldValue)")
}
}
var y: Double = 20 {
willSet {
print("y willSet newValue: \(newValue)")
}
didSet {
print("y didSet oldValue: \(oldValue)")
}
}
init(x: Double) {
self.x = x
}
}
print("begin")
let p = SHPoint(x: 10)
print("-----")
p.x = 15
p.y = 30
print("end")
begin
-----
x willSet newValue: 15.0
x didSet oldValue: 10.0
y willSet newValue: 30.0
y didSet oldValue: 20.0
end
2. 属性观察器在继承关系下调用顺序
我们添加一个继承自 SHPoint 的 SHRect 类,在 SHRect 中重写 x 和 y 属性,代码如下:
class SHRect: SHPoint {
override var x: Double {
willSet {
print("override x willSet newValue: \(newValue)")
}
didSet {
print("override x didSet oldValue: \(oldValue)")
}
}
override var y: Double {
willSet {
print("override y willSet newValue: \(newValue)")
}
didSet {
print("override y didSet oldValue: \(oldValue)")
}
}
var width: Double = 100
var height: Double = 100
}
调用如下:
print("begin")
let p = SHRect(x: 10)
print("-----")
p.x = 15
p.y = 30
print("end")
begin
-----
override x willSet newValue: 15.0
x willSet newValue: 15.0
x didSet oldValue: 10.0
override x didSet oldValue: 10.0
override y willSet newValue: 30.0
y willSet newValue: 30.0
y didSet oldValue: 20.0
override y didSet oldValue: 20.0
end
我们来总结一下调用顺序:子类 willSet -> 父类 willSet -> 父类 didSet -> 子类 didSet。
3. willSet 和 didSet 底层调用
我们生成 sil 的代码之后,先来看一下 SHPoint 中 x 的 setter:
// SHPoint.x.setter
sil hidden @$s14ViewController7SHPointC1xSdvs : $@convention(method) (Double, @guaranteed SHPoint) -> () {
// %0 "value" // users: %13, %10, %2
// %1 "self" // users: %16, %11, %10, %4, %3
bb0(%0 : $Double, %1 : $SHPoint):
debug_value %0 : $Double, let, name "value", argno 1 // id: %2
debug_value %1 : $SHPoint, let, name "self", argno 2 // id: %3
%4 = ref_element_addr %1 : $SHPoint, #SHPoint.x // user: %5
%5 = begin_access [read] [dynamic] %4 : $*Double // users: %6, %7
%6 = load %5 : $*Double // users: %8, %16
end_access %5 : $*Double // id: %7
debug_value %6 : $Double, let, name "tmp" // id: %8
// function_ref SHPoint.x.willset
%9 = function_ref @$s14ViewController7SHPointC1xSdvw : $@convention(method) (Double, @guaranteed SHPoint) -> () // user: %10
%10 = apply %9(%0, %1) : $@convention(method) (Double, @guaranteed SHPoint) -> ()
%11 = ref_element_addr %1 : $SHPoint, #SHPoint.x // user: %12
%12 = begin_access [modify] [dynamic] %11 : $*Double // users: %13, %14
store %0 to %12 : $*Double // id: %13
end_access %12 : $*Double // id: %14
// function_ref SHPoint.x.didset
%15 = function_ref @$s14ViewController7SHPointC1xSdvW : $@convention(method) (Double, @guaranteed SHPoint) -> () // user: %16
%16 = apply %15(%6, %1) : $@convention(method) (Double, @guaranteed SHPoint) -> ()
%17 = tuple () // user: %18
return %17 : $() // id: %18
} // end sil function '$s14ViewController7SHPointC1xSdvs'
如果加了 willSet 和 didSet,底层的 sil 代码会在属性的 setter 中,调用 willset 和 didset 方法。这两个方法拥有两个参数,第一个参数对应的应该是 newValue 和 oldValue。
我们再来看一下 SHRect 中重写 x 之后的 setter 调用情况:
// SHRect.x.setter
sil hidden @$s14ViewController6SHRectC1xSdvs : $@convention(method) (Double, @guaranteed SHRect) -> () {
// %0 "value" // users: %17, %13, %2
// %1 "self" // users: %15, %14, %5, %4, %20, %13, %3
bb0(%0 : $Double, %1 : $SHRect):
debug_value %0 : $Double, let, name "value", argno 1 // id: %2
debug_value %1 : $SHRect, let, name "self", argno 2 // id: %3
strong_retain %1 : $SHRect // id: %4
%5 = upcast %1 : $SHRect to $SHPoint // users: %11, %6
%6 = ref_element_addr %5 : $SHPoint, #SHPoint.x // user: %7
%7 = begin_access [read] [dynamic] %6 : $*Double // users: %8, %9
%8 = load %7 : $*Double // users: %10, %20
end_access %7 : $*Double // id: %9
debug_value %8 : $Double, let, name "tmp" // id: %10
strong_release %5 : $SHPoint // id: %11
// function_ref SHRect.x.willset
%12 = function_ref @$s14ViewController6SHRectC1xSdvw : $@convention(method) (Double, @guaranteed SHRect) -> () // user: %13
%13 = apply %12(%0, %1) : $@convention(method) (Double, @guaranteed SHRect) -> ()
strong_retain %1 : $SHRect // id: %14
%15 = upcast %1 : $SHRect to $SHPoint // users: %18, %17
// function_ref SHPoint.x.setter
%16 = function_ref @$s14ViewController7SHPointC1xSdvs : $@convention(method) (Double, @guaranteed SHPoint) -> () // user: %17
%17 = apply %16(%0, %15) : $@convention(method) (Double, @guaranteed SHPoint) -> ()
strong_release %15 : $SHPoint // id: %18
// function_ref SHRect.x.didset
%19 = function_ref @$s14ViewController6SHRectC1xSdvW : $@convention(method) (Double, @guaranteed SHRect) -> () // user: %20
%20 = apply %19(%8, %1) : $@convention(method) (Double, @guaranteed SHRect) -> ()
%21 = tuple () // user: %22
return %21 : $() // id: %22
} // end sil function '$s14ViewController6SHRectC1xSdvs'
注意看,在 SHRect 中调用 x 的 setter 之后,会先调用自身的 willset ,然后调用 SHPoint 的 setter,最后调用 自身的 didset。所以这个时候,就明白了 第 2 点的调用顺序是怎么来的了。
4. 全局变量、局部变量
属性观察器,计算属性的功能,同样可以应用在全局变量、局部变量身上。
使用属性观察器的功能:
// 全局变量
var num: Int = 0 {
willSet {
print("willSet num newValue: \(newValue)")
}
didSet {
print("didSet num oldValue: \(oldValue), currentValue: \(num)")
}
}
// 局部变量
func test() {
var age = 10 {
willSet {
print("willSet age newValue: \(newValue)")
}
didSet {
print("didSet age oldValue: \(oldValue), currentValue: \(age)")
}
}
age = 11
}
// 调用
num = 11
print("-------")
test()
打印如下:
willSet num newValue: 11
didSet num oldValue: 0, currentValue: 11
-------
willSet age newValue: 20
didSet age oldValue: 10, currentValue: 20
使用计算属性的功能:
// 全局变量
var num: Int {
get {
return 10
}
set {
print("setter num: \(newValue)")
}
}
// 局部变量
func test() {
var age: Int {
get {
return 10
}
set {
print("setter age: \(newValue)")
}
}
age = 20
}
// 调用
num = 11
print("-------")
test()
打印如下:
setter num: 11
-------
setter age: 20
五、类型属性(Type Property)
严格来说,属性可以分为实例属性(Instance Property)和类型属性(Type Property)。
1. 实例属性
-
存储实例属性(Stored Instance Property):存储在实例的内存中,每个实例都有1份。
-
计算实例属性(Computed Instance Property)
实例属性只能通过实例去访问,在上面几点所讲的属性都属于实例属性。
2. 类型属性
-
存储类型属性(Stored Type Property):整个程序运行过程中,就只有1份内存(类似于全局变量)。
-
计算类型属性(Computed Type Property)
-
可以通过 static 定义类型属性,如果是类,也可以用关键字 class。
类型属性,只能通过类型去访问。在结构体中定义一个类型属性,可以通过 static 来修饰。
struct SHCar {
static var count: Int = 0
}
SHCar.count += 1;
-
不同于存储实例属性,你必须给存储类型属性设定初始值,因为类型没有像实例那样的
init初始化器来初始化存储属性。 -
存储类型属性默认就是
lazy,会在第一次使用的时候才初始化,就算被多个线程同时访问,保证只会初始化一次。 -
存储类型属性可以是
let。 -
枚举类型也可以定义类型属性(存储类型属性、计算类型属性)。
3. 单例模式
3.1 OC 单例模式对比 Swift 单例模式
在 OC 中设计单例的代码大致如下:
+ (SHNetworkManager *)sharedManager;
+ (SHNetworkManager *)sharedManager {
static SHNetworkManager* instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[SHNetworkManager alloc] init];
});
return instance;
}
在 Swift 中设计的单例的代码大致如下:
public class SHNetworkManager {
public static let shared = SHNetworkManager()
private init() {}
}
或者:
public class SHNetworkManager {
public static let shared = {
// ....
return SHNetworkManager()
}()
private init() { }
}
在初始化器 init 前用 private 修饰,可以将被修饰的初始化器进行私有,外部访问只能通过 shared 来进行访问。
在 OC 中,我们在设计单例模式的时候,为了保证线程安全,通常会配合 dispatch_once 来创建实例,以保证单例对象在内存中只有一份。但在写 Swift 的单例的时候,并不需要写 dispatch_once 来配合创建实例,那 Swift 的单例是如何保证线程安全的呢?请往下看。
3.2 Swift 单例模式的本质
编译出 sil 代码后,单例的声明如下:
@_hasMissingDesignatedInitializers public class SHNetworkManager {
@_hasStorage @_hasInitialValue public static let shared: SHNetworkManager { get }
private init()
@objc deinit
}
我们接下来看一下 shared 的 getter 是如何实现的:
// static SHNetworkManager.shared.getter
sil [transparent] @$s14ViewController16SHNetworkManagerC6sharedACvgZ : $@convention(method) (@thick SHNetworkManager.Type) -> @owned SHNetworkManager {
// %0 "self" // user: %1
bb0(%0 : $@thick SHNetworkManager.Type):
debug_value %0 : $@thick SHNetworkManager.Type, let, name "self", argno 1 // id: %1
// function_ref SHNetworkManager.shared.unsafeMutableAddressor
%2 = function_ref @$s14ViewController16SHNetworkManagerC6sharedACvau : $@convention(thin) () -> Builtin.RawPointer // user: %3
%3 = apply %2() : $@convention(thin) () -> Builtin.RawPointer // user: %4
%4 = pointer_to_address %3 : $Builtin.RawPointer to [strict] $*SHNetworkManager // user: %5
%5 = load %4 : $*SHNetworkManager // users: %7, %6
strong_retain %5 : $SHNetworkManager // id: %6
return %5 : $SHNetworkManager // id: %7
} // end sil function '$s14ViewController16SHNetworkManagerC6sharedACvgZ'
在 getter 中,调用了一个 unsafeMutableAddressor 方法,unsafeMutableAddressor 的实现如下:
// SHNetworkManager.shared.unsafeMutableAddressor
sil [global_init] @$s14ViewController16SHNetworkManagerC6sharedACvau : $@convention(thin) () -> Builtin.RawPointer {
bb0:
%0 = global_addr @$s14ViewController16SHNetworkManagerC6shared_Wz : $*Builtin.Word // user: %1
%1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
// function_ref one-time initialization function for shared
%2 = function_ref @$s14ViewController16SHNetworkManagerC6shared_WZ : $@convention(c) () -> () // user: %3
%3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
%4 = global_addr @$s14ViewController16SHNetworkManagerC6sharedACvpZ : $*SHNetworkManager // user: %5
%5 = address_to_pointer %4 : $*SHNetworkManager to $Builtin.RawPointer // user: %6
return %5 : $Builtin.RawPointer // id: %6
} // end sil function '$s14ViewController16SHNetworkManagerC6sharedACvau'
注意!这段代码有 once 这个敏感的字眼,因为 OC 是通过配合 dispatch_once 来创建单例的,并且上面有注释:function_ref one-time initialization function for shared,大概意思是用于共享的一次性初始化函数。可是貌似这么看,并没有看出太多东西。
接下来我编译成 ir 的代码,全局来查找 once。用 swiftc -emit-ir -target x86_64-apple-ios13.5-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) ViewController.swift > ViewController.ll 。
编译后并且找到 once 相关的代码如下:
define swiftcc i8* @"$s14ViewController16SHNetworkManagerC6sharedACvau"() #0 {
entry:
%0 = load i64, i64* @"$s14ViewController16SHNetworkManagerC6shared_Wz", align 8
%1 = icmp eq i64 %0, -1
%2 = call i1 @llvm.expect.i1(i1 %1, i1 true)
br i1 %2, label %once_done, label %once_not_done
once_done: ; preds = %once_not_done, %entry
%3 = load i64, i64* @"$s14ViewController16SHNetworkManagerC6shared_Wz", align 8
%4 = icmp eq i64 %3, -1
call void @llvm.assume(i1 %4)
ret i8* bitcast (%T14ViewController16SHNetworkManagerC** @"$s14ViewController16SHNetworkManagerC6sharedACvpZ" to i8*)
once_not_done: ; preds = %entry
call void @swift_once(i64* @"$s14ViewController16SHNetworkManagerC6shared_Wz", i8* bitcast (void ()* @"$s14ViewController16SHNetworkManagerC6shared_WZ" to i8*), i8* undef)
br label %once_done
}
注意看这个混写过后的方法 s14ViewController16SHNetworkManagerC6sharedACvau,这个方法在 sil 的代码中是 SHNetworkManager.shared.unsafeMutableAddressor 这个方法,正好是对应的。
看到 once_not_done: 这一段代码,在这段代码中调用了一个函数:swift_once,在调用之后跳转到 once_done,返回 SHNetworkManager 的实例。
那么 swift_once 这个是什么方法呢?它是以 swift 开头的,我猜测它是 swift 内部的一个函数。接下来我在 swift 源码中找到了它。它的实现在源码中的 Once.cpp 文件中
其源码实现如下:
/// Runs the given function with the given context argument exactly once.
/// The predicate argument must point to a global or static variable of static
/// extent of type swift_once_t.
void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
void *context) {
#ifdef SWIFT_STDLIB_SINGLE_THREADED_RUNTIME
if (! *predicate) {
*predicate = true;
fn(context);
}
#elif defined(__APPLE__)
dispatch_once_f(predicate, context, fn);
#elif defined(__CYGWIN__)
_swift_once_f(predicate, context, fn);
#else
std::call_once(*predicate, [fn, context]() { fn(context); });
#endif
}
注意看如果等于 __APPLE__,会调用 dispatch_once_f 函数,这个玩意儿不就是 iOS 中 GCD 的函数吗。所以 Swift 中单例模式在底层中是和 OC 一样,都调用了 GCD 的函数。并且这个函数在执行一次后就不会在执行,以保证线程安全。
六、属性在 Mach-O 文件的位置信息
1. 源码探索属性存储信息的结构
在上篇文章《方法》中已经知道了类的方法存放在 VTable 中。
Swift 类的本质是一个 HeapObject 的结构体指针,在 HeapObject 中有一个 metadata ,metadata 中有一个 Description 成员变量。Description 是名为 TargetClassDescriptor 的类,在 TargetClassDescriptor 中有一个 VTable 存储 Swift 类的方法。
那么在 Swift 中类的属性又是存放在什么地方呢?我们来回顾一下 TargetClassDescriptor 的结构:
class TargetClassDescriptor {
var flags: UInt32
var parent: UInt32
var name: Int32
var accessFunctionPointer: Int32
var fieldDescriptor: Int32
var superClassType: Int32
var metadataNegativeSizeInWords: UInt32
var metadataPositiveSizeInWords: UInt32
var numImmediateMembers: UInt32
var numFields: UInt32
var fieldOffsetVectorOffset: UInt32
var Offset: UInt32
// var size: UInt32
//V-Table
}
Swift 类的属性存放在 fieldDescriptor 中的,其源码定义在 Metadata.h 中的 TargetTypeContextDescriptor 类中,这里需要注意,TargetClassDescriptor 继承自 TargetTypeContextDescriptor。
源码的定义如下:
/// A pointer to the field descriptor for the type, if any.
TargetRelativeDirectPointer<Runtime, const reflection::FieldDescriptor, /*nullable*/ true> Fields;
一开始的时候我找不到 Fields 相关的东西,但我看到了 FieldDescriptor,我在当前文件搜索 FieldDescriptor,发现有一段代码:
namespace reflection {
class FieldDescriptor;
}
这会儿终于看到了,点击去,查看 FieldDescriptor 类的实现,如下:
我将其整理出来,那么 FieldDescriptor 的结构大致如下:
class FieldDescriptor {
MangledTypeName int32
Superclass int32
Kind uint16
FieldRecordSize uint16
NumFields uint32
FieldRecords [FieldRecord]
}
FieldRecords 记录了每个属性的信息,其实在源码中并没有看到 FieldRecords 这个成员变量,这里这么写只是为了方便理解。其实 FieldRecords 在源码中的定义是这样的:
using const_iterator = FieldRecordIterator;
并且在 FieldDescriptor 中有这么几个方法:
const_iterator begin() const {
auto Begin = getFieldRecordBuffer();
auto End = Begin + NumFields;
return const_iterator { Begin, End };
}
const_iterator end() const {
auto Begin = getFieldRecordBuffer();
auto End = Begin + NumFields;
return const_iterator { End, End };
}
llvm::ArrayRef<FieldRecord> getFields() const {
return {getFieldRecordBuffer(), NumFields};
}
从这三个方法和其定义大概可以猜得出这是迭代器,那么这个迭代器是 FieldRecordIterator ,其源码结构如下:
struct FieldRecordIterator {
const FieldRecord *Cur;
const FieldRecord * const End;
FieldRecordIterator(const FieldRecord *Cur, const FieldRecord * const End) : Cur(Cur), End(End) {}
const FieldRecord &operator*() const {
return *Cur;
}
const FieldRecord *operator->() const {
return Cur;
}
FieldRecordIterator &operator++() {
++Cur;
return *this;
}
bool operator==(const FieldRecordIterator &other) const {
return Cur == other.Cur && End == other.End;
}
bool operator!=(const FieldRecordIterator &other) const {
return !(*this == other);
}
};
看到其成员变量的类型为 FieldRecord,我们来看一下它在源码的结构:
class FieldRecord {
const FieldRecordFlags Flags;
public:
const RelativeDirectPointer<const char> MangledTypeName;
const RelativeDirectPointer<const char> FieldName;
FieldRecord() = delete;
bool hasMangledTypeName() const {
return MangledTypeName;
}
StringRef getMangledTypeName() const {
return Demangle::makeSymbolicMangledNameStringRef(MangledTypeName.get());
}
StringRef getFieldName() const {
return FieldName.get();
}
bool isIndirectCase() const {
return Flags.isIndirectCase();
}
bool isVar() const {
return Flags.isVar();
}
};
它有三个成员变量:Flags、MangledTypeName、FieldName。Flags 不用管,MangledTypeName 为属性的类型信息,FieldName 为属性的名称信息。
到这里,我们基本可以确定属性存储在底层源码的结构以及位置了。我们的查找流程如下:
- 找到 HeapObject。
- 从 HeapObject 中找到 HeapMetadata。
- 继续跟进,HeapMetadata 为 TargetHeapMetadata 的别名。
- 找到 TargetHeapMetadata 结构体,并且找到了
Fields成员变量。 - 通过
Fields找到了 FieldDescriptor 的定义,并且找到其中的 FieldRecords。 - 通过迭代器 FieldRecordIterator 找到了 FieldRecord。
2. Mach-O 文件查找属性信息的流程
在了解属性信息在源码中的结构及位置之后,我这个时候需要在 Mach-O 中找到属性相关的东西。新建一个项目,注意,命名不能有中文,添加以下代码,进行编译:
class SHPerson {
var age = 18
var name = "Coder_张三"
}
用 MachOView 将项目的可执行文件打开。
找到 Descriptor 在 Mach-O 文件的内存地址。相加之后得到的内存地址,还需要减掉虚拟内存的基地址,虚拟内存的基地址为 0x100000000,计算过程如下:
0x3F40 + 0xFFFFFF54 = 0x100003E94
0x100003E94 - 0x100000000 = 0x3E94
得到的结果 3E94,3E94 的内存地址在 Mach-O 文件的位置如下:
如图所示, 3E94 在 Mach-O 文件 const 的位置,50 00 00 80 就是 3E94 内存地址的开始,我们要找到 fieldDescriptor ,需要偏移 fieldDescriptor 前面的四个成员变量的大小,也就是 4 个 4 字节。
偏移到 74 00 00 00 ,74 00 00 00 其实是 fieldDescriptor 的偏移信息,所以要找到 fieldDescriptor 在 Mach-O 文件当中的具体位置,还需要加上 74 00 00 00 ,计算过程如下:
0x3EA0 + 0x4 + 0x74 = 0x3F18
计算的结果为 3F18,3F18 内存地址在 Mach-O 文件的位置如下:
如图所示, 3F18 在 Mach-O 文件 fieldmd 的位置。
3F18 为 FieldDescriptor 结构的首地址,要找到 FieldRecords 的具体位置,需要偏移 FieldRecords 之前的成员变量的大小,3 个 4 字节,2 个 2 字节,所以一共 4 个 4 字节。
所以,02 00 00 00 为 Flags。DC FF FF FF 为 MangledTypeName。DF FF FF FF 为 FieldName,但 DF FF FF FF 只是 FieldName 的偏移信息,所以还需要最后一步计算,计算过程如下:
0x3F28 + 0x4 + 0x4 + FF FF FF DF = 0x100003F0F
// 需要减去虚拟内存的基地址
0x100003F0F - 0x100000000 = 0x3F0F
计算的结果为 3F0F,3F0F 的内存地址在 Mach-O 文件的位置如下:
如图所示, 3F0F 在 Mach-O 文件 reflstr 的位置。 61 67 65 00 是属性 age 的信息,6E 61 6D 65 是属性 name 的信息。
到此,Swift 属性信息在 Mach-O 文件的位置就清楚了。