美文网首页Swift
Swift进阶-属性

Swift进阶-属性

作者: 顶级蜗牛 | 来源:发表于2022-01-05 17:44 被阅读0次

    Swift进阶-类与结构体
    Swift-函数派发
    Swift进阶-属性
    Swift进阶-指针
    Swift进阶-内存管理
    Swift进阶-TargetClassMetadata和TargetStructMetadata数据结构源码分析
    Swift进阶-Mirror解析
    Swift进阶-闭包
    Swift进阶-协议
    Swift进阶-泛型
    Swift进阶-String源码解析
    Swift进阶-Array源码解析

    一、存储属性

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

    class Teacher { 
          let age: Int 
          var name: String 
    
          init(_ age: Int, _ name: String) { 
            self.age = age 
            self.name = name 
          }
     }
    
    struct Student { 
          let age: Int 
          var name: String
     }
    

    let 和 var 的区别:

    从定义上:

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

    汇编角度:
    看不出有什么不同的,无非把值赋值给对应的寄存器,大可自行调试看看。
    开启汇编模式:Debug -> Debug Workflow -> Always Show Disassembly

    var age = 18
    let name = 20
    print("断点打在这,运行")
    

    从 SIL的角度:

    swiftc xxx.swift -emit-sil // 生成sil(已优化)
    
    sil的常量变量声明部分

    存储属性在编译的时候,编译器默认会合成get/set方式,而我们访问/赋值 存储属性的时候,实际上就是调用get/set

    let声明的属性默认不会提供setter

    二、计算属性

    类、结构体和枚举也能够定义计算属性,计算属性并不存储值,他们提供 gettersetter 来修改和获取值。

    计算属性必须定义为变量计算属性时候必须包含类型,因为编译器需 要知道期望返回值是什么。

    struct Square {
        var width: Double // 存储属性,在实例中Double占据8字节
        var area: Double{ // 计算属性,不占据内存空间
            get {
                return width * width
            }
            set {
                self.width = newValue
            }
        }
    }
    
    class ViewController: UIViewController{
        override func viewDidLoad() {
            var s = Square(width: 10.0)
            s.area = 30.0 //断点打在着,看汇编
        }
    }
    

    看看汇编给计算属性赋值是个什么过程

    image.png

    可以看到这是一个静态调用,按住 control 点击下一步进去

    image.png

    给计算属性赋值,其实是调用setter

    来看看sil的Square声明

    image.png

    计算属性area的本质就是getter和setter,不一样的,不占用实例的内存。

    以下这种声明是存储属性:

    struct Square {
        var width: Double
        private(set) var area: Double = 10.0
    }
    
    sil

    虽然有set方法,但是超过了类作用域,外界是访问不到的。

    三、属性观察者

    属性观察者会观察用来观察属性值的变化:
    willSet 当属性将被改变调用,即使这个值与 原有的值相同;
    didSet 在属性已经改变之后调用。

    class SubjectName{
        // 它是存储属性
        var subjectName: String = "" {
            willSet{
                print("subjectName will set value \(newValue)")
            }
            didSet{
                print("subjectName has been changed \(oldValue)")
            }
        }
    }
    
    class ViewController: UIViewController{
        override func viewDidLoad() {
            let n = SubjectName()
            n.subjectName = "wj"
        }
    }
    
    sil

    在赋值的前后去调用willSetdidSet方法。

    class SubjectName{
        // 它是存储属性
        var subjectName: String = "" {
            willSet{
                print("subjectName will set value \(newValue)")
            }
            didSet{
                print("subjectName has been changed \(oldValue)")
            }
        }
    
        init(_ subjectName: String) {
            self.subjectName = subjectName
        }
    }
    
    sil

    在初始化期间设置属性时不会调用 willSetdidSet 观察者。

    继承关系demo:

    class Teacher {
        var name: String {
            willSet {
                print("will set newValue: \(newValue)")
            }
            didSet {
                print("did set oldValue \(oldValue)")
            }
        }
        
        init(name: String) {
            self.name = name
        }
    }
    
    class ParTimeTeacher: Teacher {
        override var name: String {
            willSet {
                print("override will set newValue: \(newValue)")
            }
            didSet {
                print("override did set oldValue \(oldValue)")
            }
        }
        
        override init(name: String) {
            super.init(name: name)
        }
    }
    
    class ViewController: UIViewController {
        override func viewDidLoad() {
            let t = ParTimeTeacher(name: "未知")
            t.name = "林老师"
        }
    }
    

    打印结果:

    override will set newValue: 林老师   // 子类will set
    will set newValue: 林老师            // 父类will set
    did set oldValue 未知                // 父类 did set
    override did set oldValue 未知       // 子类did set
    
    sil

    实际上ParTimeTeacher实例调用getter/setter实际就是调用父类Teachergetter/setter

    sil

    四、延迟存储属性

    • 必须有初始值
    • 延迟存储属性的初始值是在它第一次被使用时才进行计算;
    • 用关键字 lazy 来标识一个延迟存储属性。
    image.png

    在没有第一次访问延迟存储属性时,它的值是0

    image.png

    访问过一次后,它才会有值。
    再去看看sil:

    sil

    所以苹果做到懒加载属性第一次访问前没有值,访问后就有值?
    本质就是在编译后生成一个可选型存储属性

    iinitialization

    在初始化的时候给的是 nil

    getter

    拿到生成的属性 $__lazy_storage_$_name的地址,然后去switch:如果有值,走bb1的代码块;如果没有值走bb2代码块

    image.png

    bb2没有加载过:去构建初始值,给到变量$__lazy_storage_$_name
    bb1已加载过:已经有值了,直接给他返回出去

    setter

    没什么好说的,给属性$__lazy_storage_$_name赋值

    五、类型属性

    • 类型属性本质上就是一个全局变量
    • 类型属性只会被初始化一次
    class Teacher {
        static var name: String = "未知"
    }
    
    class ViewController: UIViewController{
        override func viewDidLoad() {
            Teacher.name = "林老师"
        }
    }
    
    sil

    编译后的sil里多了两个全局变量
    一个是@$s14ViewController7TeacherC4name_Wz - token
    一个是@$s14ViewController7TeacherC4nameSSvpZ - Teacher.name

    看看viewDidLoad里面

    viewDidLoad

    做了一个全局变量内存地址的转换,它是怎么转换的呢?
    搜一下这个函数名 @$s14ViewController7TeacherC4nameSSvau

    image.png 初始化Teacher.name

    builtin "once"在sil降级之后,其实底层调用的就是swift_once,而swift_once的源码底层调用的就是GCD的dispatch_once

    swift_once
    单例的写法
    class Teacher {
        static let share = Teacher()
        private init() {}
    }
    
    单例sil

    六、属性在Mach-O文件的位置信息

    上一篇文章介绍了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   // class/struct/enum 的名称
          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  (methods) 
    }
    

    其中 fieldDescriptor 记录了当前的属性信息,经分析后fieldDescriptor 在源码中的数据结构如下:

    struct FieldDescriptor { 
        MangledTypeName int32 // 混写后的类型名称
        Superclass      int32 
        Kind            uint16 
        FieldRecordSize uint16 
        NumFields       uint32 // 属性个数
        FieldRecords [FieldRecord]  // 记录了每个属性的信息
     }
    

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

    struct FieldRecord{ 
        Flags           uint32 // 标志位
        MangledTypeName int32 // 属性的类型名称
        FieldName       int32 // 属性名称
     }
    

    了解完数据结构之后,现在从Mach-O来分析属性在Mach-O文件的位置信息

    main.swift 声明一个对象

    class Teacher {
        var age = 18
        var age1 = 20
    }
    

    编译后,找到其可执行文件exe,将其拖拽到 MachOView 软件中

    __TEXT,__swift5_types

    在data区找到__TEXT,__swift5_types 这是记录所有的 struct/enum/类的Descriptor的信息,现在我们只有一个Teacher类
    可以拿到 Teacher类的DescriptorMach-O 里的位置:

    0xFFFFFE28 + 0x3ED8 = 0x100003D00
    

    所以Dscriptor在mach-o的data区的偏移量,需要再减去虚拟内存地址:

    0x100003D00 - 0x100000000 = 0x3D00
    

    然后再data区找到 __TEXT,__const里边,去找0x3D00的位置:

    __TEXT,__const

    接下来要找到 fieldDescriptor它的地址,因为fieldDescriptorTargetClassDescriptor的数据结构前面还有4个UInt32的成员:

    struct TargetClassDescriptor{ 
          var flags: UInt32 
          var parent: UInt32 
          var name: Int32   // class/struct/enum 的名称
          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  (methods) 
    }
    

    所以还需要向后偏移4个4字节:

    fieldDescriptor在Mach-O的偏移信息

    得到的位置也就是fieldDescriptorMach-O的偏移信息
    求出fieldDescriptorMach-O的的信息:

    0x3D10 + 0x1A0 = 0x3EB0
    

    而属性存放在data区的 __TEXT,__swift5_fieldmd,并在上面找到0x3EB0:

    __TEXT,__swift5_fieldmd

    从0x3EB0开始后面的就是 FieldDescriptor属性描述的成员内容,而我要找到 FieldRecords [FieldRecord]

    struct FieldDescriptor { 
        MangledTypeName int32 // 混写后的类型名称
        Superclass      int32 
        Kind            uint16 
        FieldRecordSize uint16 
        NumFields       uint32 // 属性个数
        FieldRecords [FieldRecord]  // 记录了每个属性的信息
     }
    

    就要往后偏移4个4字节(其中有2个uint16,3个int32):

    FieldRecords

    因为我们只有一个Teacher类,这后面的连续的存储空间就是 FieldRecords数组的内容:

    FieldRecord 的数据结构:

    struct FieldRecord{ 
        Flags           uint32 // 标志位
        MangledTypeName int32 // 属性的类型名称
        FieldName       int32 // 属性名称
     }
    
    `FieldRecord` 对应成员的位置

    我这里画出找到 第一个FieldRecord 对应成员的位置
    我拿到 第一个FieldRecordFieldName属性名称在Mach-O上的信息

    0x3EC0 + 0x8 + 0xFFFFFFDF = 0x100003EA7
    

    需要减去虚拟内存地址:

    0x100003EA7 - 0x100000000 = 0x3EA7
    

    再去找data区中的 __TEXT,__swift5_reflstr找到0x3EA7:

    __TEXT,__swift5_reflstr

    直接可以找到Teacher类的第一个成员变量age的16进制

    相关文章

      网友评论

        本文标题:Swift进阶-属性

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