Swift的属性

作者: spyn_n | 来源:发表于2022-01-10 18:13 被阅读0次

    一、存储属性

      存储属性 是一个类或者值类型(结构体,enum等)实例一部分的常量或变量,其存储属性一般有两种引入方式:

    1. var关键字定义的,叫变量存储属性
    2. let关键字定义的,叫常量存储属性

    那么这两种有什么区别呢?在汇编层面,他们其实没有什么区别,但是在编译时他们主要的区别是:

    • let修饰的常量,其值一旦设置就无法再修改,否则编译不通过
    • var修饰的常量,其值可以随处设置不同的值

    eg:

    sil分析:

    class PSYModel {
      @_hasStorage var age: Int { get set }
       @hasStorage final let name: String { get }
       init(
    age: Int, _ name: String)
       @objc deinit
    }
      
    // PSYModel.age.getter    //age的getter方法
    // PSYModel.age.setter    //age的setter方法
    // PSYModel.name.getter   //name的getter方法

    通过上面可以看出var修饰的变量,具有 存储属性,并且具有gettersetter方法,所以其设置值之后还可被设置新值。而let修饰的变量,被添加了final标识,并且只有getter方法,没有setter方法,所以在被设置初值之后就不能再被设置其值。但是为啥在指定初始化器中怎么能够给它设置值呢?因为在init方法中,是直接操作内存,存储到对应的内存的,并不需要调用setter方法

    二、计算属性

      在swift中存储属性很常见,当然除了存储属性,类、结构体和枚举也能够定义计算属性。计算属性并不存储值,他们提供gettersetter方法来获取和修改值,并且不同于存储属性可以使用varlet关键字修饰,计算属性必须用var修饰,并且定义变量的类型。如下:

    struct square {
      var width: Double
      
      var area: Double{
         get{
           return width * width
         }
        set(newValue){
          self.width = newValue
        }
       }
    }
    var s = square(width: 10.0)
    s.area = 20

    sil文件

    sil文件area具有settergetter方法,但是并没有 hasStoreage标识,也就是没有存储属性。直接打印其结构体的大小也可以发现area并不占用内存。

    如果private (set)修饰一个变量,说明其实具有存储属性的私有变量,只能在类或结构体代码块内可以访问,并且有settergetter方法,只是将setter方法设置为私有,外部只能调用getter方法。

    三、属性观察者

      属性观察者会观察用来观察属性值的变化,当属性值即将改变时调用willSet,当属性值改变之后调用didSet。其方法类似于gettersetter方法,在初始化器内修改其值并不会触发调用willSetdidSet方法,因为初始化器是直接内存的操作。(计算属性不用添加观察者

    初始化器内不会触发 非初始化器赋值触发

    @_hasStorage var subName: String { get set } 也具有存储属性,其赋值的时候是由setter方法触发的,sil文件如下:

    subName的setter方法的sil文件

    如果类存在继承,观察者属性调用又是啥样呢?

    class PSYModel{
        var age: Int
        let name: String
    
        init(_ age: Int, _ name: String, _ subName: String) {
            self.age = age
            self.name = name
            self.subName = subName
        }
        var subName: String {
            willSet{
                print("PSY subName value willSet")
            }
            didSet{
                print("PSY subName value didSet")
            }
        }
    }
    
    class LQRModel: PSYModel {
        var sex: Int
        
        override var subName: String{
            willSet{
                print("LQR subName value willSet")
            }
            didSet{
                print("LQR subName value willSet")
            }
        }
        init(_ sex: Int) {
            self.sex = sex
            super.init(18, "P", "SY")
        }
    }
    
    var psy = LQRModel.init(1)
    psy.subName = "spy"
    print("end")
    
    输出结果:****************************
    LQR subName value willSet
    PSY subName value willSet
    PSY subName value didSet
    LQR subName value willSet
    end
    Program ended with exit code: 0
    
    
    image.png

    四、延迟存储属性

      延迟存储属性,其初始值是在第一次使用的时候才计算,使用关键字lazy来标识一个延迟存储属性。延迟存储属性只能用let修饰,不能用let,且必须有值。

    class PSYModel{
        lazy var age: Int = 1
        let name: String
    
        init(_ name: String) {
            self.name = name
        }
    }
    var psy = PSYModel.init("PSY")
    print("psy.age = \(psy.age)")
    print("end")
    

    lldb打印内存看一下其实例对象在初始化的时候并没有值,在使用的时候才有值:

    lldb内存
    那么它具体是怎么做到,使用的时候才有值呢?它实际是被编译成了一个_age: Int?可选类型的存储属性,并且生成了一个_age初始化表达式,返回了一个枚举。
    sil文件 初始化表达式返回了一个枚举 age的getter方法 bb9是初始化值为6

    可知,延迟存储属性延迟了内存的分配,相当于懒加载一样的效果,但是其并不是线程安全的,因为其并不能保证属性只被访问一次。

    五、类型属性

      类型属性其实是一个全局变量,只会被初始化一次,使用static修饰,eg :

    class PSYModel{
        static var age: Int = 6
    }
    PSYModel.age = 18
    

    将上面的代码生成.sil代码看一下,可以发现其仍具有存储属性,比一般存储属性多了个static修饰符,并且生成了一个全局变量age:

    sil代码
    main函数主要意思:
    1. 引用函数PSYModel.age.unsafeMutableAddressor
    2. 调用函数PSYModel.age.unsafeMutableAddressor并转成RawPointer原生指针
    3. 将原生指针指向Int数据类型
    4. 创建数值为18的数据Int64数据
    5. 将数据存入内存
    main函数

    PSYModel.age.unsafeMutableAddressor到底做了啥?内存是如何分配的?继续解读sil文件PSYModel.age.unsafeMutableAddressor,可以发现其主要做了:

    1. 申请了一个全局的token0@globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0
    2. 拿到globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0申请内存函数指针
    3. 调用了一个“once”函数,参数是上面得到的token0globalinit函数指针
    PSYModel.age.unsafeMutableAddressor

    @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0真正初始化的函数,这个就很简单了,创建全局属性age,拿到内存地址,创建初始值6,将6存储到内存当中并返回。

    @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0

    但是那个“once”是有什么用吗?为什么还有一个token0?为了得到更接近底层汇编的中间代码,在终端使用命令生成ir文件: swiftc main.swift -emit-ir -o main1.c这里为了方便生成的文件查看我制定后缀为.c文件

    找到相同的函数@s4main8PSYModelC3ageSivau--> PSYModel.age.unsafeMutableAddressor,可以发现“once”函数的调用译成了根据标记,如果没有内存分配(once_ont_done)就调用源码swift_once方法,如果已经分配了(once_done)就直接读取内存(load),保证了其只会分配一次内存,只初始化一次。

    image.png

    结合Swift源码 看一下swift_once做了什么?全局搜索swift_once可以找到其,并根据注释可以知道其调用了GCD(dispatch_once_f)保证函数只被执行一次。

    /// 使用给定的上下文参数运行给定函数一次。
    ///predicate参数必须指向一个全局变量或静态变量,其静态范围类型为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
    }
    
    拓展(单例):

     // 单例
    class PSYModel{
      static let shareInstance = PSYModel()
      private init() {}
    } 
    如果不希望单例类被继承,class前面加上final

    六、属性在MachO文件中的位置

      首先我们要有一个共识,根据前两篇文章【Swift语言的类与结构体--2Swift语言的类与结构体--1】我们得到了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
    }

    以及类信息typeDescriptor的结构信息:

    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
    }

    以及在typeDescriptor的结构信息中,fieldDescriptor记录了属性的信息,其结构体如下:

    struct FieldDescriptor{
       var MangledTypeName: UInt32
      var Superclass: UInt32
      var Kind: Int16
      var FieldRecordSize: Int16
      var NumFields: Int32 // 属性个数
      var FieldRecords // [FieldRecords]
    }

    FieldRecords中记录每个属性的信息,其结构体如下:

    struct FieldRecord{
      Flags :         uint32
      MangledTypeName:   int32
      FieldName:       int32
    }

    新建代码,编译成.app文件,使用MachO View打开,手动计算验证是否能找到属性名:

    class PSYModel{
        var age: Int = 18
        let name: String = "psy"
    }
    

    首先找到Section__TEXT,__swift5_types,将pFile + Data - baseAddress(基地址)0xFFFFFF5C + 0x1EE0 - 0x100000000 = 0x1E3C)得到typeDescriptor的偏移

    得到typeDescriptor

    点击Section64__TEXT,__const,找到0x1E3CpFile,向后偏移4个4字节,得到fieldDescriptor,此时也是得到fieldDescriptoroffset信息,需要pFile + Data0x1E4C+ 0x6C = 0x1EB8)才能得到真正的FieldDescriptor

    得到FieldDescriptor

    点击Section64__TEXT,__switf5_fieldmd,找到0x1EB8,向后偏移3个4字节,得到连续的内存空间即是FiledRecords,再根据FiledRecords结构,第1个4字节是Flags,第2个4字节是MangledTypeName,接下来的4字节是FiledName的offset(0xffffffdf

    得到FieldRecords

    pFile + FiledName的offset0x1ED0 + 0xffffffdf = 0x100001EAF),然后在Section64__TEXT,__switf5_reflstr就可找到属性名字如下:同理,0x1EDC + 0xffffffd7 = 0x100001EB3得到另一个属性名name

    得到FiledName

    相关文章

      网友评论

        本文标题:Swift的属性

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