美文网首页Swift
Swift 中属性介绍

Swift 中属性介绍

作者: 晨曦的简书 | 来源:发表于2022-01-07 10:55 被阅读0次

    一. 存储属性

    存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性(由 var 关键字引入),要么是常量存储属性(由 let 关键字引入)。存储属性这里没有什么特别要强调的,因为随处可⻅。

    class CXTeacher { 
        var age: Int
        var name: String 
    }
    

    从定义上区分 var 与 let

    比如这里的 agename 就是我们所说的存储属性,这里我们需要加以区分的是 letvar 两者的区别,从定义上 let 用来声明常量,常量的值一旦设置好便不能再被更改;var 用来声明变量,变量的值可以在将来设置为不同的值。

    这里我们来看几个案例:

    从汇编的角度分析 var 与 let 的区别

     var age = 20
    
    let x = 10
    
    print(age,x)
    

    这里通过汇编调试可以看到,14 行与 18 行分布代表将 0x14 存入 w8 跟 将 0xa 存入 w8,可以看出在汇编上 varlet 并没有区别。

    通过内存的读取也可以看到 agex 就相差 8 个字节,并没有别的区别。

    从 SIL 的角度分析 var 与 let 的区别

    • swift 代码
    var age = 20
    
    let x = 10
    
    • sil 代码
    // _hasStorage 代表是存储属性,_hasInitialValue 代表都有初始值,{ get set } : 编译器默认会给存储属性生成 get 跟 set 方法,当我们访问存储属性的时候就是访问 get 方法,当我们改变属性的值的时候就是访问 set 方法
    @_hasStorage @_hasInitialValue var age: Int { get set }
    // 当用 let 修饰的时候,编译器没有生成 set 方法,所以修改属性值的时候编译器会报错
    @_hasStorage @_hasInitialValue let x: Int { get }
    

    通过 sil 代码可以看到,varlet 其实也是一种语法糖,let 修饰的属性不会生成 set 方法。

    二. 计算属性

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

    • swift 代码
    struct square {
        // 在实例当中需要占用内存空间
        var width: Double
        var hegith: Double
        
        // 代表将 set 方法私有,只能在结构体内部通过 self.area 修改 area 的值
    //    private(set) var area: Double
        
        var area: Double {
            get {
                return width * hegith
            }
            // newValue 是编译器默认生成的,自己也可以通过 set(自定义名称) {} 修改
            set {
                self.width = newValue
            }
        }
    }
    
    • sil 代码
    struct square {
      @_hasStorage var width: Double { get set }
      @_hasStorage var hegith: Double { get set }
      var area: Double { get set }
      init(width: Double, hegith: Double)
    }
    

    通过 sil 代码可以看到,计算属性的本质就是 setget 方法。

    三. 属性观察者

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

    • swift 代码
    class SubjectName {
        var subjectName: String = "" {
            willSet {
                print("subjectName will set value \(newValue)")
            }
            didSet {
                print("subjectName has been changed \(oldValue)") }
            }
    }
    
    • sil 代码
    // 在赋值之前会调用 willset 方法
       // function_ref SubjectName.subjectName.willset
      %10 = function_ref @$s4main11SubjectNameC07subjectC0SSvw : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> () // user: %11
      %11 = apply %10(%0, %1) : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> ()
    
    // 给 subjectName 属性设置新的值
      retain_value %0 : $String                       // id: %12
      %13 = ref_element_addr %1 : $SubjectName, #SubjectName.subjectName // 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
    
    // 在赋值之后会调用 didset 方法
      // function_ref SubjectName.subjectName.didset
      %19 = function_ref @$s4main11SubjectNameC07subjectC0SSvW : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> () // user: %20
      %20 = apply %19(%6, %1) : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> ()
    

    这里我们在使用属性观察器的时候,需要注意的一点是在初始化期间设置属性时不会调用 willSetdidSet 观察者;只有在为完全初始化的实例分配新值时才会调用它们。运行下面这
    段代码,你会发现当前并不会有任何的输出。

    • swift 代码
    class SubjectName{
        var subjectName: String = "[unnamed]"{
            willSet{
                print("subjectName will set value \(newValue)")
            }
            didSet{
                print("subjectName has been changed \(oldValue)")
            }
        }
        init(subjectName: String) {
            self.subjectName = subjectName
        }
    }
    let s = SubjectName(subjectName: "Swift")
    
    • sil 代码
    // SubjectName.subjectName.getter
    sil hidden [transparent] @$s4main11SubjectNameC07subjectC0SSvg : $@convention(method) (@guaranteed SubjectName) -> @owned String {
    // %0 "self"                                      // users: %2, %1
    bb0(%0 : $SubjectName):
      debug_value %0 : $SubjectName, let, name "self", argno 1 // id: %1
    // 拿到 subjectName 这个属性的内存地址
      %2 = ref_element_addr %0 : $SubjectName, #SubjectName.subjectName // user: %3
      %3 = begin_access [read] [dynamic] %2 : $*String // users: %4, %6
    // 将要赋值的字符串的值直接拷贝到内存地址,并没有调用 setter 方法
      %4 = load %3 : $*String                         // users: %7, %5
      retain_value %4 : $String                       // id: %5
      end_access %3 : $*String                        // id: %6
      return %4 : $String                             // id: %7
    } // end sil function '$s4main11SubjectNameC07subjectC0SSvg'
    

    通过 sil 代码可以看到,这个时候是直接将字符串的值拷贝到 subjectName 属性的内存地址,并没有调用 setter 方法。编译器这样做的原因可能是这个时候有些属性并没有初始化完成,通过 setter 方法赋值可能会造成内存错误。

    上面的属性观察者只是对存储属性起作用,如果我们想对计算属性起作用怎么办?很简单,只需将相关代码添加到属性的 setter。我们先来看这段代码:

    这里可以看到在计算属性中添加 willSetdidSet 会报错,因为这里已经实现 set 方法了,可以直接在 set 中属性赋值前跟属性赋值后进行添加监听。

    如果子类继承于父类的情况下,willSetdidSet 方法调用是什么样的呢?

    这里可以看到当子类继承父类的时候,修改子类实例对象 age 属性的值会先调用子类的 willSet 方法,然后调用父类的 willSet,赋值完成后会先调用父类的 didSet 方法,然后再调用子类的 didSet 方法。

    四. 延迟存储属性

    • 延迟存储属性的初始值在其第一次使用时才进行计算。
    • 用关键字 lazy 来标识一个延迟存储属性。

    通过案例可以看到,在访问 age 属性之前内存中的值是 0,当访问之后才会对内存空间进行初始化。

    • sil 代码
    class CXPerson {
      lazy var age: Int { get set }
    // 这里延时属性本质上是可选类型
      @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
      @objc deinit
      init()
    }
    
     // variable initialization expression of CXPerson.$__lazy_storage_$_age
    sil hidden [transparent] @$s4main8CXPersonC21$__lazy_storage_$_age029_12232F587A4C5CD8B1EEDF696793G2FCLLSiSgvpfi : $@convention(thin) () -> Optional<Int> {
    // Optional.none 相当于 nil,初始化的时候 age 为 0
    bb0:
      %0 = enum $Optional<Int>, #Optional.none!enumelt // user: %1
      return %0 : $Optional<Int>                      // id: %1
    } // end sil function '$s4main8CXPersonC21$__lazy_storage_$_age029_12232F587A4C5CD8B1EEDF696793G2FCLLSiSgvpfi'
    
    // 访问 age 的时候会调用 getter 方法
    // CXPerson.age.getter
    sil hidden [lazy_getter] [noinline] @$s4main8CXPersonC3ageSivg : $@convention(method) (@guaranteed CXPerson) -> Int {
    // %0 "self"                                      // users: %14, %2, %1
    bb0(%0 : $CXPerson):
      debug_value %0 : $CXPerson, let, name "self", argno 1 // id: %1
      %2 = ref_element_addr %0 : $CXPerson, #CXPerson.$__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
    // 这里会进行枚举判断,当age有值的时候走 bb1,没值的时候走 bb2
      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
    
    // 构建一个 Int 类型的值并存储到 age 属性的内存地址
    bb2:                                              // Preds: bb0
      %10 = integer_literal $Builtin.Int64, 6         // 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 : $CXPerson, #CXPerson.$__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
    

    这里延迟属性也可以理解为懒加载,我们使用 lazy 修饰属性的时候可以帮我们节省内存空间。但是延迟属性不能保证变量只能被访问一次,因为会涉及到多个线程同时访问的情况,所以并不是线程安全的。

    内存独占

    • swift 代码
    class CXPerson {
        lazy var age: Int = 6
    //    lazy var age: Int {
    //        return 6
    //    }()
    }
    
    var p = CXPerson()
    //
    let t = p.age
    p.age = 30
    
    • sil 代码
    // CXPerson.age.setter
    sil hidden @$s4main8CXPersonC3ageSivs : $@convention(method) (Int, @guaranteed CXPerson) -> () {
    // %0 "value"                                     // users: %4, %2
    // %1 "self"                                      // users: %5, %3
    bb0(%0 : $Int, %1 : $CXPerson):
      debug_value %0 : $Int, let, name "value", argno 1 // id: %2
      debug_value %1 : $CXPerson, let, name "self", argno 2 // id: %3
      %4 = enum $Optional<Int>, #Optional.some!enumelt, %0 : $Int // user: %7
      %5 = ref_element_addr %1 : $CXPerson, #CXPerson.$__lazy_storage_$_age // user: %6
      %6 = begin_access [modify] [dynamic] %5 : $*Optional<Int> // users: %7, %8
      store %4 to %6 : $*Optional<Int>                // id: %7
      end_access %6 : $*Optional<Int>                 // id: %8
      %9 = tuple ()                                   // user: %10
      return %9 : $()                                 // id: %10
    } 
    

    在我们修改 age 属性的前后会调用 begin_accessend_access,保证在赋值的过程中独占内存,也是为了内存访问安全。

    五. 类型属性

    • 类型属性其实就是一个全局变量
    • 类型属性只会被初始化一次

    下面我们通过将以下 swift 代码转成 sil 代码来看一下。

    • swift 代码
    class CXPerson {
        static var age: Int = 18
    }
    
    CXPerson.age = 30
    
    • sil 代码、IR 代码、源码

    下面我们通过阅读 sil 代码、IR 代码及源码,看一下类型属性的底层实现步骤。

    1. CXPerson 声明
    class CXPerson {
      @_hasStorage @_hasInitialValue static var age: Int { get set }
      @objc deinit
      init()
    }
    // one-time initialization token for age
    sil_global private @$s4main8CXPersonC3age_Wz : $Builtin.Word
    // static CXPerson.age
    sil_global hidden @$s4main8CXPersonC3ageSivpZ : $Int
    

    CXPerson 类的声明中 age 还是一个存储属性,只是多了 static 修饰。而且 age 会被声明成一个全局变量。

    1. age 属性的访问
      // function_ref CXPerson.age.unsafeMutableAddressor
      %3 = function_ref @$s4main8CXPersonC3ageSivau : $@convention(thin) () -> Builtin.RawPointer // user: %4
      %4 = apply %3() : $@convention(thin) () -> Builtin.RawPointer // user: %5
    

    这里注释说明可以看出这里是通过 CXPerson.age 的内存地址进行访问,所以我们搜索 s4main8CXPersonC3ageSivau

    // CXPerson.age.unsafeMutableAddressor
    sil hidden [global_init] @$s4main8CXPersonC3ageSivau : $@convention(thin) () -> Builtin.RawPointer {
    bb0:
    // 通过步骤 1 中声明的 token,拿到 age 属性的内存地址,并把地址赋值给 %1
      %0 = global_addr @$s4main8CXPersonC3age_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 调用 initialization 函数,并把函数地址赋值给 %2
      %2 = function_ref @$s4main8CXPersonC3age_WZ : $@convention(c) () -> () // user: %3
     // 在这里传入 %1 %2 作为参数
      %3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
     //拿到全局变量的内存地址
      %4 = global_addr @$s4main8CXPersonC3ageSivpZ : $*Int // user: %5
     //将全局变量的内存地址转为原生指针
      %5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
     // 这里返回上面转换得到的原生指针
      return %5 : $Builtin.RawPointer                 // id: %6
    } // end sil function '$s4main8CXPersonC3ageSivau'
    

    通过 sil 代码可以看到,在 CXPerson 类的声明中 age 还是一个存储属性,只是多了 static 修饰。而且 age 会被声明成一个全局变量。

    1. 找到 initialization 函数
      xcrun swift-demangle 命令执行结果:
    xcrun swift-demangle s4main8CXPersonC3ageSivpZ
    $s4main8CXPersonC3ageSivpZ ---> static main.CXPerson.age : Swift.Int
    
    // one-time initialization function for age
    sil private [global_init_once_fn] @$s4main8CXPersonC3age_WZ : $@convention(c) () -> () {
    bb0:
    // 创建全局变量 age 
      alloc_global @$s4main8CXPersonC3ageSivpZ        // id: %0
    // 获取到全局变量内存地址
      %1 = global_addr @$s4main8CXPersonC3ageSivpZ : $*Int // user: %4
    // 构建 Int 类型的结构体
      %2 = integer_literal $Builtin.Int64, 18         // user: %3
      %3 = struct $Int (%2 : $Builtin.Int64)          // user: %4
    // 初始化 age 变量 
      store %3 to %1 : $*Int                          // id: %4
      %5 = tuple ()                                   // user: %6
      return %5 : $()                                 // id: %6
    } // end sil function '$s4main8CXPersonC3age_WZ'
    
    1. swift 代码转为 IR 代码并找到 once
    define hidden swiftcc i8* @"$s4main8CXPersonC3ageSivau"() #0 {
    entry:
      %0 = load i64, i64* @"$s4main8CXPersonC3age_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* @"$s4main8CXPersonC3age_Wz", align 8
      %4 = icmp eq i64 %3, -1
      call void @llvm.assume(i1 %4)
      ret i8* bitcast (%TSi* @"$s4main8CXPersonC3ageSivpZ" to i8*)
    
    once_not_done:                                    ; preds = %entry
    //这里可以看到会调用 swift_once 函数
      call void @swift_once(i64* @"$s4main8CXPersonC3age_Wz", i8* bitcast (void ()* @"$s4main8CXPersonC3age_WZ" to i8*), i8* undef)
      br label %once_done
    }
    

    通过 xcrun swift-demangle 命令可以看到 s4main8CXPersonC3ageSivau 就是 sil 代码中的 unsafeMutableAddressor

    xcrun swift-demangle s4main8CXPersonC3ageSivau
    $s4main8CXPersonC3ageSivau ---> main.CXPerson.age.unsafeMutableAddressor : Swift.Int
    
    1. 在源码中搜索 swift_once 函数。

    通过源码可以看到,全局变量本质上还是使用了 GCDdispatch_once_f,确保类型属性只会被初始化一次,但是可以在外部修改类型属性的值。

    swift 中单例写法

    final class CXPerson {
        static let sharedInstance = CXPerson()
        
        private init() {} 
    }
    

    类型方法

    class CXPerson {
        static func staticFunc() {
            print("staticFunc")
        }
    
        class func classFunc() {
            print("staticFunc")
        }
    }
    
    CXPerson.staticFunc()
    CXPerson.classFunc()
    

    以上在方法前用 static 修饰是类型方法的写法,通过汇编可以看到 static 修饰的方法跟 class 修饰的方法调度的时候都是通过静态派发的方式。

    sil_vtable CXPerson {
      #CXPerson.classFunc: (CXPerson.Type) -> () -> () : @$s4main8CXPersonC9classFuncyyFZ   // static CXPerson.classFunc()
      #CXPerson.init!allocator: (CXPerson.Type) -> () -> CXPerson : @$s4main8CXPersonCACycfC    // CXPerson.__allocating_init()
      #CXPerson.deinit!deallocator: @$s4main8CXPersonCfD    // CXPerson.__deallocating_deinit
    }
    

    将以上代码转成 sil 代码可以看到,class 修饰的方法会被注册到 vtable 中,这也是 class 修饰的方法能被子类重写的原因。

    如图可以看到 static 修饰的方法在子类中重写会报错。

    这里可以看到 class 不能用来修饰 struct(值)类型。

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

    Swift 中类与结构体(一)中我们讲到了 Metadata 的元数据结构,我们回顾一下

    struct Metadata{
        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
    }
    

    Swift 中类与结构体(二)中讲到方法调度的过程中我们认识了 typeDescriptor,这里面记录了 V-Table 的相关信息,接下来我们需要认识一下 typeDescriptor 中的 fieldDescripto

    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
    }
    

    fieldDescriptor 记录了当前的属性信息,其中 fieldDescriptor 在源码中的结构如下:

    struct FieldDescriptor {
        var MangledTypeName: Int32
        var Superclass: Int32
        var Kind: uint16
        var FieldRecordSize: uint16
        var NumFields: UInt32
        var FieldRecords: [FieldRecord]
    }
    

    其中 NumFields 代表当前有多少个属性,FieldRecords 记录了每个属性的信息,FieldRecord 的结构体如下:

    struct FieldRecord{
        var Flags: uint32
        var MangledTypeName: Int32
        var FieldName: Int32
    }
    

    基于以上认知,下面我们来看一下相关属性在 MachO 中对应的信息。

    class CXPerson {
        var age = 10
        var age1 = 20
    }
    

    将以上代码编译后的可执行文件用 MachOView 工具打开。

    这里 0xFFFFFE78 + 0x00003EFC - 0x100000000 = 3D74 就是 CXPersontypeDescriptorMachO 文件中的位置。

    _TEXT, _const 文件中找到 3D74,向后偏移 4 个字节之后就是 fieldDescriptorMachO 中的信息,所以 0x00003D84 + 0x00000150 = 0x3ED4 就是 typeDescriptorfieldDescriptor 属性在 MachO 文件中的位置。

    _TEXT,__swift5_fieldmd 文件中可以看到 FieldDescriptor 的结构体的首地址就是 0x3ED4,所以 0x3ED4 向后偏移 4 个字节之后的连续存储空间存储的就是 FieldRecords 属性的信息。所以 0x3EEC + FFFFFFDD - 0x100000000 = 0x3EC9 就是 FieldNameMachO 中的信息。

    FieldName 代表属性的名称,如上图所示,0x3EC9 位置确实存储的就是 CXPerson 类的属性名称,0x00656761 就是 age 的 16 进制,0x31656761 就是 age1 的 16 进制。

    相关文章

      网友评论

        本文标题:Swift 中属性介绍

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