美文网首页Swift探索
Swift探索(三): 属性

Swift探索(三): 属性

作者: Lee_Toto | 来源:发表于2022-01-09 00:44 被阅读0次

    一. 存储属性

    1. 存储属性定义

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

    var age = 18
    let name = "小明"
    
    • var 用来声明变量,变量的值可以在将来设置为不同的值。
    • let 用来声明常量,常量的值一旦设置好便不能再被更改

    在创建类或结构体的实例时,必须为所有的存储属性设置一个初始值。可以在初始化器( init )里为存储属性设置一个初始值,可以分配一个默认的属性值作为定义的一部分。

    struct PersonStruct {
        var age: Int = 10
        let name: String = "小明"
    }
    
    class PersonClass {
        var age: Int
        let name: String
    
        init(_ age: Int, _ name: String) {
            self.age = age
            self.name = name
        }
    }
    
    let person = PersonStruct()
    let person1 = PersonClass(18, "小明")
    

    2. 从SIL查看let和var的区别

    var age = 18
    let name = "小明"
    

    生成sil文件后可以看到如下代码

    @_hasStorage @_hasInitialValue var age: Int { get set }
    
    @_hasStorage @_hasInitialValue let name: String { get }
    

    通过 sil 我们可以发现

    • var 修饰的属性有 getset 方法
    • let 修饰的属性只有 get 方法,所有 let 修饰的属性不能修改。

    二. 计算属性

    1. 计算属性定义

    计算属性并不存储值,他们提供 gettersetter 来修改和获取值。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量。于此同时我们书写计算属性时候必须包含类型,因为编译器需要知道期望返回值是什么

    struct square {
        var width: Double
        var area: Double {
            get {
                width * width
    //            return width * width
            }
            set {
                self.width = newValue
            }
    //        set (newArea) {
    //            self.width = newArea
    //        }
        }
    }
    

    重写一个实例的 settergetter 可以用 setget 来修饰,setter 传入新的默认值叫做 newValue,也可以自定义。Swift 中,在拥有返回值的方法里,如果方法内部是单表达式,那么可以直接省略 return

    2. 只读计算属性

    2.1 方式一
    struct square {
        var width: Double = 30.0
        var area: Double {
            get {
                width * width
            }
        }
        let height: Double = 20.0
    }
    
    2.2 方式二
    struct square {
        var width: Double = 30.0
        private(set) var area : Double
        let height: Double = 20.0
    }
    
    2.3 两种方式的区别

    我们来看一看 square 结构体在 sil 中如何声明的:

    // 方式一
    struct square {
      @_hasStorage @_hasInitialValue var width: Double { get set }
      var area: Double { get }
      @_hasStorage @_hasInitialValue let height: Double { get }
      init()
      init(width: Double = 30.0)
    }
    
    // 方式二
    struct square {
      @_hasStorage @_hasInitialValue var width: Double { get set }
      @_hasStorage private(set) var area: Double { get set }
      @_hasStorage @_hasInitialValue let height: Double { get }
      init(width: Double = 30.0, area: Double)
    }
    

    我们可以看到方式二中的 area 实际上是存储属性,只是 set 方法被私有化了。而方式一中的 area没有任何修饰,只有get方法。

    3. 计算属性本质

    我们来查看一下完整代码的sil文件

    struct square {
        var width: Double
        var area: Double {
            get {
                width
            }
            set {
                self.width = newValue
            }
        }
    }
    
    class ViewController: UIViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            var s = square(width: 10.0)
            s.width = 20.0
            s.area = 30.0
            let w = s.width
            let a = s.area
        }
    }
    

    通过 swiftc ViewController.swift -emit-silViewController.swift 编译成 sil 文件

    // sil 文件中 square 的声明
    struct square {
      @_hasStorage var width: Double { get set }
      var area: Double { get set }
      init(width: Double)
    }
    

    我们可以看到存储属性 width 和计算属性 area 都跟有 { get set } ,那么就说明他们都是有 settergetter 方法的。我们接着往下看他们具体

    3.1 width 属性的 getter 和 setter 在 sil 的内部实现

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

    可以发现

    • 在调用 widthgetter 方法的时候,可以发现是直接获取 squarewidth 的值进行返回。
    • 在调用 widthsetter 方法的时候,是直接获取到 squarewidth 的地址,对地址所指向的内容进行修改。

    3.2 area 属性的 getter 和 setter 在 sil 的内部实现

    // square.area.getter
    sil hidden @$s4main6squareV4areaSdvg : $@convention(method) (square) -> Double {
    // %0 "self"                                      // users: %2, %1
    bb0(%0 : $square):
      debug_value %0 : $square, let, name "self", argno 1 // id: %1
      %2 = struct_extract %0 : $square, #square.width // user: %3
      return %2 : $Double                             // id: %3
    } // end sil function '$s4main6squareV4areaSdvg'
    
    // square.area.setter
    sil hidden @$s4main6squareV4areaSdvs : $@convention(method) (Double, @inout square) -> () {
    // %0 "newValue"                                  // users: %6, %2
    // %1 "self"                                      // users: %4, %3
    bb0(%0 : $Double, %1 : $*square):
      debug_value %0 : $Double, let, name "newValue", argno 1 // id: %2
      debug_value_addr %1 : $*square, var, name "self", argno 2 // id: %3
      %4 = begin_access [modify] [static] %1 : $*square // users: %7, %5
      %5 = struct_element_addr %4 : $*square, #square.width // user: %6
      store %0 to %5 : $*Double                       // id: %6
      end_access %4 : $*square                        // id: %7
      %8 = tuple ()                                   // user: %9
      return %8 : $()                                 // id: %9
    } // end sil function '$s4main6squareV4areaSdvs'
    

    可以发现跟 width 属性的 gettersetter 完全一样。但是我们发现并没有 area 相关的存储变量。所以其实,计算属性根本不会有存储在实例的成员变量,那也就意味着计算属性不占用内存。

    3.3 在汇编中计算属性的实现

    将上面的代码编译成汇编代码,并在 s.width = 20.0 打上断点

    计算属性的汇编代码.png
    我们可以看到计算属性 area 的赋值和取值的本质就是 settergetter 方法。而存储属性 width 则是在进行一系列的 movstr 操作。

    三. 属性观察者

    1. 属性观察者定义

    属性观察者用来观察属性值的变化, willSet 当属性将被改变调用,即使这个值与原有的值相同,而 didSet 在属性已经改变之后调用。它们的语法类似于 gettersetter

    class Person{
        // 存储属性
        var  name: String = ""{
            willSet {
                print("name will set value \(newValue)")
            }
            didSet {
                print("name did set value \(oldValue)")
            }
        }
    }
    
    let p = Person()
    p.name = "小明"
    
    // 打印结果
    name will set value 小明
    name did set value 
    

    我们通过 sil来看 willSetdidset是怎么被调用的

    // Person.name.setter
    sil hidden @$s4main6PersonC4nameSSvs : $@convention(method) (@owned String, @guaranteed Person) -> () {
    // %0 "value"                                     // users: %22, %16, %12, %11, %2
    // %1 "self"                                      // users: %20, %13, %11, %4, %3
    bb0(%0 : $String, %1 : $Person):
      debug_value %0 : $String, let, name "value", argno 1 // id: %2
      debug_value %1 : $Person, let, name "self", argno 2 // id: %3
      %4 = ref_element_addr %1 : $Person, #Person.name // user: %5
      %5 = begin_access [read] [dynamic] %4 : $*String // users: %6, %8
      %6 = load %5 : $*String                         // users: %21, %9, %20, %7
      retain_value %6 : $String                       // id: %7
      end_access %5 : $*String                        // id: %8
      debug_value %6 : $String, let, name "tmp"       // id: %9
      // function_ref Person.name.willset
      %10 = function_ref @$s4main6PersonC4nameSSvw : $@convention(method) (@guaranteed String, @guaranteed Person) -> () // user: %11
      %11 = apply %10(%0, %1) : $@convention(method) (@guaranteed String, @guaranteed Person) -> ()
      retain_value %0 : $String                       // id: %12
      %13 = ref_element_addr %1 : $Person, #Person.name // user: %14
      %14 = begin_access [modify] [dynamic] %13 : $*String // users: %16, %15, %18
      %15 = load %14 : $*String                       // user: %17
      store %0 to %14 : $*String                      // id: %16
      release_value %15 : $String                     // id: %17
      end_access %14 : $*String                       // id: %18
      // function_ref Person.name.didset
      %19 = function_ref @$s4main6PersonC4nameSSvW : $@convention(method) (@guaranteed String, @guaranteed Person) -> () // user: %20
      %20 = apply %19(%6, %1) : $@convention(method) (@guaranteed String, @guaranteed Person) -> ()
      release_value %6 : $String                      // id: %21
      release_value %0 : $String                      // id: %22
      %23 = tuple ()                                  // user: %24
      return %23 : $()                                // id: %24
    } // end sil function '$s4main6PersonC4nameSSvs'
    

    我们可以看到在 Person.namesetter 方法中在赋值操作之前调用了 willSet 方法,然后进行赋值操作,最后再调用 didSet 方法。

    2. 初始化期间设置属性时不会调用观察者

    在初始化期间设置属性时不会调用 willSetdidSet 观察者,只有在为完全初始化的实例分配新值时才会调用它们。

    class Person{
        var  name: String = "unnamed"{
            willSet {
                print("name will set value \(newValue)")
            }
            didSet {
                print("name did set value \(oldValue)")
            }
        }
        
        init(name: String) {
            self.name = name
        }
    }
    
    let p = Person.init(name: "小明")
    
    

    运行上述代码,可以发现没有任何打印结果

    初始化方法.png
    可以发现在初始化方法里是拿到 Person.name 属性的内存地址,然后将字符串直接拷贝到 Person.name 属性的内存地址中,并没有调用 getset 方法,是因为 Person.name 还没有初始化完成。

    3. 观察者对计算属性的观察

    上面的属性观察者只是对存储属性起作用,如果我们想对计算属性起作用怎么办?

    class Square {
        var width: Double
        var area: Double {
            get {
                return width * width
            }
            set {
                print("do something \(newValue)")
                self.width = sqrt(newValue)
                print("do something \(oldValue)")
            }
        }
        init(width: Double) {
            self.width = width
        }
    }
    

    如果我们想对计算属性设置值之前对他进行操作,只需将相关代码添加到属性的 setter 方法中。对于计算属性我们没有必要给他添加 willSetdidSet,只会画蛇添足。

    4. 在继承中的属性观察者

    class Person {
        var age: Int {
            willSet {
                print("age will set value \(newValue)")
            }
            didSet {
                print("age did set value \(oldValue)")
            }
        }
        var height: Double
        
        init(_ age: Int, _ height: Double) {
            self.age = age
            self.height = height
        }
    }
    
    class subPerson :Person {
        override var age: Int {
            willSet {
                print("override age will set value \(newValue)")
            }
            didSet {
                print("override age did set value \(oldValue)")
            }
        }
        
        var name: String
        init(name: String) {
            self.name = name
            super.init(18, 185.0)
            self.age = 20
        }
    }
    
    let p = subPerson.init(name: "小明")
    
    // 打印结果
    override age will set value 20
    age will set value 20
    age did set value 18
    override age did set value 18
    

    我们可以看出对于继承关系的属性他的调用顺序是先调用子类的 willSet,再调用父类的 willSet,再进行赋值操作,再调用父类的 didSet,最后调用子类的 didSet。值得注意的是打印结果是在执行 self.age = 20 这段代码的时候打印的,因为上面提到过初始化期间设置属性时不会调用观察者,因此这里只会打印一次。

    subPerson.age的setter方法的sil文件.png
    通过 sil 文件我们也可以看出 subPerson.agesetter 方法中的调用顺序是先调用 subPerson.agewillSet 方法,在调用 Person.agesetter 方法,在调用 subPerson.agedidSet 方法。

    四. 延迟存储属性

    1. 延迟存储属性定义

    延时加载存储属性是指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用 lazy 来标示一个延时加载存储属性。

    class Person {
        lazy var age: Int = 18
    }
    
    var p = Person()
    
    print(p.age)
    
    print("end")
    

    我们通过观察 p 的内存来观察延迟存储属性 age

    p的内存.png
    这个时候,可以看到 p.age 还没有调用时在 metadata + refcount 后面的内存空间是没有值的,接着我们在 print("end") 打个断点,让 age 访问到,并打印p 的内存
    p的内存2.png
    我们可以看到当 age 第一次被访问的时候这个内存空间有了值。

    2. 延迟存储属性在sil中的实现

    为了简化 sil 文件,将上述代码替换成

    class Person {
        lazy var age: Int = 18
    }
    
    var p = Person()
    
    var t = p.age
    
    class Person {
      lazy var age: Int { get set }
      @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
      @objc deinit
      init()
    }
    

    通过在 sil 文件中 age 的声明我们可以看出延迟存储属性是可选值,因此在 age 还没有加载时为空,在第一次加载后有值。

    // variable initialization expression of Person.$__lazy_storage_$_age
    sil hidden [transparent] @$s4main6PersonC21$__lazy_storage_$_age029_12232F587A4C5CD8B1EEDF696793G2FCLLSiSgvpfi : $@convention(thin) () -> Optional<Int> {
    bb0:
      %0 = enum $Optional<Int>, #Optional.none!enumelt // user: %1
      return %0 : $Optional<Int>                      // id: %1
    } // end sil function '$s4main6PersonC21$__lazy_storage_$_age029_12232F587A4C5CD8B1EEDF696793G2FCLLSiSgvpfi'
    

    $__lazy_storage_$_age 初始化表达式中可以看到默认的空值是 #Optional.none ,就是一个空值。

    // Person.age.getter
    sil hidden [lazy_getter] [noinline] @$s4main6PersonC3ageSivg : $@convention(method) (@guaranteed Person) -> Int {
    // %0 "self"                                      // users: %14, %2, %1
    bb0(%0 : $Person):
      debug_value %0 : $Person, let, name "self", argno 1 // id: %1
      %2 = ref_element_addr %0 : $Person, #Person.$__lazy_storage_$_age // user: %3
      %3 = begin_access [read] [dynamic] %2 : $*Optional<Int> // users: %4, %5
      %4 = load %3 : $*Optional<Int>                  // user: %6
      end_access %3 : $*Optional<Int>                 // id: %5
      switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6
    
    // %7                                             // users: %9, %8
    bb1(%7 : $Int):                                   // Preds: bb0
      debug_value %7 : $Int, let, name "tmp1"         // id: %8
      br bb3(%7 : $Int)                               // id: %9
    
    bb2:                                              // Preds: bb0
      %10 = integer_literal $Builtin.Int64, 18        // user: %11
      %11 = struct $Int (%10 : $Builtin.Int64)        // users: %18, %13, %12
      debug_value %11 : $Int, let, name "tmp2"        // id: %12
      %13 = enum $Optional<Int>, #Optional.some!enumelt, %11 : $Int // user: %16
      %14 = ref_element_addr %0 : $Person, #Person.$__lazy_storage_$_age // user: %15
      %15 = begin_access [modify] [dynamic] %14 : $*Optional<Int> // users: %16, %17
      store %13 to %15 : $*Optional<Int>              // id: %16
      end_access %15 : $*Optional<Int>                // id: %17
      br bb3(%11 : $Int)                              // id: %18
    
    // %19                                            // user: %20
    bb3(%19 : $Int):                                  // Preds: bb2 bb1
      return %19 : $Int                               // id: %20
    } // end sil function '$s4main6PersonC3ageSivg'
    

    agegetter 方法中可以看到,首先执行 bb0代码块,在 bb0 中首先获取到 #Person.$__lazy_storage_$_age 的地址,并把内存地址的值读取到寄存器 %4,然后对 %4 进行枚举模式匹配,如果有值就走 bb1 的代码块,没有值就走 bb2 代码块。bb1中的具体操作就是直接返回原有的值。bb2中的具体的操作就是将 18 存储到 #Person.$__lazy_storage_$_age 的内存地址当中,并返回这个值。

    注意
    如果一个被标记为 lazy 的属性在没有初始化时就同时被多个线程访问,则无法保证该属性只会被初始化一次。

    五. 类型属性

    1. 类型属性的定义

    实例属性属于一个特定类型的实例,每创建一个实例,实例都拥有属于自己的一套属性值,实例之间的属性相互独立。你也可以为类型本身定义属性,无论创建了多少个该类型的实例,这些属性都只有唯一一份。这种属性就是类型属性。
    类型属性其实就是一个全局变量,并且只会被初始化一次。

    class Person {
        // 多线程访问时也只会被初始化一次
        static var age: Int = 18
    }
    
    Person.age = 20
    

    2. 类型属性的本质

    我们通过 swiftc main.swift -emit-sil 命令将 main.swift 编译成 sil 文件来查看

    class Person {
      @_hasStorage @_hasInitialValue static var age: Int { get set }
      @objc deinit
      init()
    }
    
    // one-time initialization token for age
    sil_global private @$s4main6PersonC3age_Wz : $Builtin.Word
    
    // static Person.age
    sil_global hidden @$s4main6PersonC3ageSivpZ : $Int
    

    我们可以看到 age 属性的声明只是多了一个 staic 关键字。 在这下方我们可以看到多了一个全局变量 s4main6PersonC3age_Wz,并且 Person.age 属性也变成了一个全局变量。

    // main
    sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
    bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
      %2 = metatype $@thick Person.Type
      // function_ref Person.age.unsafeMutableAddressor
      %3 = function_ref @$s4main6PersonC3ageSivau : $@convention(thin) () -> Builtin.RawPointer // user: %4
      %4 = apply %3() : $@convention(thin) () -> Builtin.RawPointer // user: %5
      %5 = pointer_to_address %4 : $Builtin.RawPointer to [strict] $*Int // user: %8
      %6 = integer_literal $Builtin.Int64, 20         // user: %7
      %7 = struct $Int (%6 : $Builtin.Int64)          // user: %9
      %8 = begin_access [modify] [dynamic] %5 : $*Int // users: %9, %10
      store %7 to %8 : $*Int                          // id: %9
      end_access %8 : $*Int                           // id: %10
      %11 = integer_literal $Builtin.Int32, 0         // user: %12
      %12 = struct $Int32 (%11 : $Builtin.Int32)      // user: %13
      return %12 : $Int32                             // id: %13
    } // end sil function 'main'
    

    在初始化的时候我们可以看到调用了一个函数 Person.age.unsafeMutableAddressor 就是内存地址的访问,接下来我们定位到这个函数

    // Person.age.unsafeMutableAddressor
    sil hidden [global_init] @$s4main6PersonC3ageSivau : $@convention(thin) () -> Builtin.RawPointer {
    bb0:
      %0 = global_addr @$s4main6PersonC3age_Wz : $*Builtin.Word // user: %1
      %1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
      // function_ref one-time initialization function for age
      %2 = function_ref @$s4main6PersonC3age_WZ : $@convention(c) () -> () // user: %3
      %3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
      %4 = global_addr @$s4main6PersonC3ageSivpZ : $*Int // user: %5
      %5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
      return %5 : $Builtin.RawPointer                 // id: %6
    } // end sil function '$s4main6PersonC3ageSivau'
    

    在这个函数中,显示获取到最开始 Person 声明下方多出的全局变量 s4main6PersonC3age_Wz 的地址,然后转换成 Builtin.RawPointer 类型,然后又调用了一个函数 @$s4main6PersonC3age_WZ : $@convention(c) () -> () ,接着执行 builtin "once",最后返回的是全局变量的内存地址。接着定位到 @$s4main6PersonC3age_WZ : $@convention(c) () -> () 这个函数当中。

    // one-time initialization function for age
    sil private [global_init_once_fn] @$s4main6PersonC3age_WZ : $@convention(c) () -> () {
    bb0:
      alloc_global @$s4main6PersonC3ageSivpZ          // id: %0
      %1 = global_addr @$s4main6PersonC3ageSivpZ : $*Int // user: %4
      %2 = integer_literal $Builtin.Int64, 18         // user: %3
      %3 = struct $Int (%2 : $Builtin.Int64)          // user: %4
      store %3 to %1 : $*Int                          // id: %4
      %5 = tuple ()                                   // user: %6
      return %5 : $()                                 // id: %6
    } // end sil function '$s4main6PersonC3age_WZ'
    

    这个函数当中,首先创建全局变量 s4main6PersonC3ageSivpZ 也就是我们的 age,接着获取这个全局变量的内存地址,接着构建 18 并且将 18 存放到全局变量的内存地址中。这个函数也就是在初始化 age 变量。
    在上一步中 builtin "once"是在干什么呢?我们通过 swiftc main.swift -emit-ir 命令将 main.swift 编译成 IR 文件来查看

    define hidden swiftcc i8* @"$s4main6PersonC3ageSivau"() #0 {
    entry:
      %0 = load i64, i64* @"$s4main6PersonC3age_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* @"$s4main6PersonC3age_Wz", align 8
      %4 = icmp eq i64 %3, -1
      call void @llvm.assume(i1 %4)
      ret i8* bitcast (%TSi* @"$s4main6PersonC3ageSivpZ" to i8*)
    
    once_not_done:                                    ; preds = %entry
      call void @swift_once(i64* @"$s4main6PersonC3age_Wz", i8* bitcast (void ()* @"$s4main6PersonC3age_WZ" to i8*), i8* undef)
      br label %once_done
    }
    
    s4main6PersonC3ageSivau.png
    通过 xcrun swift-demangle s4main6PersonC3ageSivau命令我们可以看到 s4main6PersonC3ageSivau 就是在 sil 文件中的 Person.age.unsafeMutableAddressor 函数。那么在这里面我们可以看到有一个 @swift_once,可能就是我们在 sil 中看到的 builtin "once",我们去 Swift源码 中查看 @swift_once 到底干了什么。
    /// 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
    }
    

    Once.cpp文件中我们可以找到这个方法,我们发现这里使用的 GCDdispatch_once。所以类型属性(static修饰的属性)只会被初始化一次。

    3. 单例的写法

    class Person {
        
        static let sharedInstace = Person()
        
        private init(){}
    }
    
    Person.sharedInstace
    

    六. 属性在Mahco文件的位置信息

    在之前的文章Swift探索(一): 类与结构体(上)中我们了解到 Swift对象的内存结构 Metadata 和在Swift探索(二): 类与结构体(下) 方法调度探索当中了解到 typeDescriptor 的结构

    struct MataData {
      var kind: Int
      var superClass: Any.Type
      var cacheData: (Int, Int)
      var data: Int
      var classFlags: Int32
      var instanceAddressPoint: UInt32
      var instanceSize: UInt32
      var instanceAlignmentMask: UInt16
      var reserved: UInt16
      var classSize: UInt32
      var classAddressPoint: UInt32
      var typeDescriptor: UnsafeMutableRawPointer
      var iVarDestroyer: UnsafeRawPointer
    }
    
    struct 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源码我们了解到 Swift对象的属性存储在 fieldDescriptor 当中,并且 fieldDescriptor 的结构体如下

    struct FieldDescriptor {
        var MangledTypeName: Int32  //回写之后的类型名称
        var Superclass: Int32 //  父类
        var Kind: uint16 // 标识
        var FieldRecordSize: uint16 // 记录当前的大小
        var NumFields: uint32 //有多少个属性
        var FieldRecords: [FieldRecord] // 每一个属性的具体细节
    }
    
    struct FieldRecord {
        var Flags: uint32 //标志位
        var MangledTypeName: Int32 // 这个属性的类型信息
        var FieldName: Int32 // 属性的名称
    }
    

    我们通过 MachO 来验证

    1. 验证流程

    class Person {
        var age = 18
        var name = "小明"
    }
    

    前面部分跟在在Swift探索(二): 类与结构体(下) 中查找 v-table 的流程一致,首先Section64(_TEXT,__swift5_types)中存放的就是 Descriptor

    typeDescriptor的地址.png

    因此得到typeDescriptor的地址为:

    0xFFFFFF54 + 0x3F44 = 0x100003E98
    
    虚拟地址的首地址.png
    Load CommandsLC_SEGMENT_64(__PAGEZERO) 中可以看到虚拟地址的首地址和大小,因此上一步得到的地址 0x100003E98 减去虚拟内存的首地址 0x100000000 就是当前typeDescriptor 在虚拟内存中的偏移量( offset )。
    0x100003E98 - 0x100000000 = 0x3E98
    

    定位到 0x3E98

    0x3E98.png

    0x3E98 就是 TargetClassDescriptor 这个结构体类的首地址,后面存储的就是相应成员变量的内容,根据前面对源码的分析我们得到了 TargetClassDescriptor结构体中的 fieldDescriptor 前面有 4Int32 类型,也就是 44 字节,于是我们向后偏移 44 字节

    向后偏移4个4字节.png
    我们可以得到 fieldDescriptorMacho 中的偏移量,于是 fieldDescriptor的地址为
    0x3EA8 + 0x74 = 0x3F1C
    

    定位到 0x3F1C

    0x3F1C.png
    根据前面的分析我们知道 FieldDescriptor结构体中的 FieldRecord 要向后偏移16个字节,并且 FieldRecord 占用 12 个字节,于是可以看出 Section64(__TEXT,__swift5_fieldmd) 内就是存储的 fieldDescriptor ,因此
    fieldDescriptor.png
    那么 fieldRecords1FieldName 的地址和fieldRecords2FieldName 的地址就分别是
    // fieldRecords1的fieldName
    0x3F2C + 4 + 4 + 0xFFFFFFDF =  0x100003F13
    // 减去虚拟内存地址
    0x100003F13  - 0x100000000 =  0x3F13
    
    // fieldRecords2的fieldName
    0x3F3C  + 4 + 0xFFFFFFD7 =  0x100003F17
    // 减去虚拟内存地址
    0x100003F17  - 0x100000000 =  0x3F17
    

    定位到 0x3F130x3F17

    0x3F13和0x3F17.png

    可以看到后面的 Value 就是 agename
    通过两篇文章的 MachO 分析我们大致的了解到

    • Section64(__TEXT,__swift5_types) : 存储的 typeDescriptor
    • Section64(__TEXT,__swift5_fieldmd) : 存储的 fieldDescriptor
    • Section64(__TEXT,__swift5_reflstr) : 存储所有的属性名称

    相关文章

      网友评论

        本文标题:Swift探索(三): 属性

        本文链接:https://www.haomeiwen.com/subject/taxlcrtx.html