Swift类与结构体--属性底层

443 阅读15分钟

swift中关于属性,可分为两类,存储属性和计算属性。

一、存储属性(Stored Property)

存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性 (由 var 关键字引入)要么是常量存储属性(由 let 关键字引入)。

关于存储属性,如果该属性不是Optional类型,在Swift实例初始化时,必须为所有的存储属性初始化一个初始值或者,Optional类型,叫可选类型,是swift特有的概念,用于给可能没有值的对象一个可能有的值。

struct WYWPoint {
    var x: Double = 10.0
    let y: Int = 20
    var z: String?
}

class WYWPerson {
    var age: Int
    let name: String
    var address: String?

    init(_ age: Int, name: String, address: String? = nil) {
        self.age = age
        self.name = name
        self.address = address
    }
}

let point = WYWPoint()
let person = WYWPerson(18, name: "Coder_张三")

结构体、类可以定义存储属性,但枚举不可以定义存储属性。

image.png

二、计算属性(Computed Property)

除了存储属性,类、结构体和枚举也能够定义计算属性,计算属性并不存储值,他们提供 getter 和 setter 来修改和获取值。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量。因为编译器需要知道计算属性返回值的类型,因此定义计算属性时候必须包含类型。

struct WYWRectangle {
    var side: Double
    var perimeter: Double {
        set {
            side = newValue * 0.25
        }
        get {
            side * 4
        }
    }
}

1.只读计算属性

只实现get{} 就是只读计算属性。

struct WYWRectangle {
    var side: Double
    var perimeter: Double {
        get {
            side * 4
        }
    }
}

或者设置set私有化也可以达到相同效果

struct WYWRectangle {
    var radius: Double
    private (set) var perimeter: Double

    init(_ radius: Double) {
        self.radius = 10
        diameter = radius * 2
    }
}

那这两种只读计算有什么区别呢?前者不管是内部访问还是外部访问,都不能进行赋值。而后者是内部可以进行赋值,外部不能进行赋值

2.计算属性的本质

那么计算属性和存储属性的本质有什么区别呢?我们可以查看  .sil 文件中的代码是如何实现的。

ViewController.swift 文件的代码如下:

import UIKit

struct WYWCircle {
    var radius: Double
    var diameter: Double {
        set {
            radius = newValue / 2
        }
        get {
            radius * 2
        }
    }
}

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        var c = WYWCircle(radius: 10)
        c.radius = 20
        c.diameter = 30
        let _ = c.diameter
    }
}

属性在sil中的区别

计算属性没有@_hasStorage标识,存储属性有

