iOS-Swift 独孤九剑:三、属性

1,030 阅读15分钟

在 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 可以用 setget 来修饰,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 处,汇编代码如下:

计算属性汇编实现.png

通过汇编观察,发现,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
}

lazy 的注意点.png

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,都是为了监听某个属性值的改变,并且都可以监听到 newValueoldValue

  • willSet 会传递新值,默认叫 newValuedidSet 会传递旧值,默认叫 oldValue

  • 在初始化器中设置属性值不会触发 willSetdidSet。在属性定义时设置初始值也不会触发 willSetdidSet

代码如下:

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'

如果加了 willSetdidSet,底层的 sil 代码会在属性的 setter 中,调用 willsetdidset 方法。这两个方法拥有两个参数,第一个参数对应的应该是 newValueoldValue

我们再来看一下 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 中有一个 metadatametadata 中有一个 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源码结构.png

我将其整理出来,那么 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();
    }
};

它有三个成员变量:FlagsMangledTypeNameFieldNameFlags 不用管,MangledTypeName 为属性的类型信息,FieldName 为属性的名称信息。

到这里,我们基本可以确定属性存储在底层源码的结构以及位置了。我们的查找流程如下:

  • 找到 HeapObject
  • HeapObject 中找到 HeapMetadata
  • 继续跟进,HeapMetadataTargetHeapMetadata 的别名。
  • 找到 TargetHeapMetadata 结构体,并且找到了 Fields 成员变量。
  • 通过 Fields 找到了 FieldDescriptor 的定义,并且找到其中的 FieldRecords
  • 通过迭代器 FieldRecordIterator 找到了 FieldRecord

2. Mach-O 文件查找属性信息的流程

在了解属性信息在源码中的结构及位置之后,我这个时候需要在 Mach-O 中找到属性相关的东西。新建一个项目,注意,命名不能有中文,添加以下代码,进行编译:

class SHPerson {
    var age = 18
    var name = "Coder_张三"
}

MachOView 将项目的可执行文件打开。

可执行文件.png

types.png

找到 DescriptorMach-O 文件的内存地址。相加之后得到的内存地址,还需要减掉虚拟内存的基地址,虚拟内存的基地址为 0x100000000,计算过程如下:

0x3F40 + 0xFFFFFF54 = 0x100003E94
0x100003E94 - 0x100000000 = 0x3E94

得到的结果 3E943E94 的内存地址在 Mach-O 文件的位置如下:

const.png

如图所示, 3E94Mach-O 文件 const 的位置,50 00 00 80 就是 3E94 内存地址的开始,我们要找到 fieldDescriptor ,需要偏移 fieldDescriptor 前面的四个成员变量的大小,也就是 4 个 4 字节。

偏移到 74 00 00 0074 00 00 00 其实是 fieldDescriptor 的偏移信息,所以要找到 fieldDescriptorMach-O 文件当中的具体位置,还需要加上 74 00 00 00 ,计算过程如下:

0x3EA0 + 0x4 + 0x74 = 0x3F18

计算的结果为 3F183F18 内存地址在 Mach-O 文件的位置如下: fieldmd.png

如图所示, 3F18Mach-O 文件 fieldmd 的位置。

3F18FieldDescriptor 结构的首地址,要找到 FieldRecords 的具体位置,需要偏移 FieldRecords 之前的成员变量的大小,3 个 4 字节,2 个 2 字节,所以一共 4 个 4 字节。

所以,02 00 00 00FlagsDC FF FF FFMangledTypeNameDF FF FF FFFieldName,但 DF FF FF FF 只是 FieldName 的偏移信息,所以还需要最后一步计算,计算过程如下:

0x3F28 + 0x4 + 0x4 + FF FF FF DF = 0x100003F0F
// 需要减去虚拟内存的基地址
0x100003F0F - 0x100000000 = 0x3F0F

计算的结果为 3F0F3F0F 的内存地址在 Mach-O 文件的位置如下:

reflstr.png

如图所示, 3F0FMach-O 文件 reflstr 的位置。 61 67 65 00 是属性 age 的信息,6E 61 6D 65 是属性 name 的信息。

到此,Swift 属性信息在 Mach-O 文件的位置就清楚了。