Swift进阶-属性

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进阶-属性)