struct WYWCircle {
    @_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 只有 set 和 get 方法呢。

存储属性的get和set在 sil 的内部实现

我们看一下radius属性在sil中的实现

// WYWCircle.radius.get
sil hidden [transparent] @$s14ViewController8WYWCircleWYWCircleV6radiusSdvg : $@convention(method) (WYWCircle) -> Double {
    // %0 "self"                                      // users: %2, %1
    bb0(%0 : $WYWCircle):
    debug_value %0 : $WYWCircle, let, name "self", argno 1 // id: %1
    %2 = struct_extract %0 : $WYWCircle, #WYWCircle.radius // user: %3
    return %2 : $Double                             // id: %3
} // end sil function '$s14ViewController8WYWCircleV6radiusSdvg'
// WYWCircle.radius.set
sil hidden [transparent] @$s14ViewController8WYWCircleV6radiusSdvs : $@convention(method) (Double, @inout WYWCircle) -> () {
    // %0 "value"                                     // users: %6, %2
    // %1 "self"                                      // users: %4, %3
    bb0(%0 : $Double, %1 : $*WYWCircle):
    debug_value %0 : $Double, let, name "value", argno 1 // id: %2
    debug_value_addr %1 : $*WYWCircle, var, name "self", argno 2 // id: %3
    %4 = begin_access [modify] [static] %1 : $*WYWCircle // users: %7, %5
    %5 = struct_element_addr %4 : $*WYWCircle, #WYWCircle.radius // user: %6
    store %0 to %5 : $*Double                       // id: %6
    end_access %4 : $*WYWCircle                      // id: %7
    %8 = tuple ()                                   // user: %9
    return %8 : $()                                 // id: %9
} // end sil function '$s14ViewController8WYWCircleV6radiusSdvs'

可以看到,在调用 radius 的 get 的时候,底层的 sil 拿到 WYWCircle 中 radius 的值直接返回。在调用 radius 的 set 的时候,会取 WYWCircle 实例的地址,对 radius 进行修改。

计算属性的 get 和 set 在 sil 的内部实现

我们看一下diameter属性在sil中的实现

// WYWCircle.diameter.set
sil hidden @$s14ViewController8WYWCircleV8diameterSdvs : $@convention(method) (Double, @inout WYWCircle) -> () {
    // %0 "newValue"                                  // users: %5, %2
    // %1 "self"                                      // users: %8, %3
    bb0(%0 : $Double, %1 : $*WYWCircle):
    debug_value %0 : $Double, let, name "newValue", argno 1 // id: %2
    debug_value_addr %1 : $*WYWCircle, 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 : $*WYWCircle // users: %11, %9
    %9 = struct_element_addr %8 : $*WYWCircle, #WYWCircle.radius // user: %10
    store %7 to %9 : $*Double                       // id: %10
    end_access %8 : $*WYWCircle                      // id: %11
    %12 = tuple ()                                  // user: %13
    return %12 : $()                                // id: %13
} // end sil function '$s14ViewController8WYWCircleV8diameterSdvs'

注意到,diameter 属性 set 的内部实现会自动生成一个名为 newValue 的常量,并且会把外部传进来的值赋值给 newValue

// WYWCircle.diameter.get
sil hidden @$s14ViewController8WYWCircleV8diameterSdvg : $@convention(method) (WYWCircle) -> Double {
    // %0 "self"                                      // users: %2, %1
    bb0(%0 : $WYWCircle):
    debug_value %0 : $WYWCircle, let, name "self", argno 1 // id: %1
    %2 = struct_extract %0 : $WYWCircle, #WYWCircle.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 '$s14ViewController8WYWCircleWYWCircleV8diameterSdvg'

从观察 diameter 属性 的 setter 和 getter 中,并未发现有 diameter 相关的存储变量。所以其实,计算属性根本不会有存储在实例的成员变量,那也就意味着计算属性不占内存

计算属性汇编实现

为了更加直观的感受,我们直接看汇编代码,我们将断点打在 c.diameter = 30 处,debug汇编代码如下:

image.png 通过汇编观察,发现,c.diameter = 30 这句代码的本质就是去调用 setter ,let _ = c.diameter 这句代码的本质就是去调用 getter。

由此可以得出:计算属性的本质就是方法(函数),并且计算属性并不占用实例的内存。

三、延迟存储属性(Lazy Stored Property)- 懒加载

这个Swift开发中经常使用。 lazy使用方式:

  • 使用 lazy 可以定义一个延迟存储属性,在第一次用到属性的时候才会进行初始化。
  • lazy 属性必须是 var,不能是 let,因为 let 必须在实例的初始化方法完成之前就拥有值。
  • 如果多条线程同时第一次访问 lazy 属性,无法保证属性只被初始化 1 次。
// 定义:
class Test {
    init() {
        print("Test init!")
    }
    func call() {
        print("call is running!")
    }
}

class Person {
    lazy var test:Test = {
        return Test()
    }()
    
    init() {
        print("Person init!")
    }

    func goOut() { test.call() }
}

// 调用:
let p = Person()
print("--------")
p.goOut()

//print
Person init! 
Test init! 
call is running!

需要注意的是:

  • 当结构体包含一个延迟存储属性时,只有 var 才能访问延迟存储属性,因为延迟属性初始化时需要改变结构体的内存。

image.png

1.lazy 在底层 sil 的调用实现

SHPerson 的声明定义如下:

class SHPerson {
    lazy var car = SHCar()
    init() {
        print("SHPerson init!")
    }

    func goOut() { car.run() }
}

相应sil

class SHPerson {
    lazy var car: SHCar { get set }
    @_hasStorage @_hasInitialValue final var $__lazy_storage_$_car: SHCar? { get set }
    init()
    func goOut()
    @objc deinit
}

存储属性在添加了 lazy 修饰后,除了拥有存储属性的特性之外@_hasStorage,在底层的 sil 代码还生成了一行代码。我们注意观察,这行代码拥有 final 修饰符,说明 lazy 修饰的属性不能被重写。并且,它是一个可选项。拥有可选项就意味着,其实在初始的时候是有值的@_hasInitialValue,只是这个值是一个 nil。

再来看看car属性的get方法:

// 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 会传递旧值,默认叫 oldValuenewValueoldValue是可以自己命名的。
  • 在初始化器中设置自己定义的属性值不会触发 willSetdidSet。在属性定义时设置初始值也不会触发 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
    }
}

class SubSHPoint: SHPoint {
    var z: Double {
        willSet {
            print("z willSet newValue: \(newValue)")
        }
        didSet {
            print("z didSet oldValue: \(oldValue)")
        }
    }
    
    required init(x: Double, z: Double) {
        self.z = z
        super.init(x: x)
        self.x = x + 1
    }
}

let s = SubSHPoint(x: 33.0, z: 99.0)
s.x = 10
s.z = s.y

///print
x willSet newValue: 34.0 //self.x = x + 1
x didSet oldValue: 33.0  //self.x = x + 1
x willSet newValue: 10.0 //s.x = 10
x didSet oldValue: 34.0  //s.x = 10
z willSet newValue: 20.0 //s.z = s.y
z didSet oldValue: 99.0 //s.z = s.y

2. 属性观察器在继承关系下调用顺序

总结一下调用顺序:子类 willSet -> 父类 willSet -> 父类 didSet -> 子类 didSet

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")

///print
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

3. willSet 和 didSet 底层调用

我们生成 sil 的代码之后,先来看一下 SHPoint 中 x 的 set:

// SHPoint.x.set
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 代码会在属性的 set 中,调用 willsetdidset 方法。这两个方法拥有两个参数,第一个参数对应的应该是 newValueoldValue

我们再来看一下 SHRect 中重写 x 之后的 set 调用情况:

// 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 的 set 之后,会先调用自身的 willset ,然后调用 SHPoint 的 set,最后调用 自身的 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份内存(类似于全局变量)。
  • 存储类型属性默认就是 lazy ,会在第一次使用的时候才初始化,就算被多个线程同时访问,保证只会初始化一次。
  • 不同于存储实例属性,你必须给存储类型属性设定初始值,因为类型没有像实例那样的 init 初始化器来初始化存储属性。
  • 存储类型属性可以是 let
  • 计算类型属性(Computed Type Property)
  • 可以通过 static 定义类型属性。如果是类,也可以用关键字 class。
  • 枚举类型也可以定义类型属性(存储类型属性、计算类型属性)。

类型属性,只能通过类型去访问。 在结构体中定义一个类型属性,可以通过 static 来修饰。

struct SHCar {
    static var count: Int = 0
}

SHCar.count += 1;

enum WHao {
    case w1, w2, w3, w4, w5
    static let defw = WHao.w1
    static var count:Int = 0
    static var value:String {
        get {
            return "wyw"
        }
        set {
            WHao.count = newValue.count
        }
    }
}

注意 class 只能修饰类里面的类型属性, 注意下面代码中 static override,子类如果想重写计算类型属性,只能用class不能用static

class CHao {
    static var staticSX:String {
        set (value) {
            print("--CHao--\(value)")
        }
        get {
            return "staticSX"
        }
    }
    
    class var calssSX:String {
        set (value) {
            print("--CHao--\(value)")
        }
        get {
            return "calssSX"
        }
    }
}

class SubCHao:CHao {
//    static override var staticSX:String {
//        set {
//            print("--SubCHao--\(newValue)")
//        }
//        get {
//            return "wwww"
//        }
//    }
    
    class override var calssSX:String {
        set {
            print("--SubCHao--\(newValue)")
        }
        get {
            return "calssSX-override"
        }
    }
}

3. 单例模式

上面一小节对于存储类型属性说到两点:

  • 存储类型属性(Stored Type Property):整个程序运行过程中,就只有1份内存(类似于全局变量)。

  • 存储类型属性默认就是 lazy ,会在第一次使用的时候才初始化,就算被多个线程同时访问,保证只会初始化一次。 既可以当作全局变量,又是第一次使用的时候才初始化,那么他不就一个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 的单例是如何保证线程安全的呢?请往下看。

OC的写法

+ (SHNetworkManager *)sharedManager {
    static SHNetworkManager* instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[SHNetworkManager alloc] init];
    });
    return instance;
}

3.2 Swift 单例模式的本质

sil 代码单例的声明如下:

@_hasMissingDesignatedInitializers public class SHNetworkManager {
    @_hasStorage @_hasInitialValue public static let shared: SHNetworkManager { get }
    private init()
    @objc deinit
}

接下来看一下 shared 的 get 是如何实现的:

// static SHNetworkManager.shared.get
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'

在 get 中,调用了一个 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;

在当前文件搜索 FieldDescriptor,发现有一段代码:

namespace reflection {
    class FieldDescriptor;
}

这会儿终于看到了,点击去,查看 FieldDescriptor 类的实现,如下:

image.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 为属性的名称信息。

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

  1. 找到 HeapObject
  2. 从 HeapObject 中找到 HeapMetadata
  3. 继续跟进,HeapMetadata 为 TargetHeapMetadata 的别名。
  4. 通过HeapMetadata的还原,和 TargetClassDescriptorTargetTypeContextDescriptorTargetClassDescriptor 的父类。
  5. TargetClassDescriptor 中找到 FieldDescriptor
  6. 搜索FieldDescriptor,发现迭代器FieldRecordIterator就是属性列表信息,FieldRecordIterator 的成员是FieldRecordFieldRecord就是每个属性的信息。

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

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

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

用 MachOView 将项目的可执行文件(小黑框)打开。 image.png 找到 Descriptor 在 Mach-O 文件的内存地址。相加之后得到的内存地址,还需要减掉虚拟内存的基地址,虚拟内存的基地址为 0x100000000,计算过程如下:

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

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

image.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 文件的位置如下:

image.png 如图所示, 3F18 在 Mach-O 文件 fieldmd 的位置。

3F18 为 FieldDescriptor 结构的首地址,要找到 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 文件的位置如下:

image.png

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

到此,Swift 属性信息在 Mach-O 文件的位置就清楚了。和我们源码推断的结构完全一致。

由于自己时间不够,这篇文章基本上是摘抄张三的文章,个人觉得这篇文章写的非常棒,所以不自觉的按照他的思路重新来了一遍。希望大家给他点赞!! 文章原地址:juejin.cn/post/704961…