Swift 进阶 03: 属性

903 阅读7分钟

一、存储属性

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

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

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

1 letvar 的区别:

var age = 18
let x = 20

1.1 汇编的⻆度分析

image.png

image.png 就是无论是let 还是 var ,从汇编来看本质上letvar都是地址,两者无任何区别

1.2 SIL的⻆度分析

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

var x : 只有get方法

都是存储属性,都有初始值
默认存储属性都很合成get/set方法 存储属性访问是get,赋值是set, let没有生成set方法; 使用的时候不会更改值用let修饰,会更改值用var。


二、计算属性(Computed Property

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

struct square{
    // 实例中占据内存 8 字节
    var width: Double
    var area: Double {
        get{
            return width * width
        }
        set{
            self.width = newValue
        }
    }
}

2.1 只读计算属性

set,只有get

  • 模式一:
struct square{
    var width: Double = 30
    var area: Double {
        get{
            return width * width
        }
    }

}

area对于内部和外部都不能进行赋值,只能读取数据

  • 模式二:
struct square{
    var width: Double = 30
    private(set) var area: Double = 40
    func test() {
        self.area
    }
}

SIL分析

struct square {
  @_hasStorage @_hasInitialValue var width: Double { get set }
  @_hasStorage @_hasInitialValue private(set) var area: Double { get set }
  func test()
  init()
  init(width: Double = 30, area: Double = 40)
}

对于外部来说,只有get属性,不能赋值;内部即可读取也可赋值

三、属性观察者

3.1 观察者 willSet && didSet

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

class SubjectName {
    var subjectName: String = "" {
        willSet {
            print("subjectName will set value \(newValue)")
        }
        didSet{
            print("subjectName has been chaged \(oldValue)")
        }
    }
}
let s = SubjectName()
s.subjectName = "Swift"

SIL分析

image.png


3.2 观察者初始化操作

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

class SubjectName {
    var subjectName: String = "" {
        willSet {
            print("subjectName will set value \(newValue)")
        }
        didSet{
            print("subjectName has been chaged \(oldValue)")
        }
    }
    init(subjectName: String) {
        // 初始化操作
        self.subjectName = subjectName
    }
}
let s = SubjectName(subjectName: "Swift 进阶")

SIL分析

image.png

初始化,直接将值从内存中拷贝到该属性

3.3 观察者 -> 计算属性

只需将相关代码添加到属性的 setter
不需要写观察属性,多此一举,如下示例: image.png

3.4 观察者 -> 继承

先写一个LGTeacher 类,然后继承LGTeacher这个类,改变继承类的值

class LGTeacher {
    var age: Int {
        willSet{
            print("age will set value \(newValue)") }
        didSet{
            print("age has been changed \(oldValue)")
        }
    }
    var height: Double
    init(_ age : Int, _ height: Double) {
        self.age = age
        self.height = height
    }
}
class LGParTimeTeacher: LGTeacher {
    override var age: Int {
        willSet{
            print("override age will set value \(newValue)") }
        didSet{
            print("override age has been changed \(oldValue)")
        }
    }
    var subjectName: String
    init(_ subjectName: String) {
        self.subjectName = subjectName
        super.init(18, 180.0)
        self.age = 20
    }
}
let t = LGParTimeTeacher("Swift")

执行结果:

override age will set value 20
age will set value 20
age has been changed 18
override age has been changed 18
Program ended with exit code: 0

可以得出调用顺序为:

override willSet(子类) -> willSet(父类) -> didSet(父类) -> override didSet(子类)

SIL分析:

image.png


四、延迟存储属性

  • 延迟存储属性的初始值在其第一次使用时才进行计算。 
  • 用关键字 lazy 来标识一个延迟存储属性
class Subject {
    lazy var age: Int = 18
}
var s = Subject()
print(s.age)

print("end")
  • LLDB调试分析:

image.png

如上图所示:未使用已使用0x0000000000000000变成了0x0000000000000012,所以第一次使用时才进行计算。 

  • SIL分析:

image.png

初始化时,lazy修饰,并且它是可选值,默认未nilfinal修饰,不能被重写。


观察Subject.age.getter的实现

image.png

先访问__lazy_storage_的地址
把内存地址的值给寄存器%4
枚举的模式匹配,有值走bb1的代码块,没有值走bb2的代码块

bb2模块,没有值先进行取地址赋值操作 image.png

bb1模块,直接将原有的值给返回出去

image.png

lazy修饰是可选值,第一次使用的时候才会把值赋值给属性

五、类型属性

5.1 类型属性

  • 类型属性其实就是一个全局变量
  • 类型属性只会被初始化一次
class LGTeacher{
    static var age: Int = 18
}
LGTeacher.age = 30

SIL分析

// one-time initialization token for age
sil_global private @$s4main9LGTeacherC3age_Wz : $Builtin.Word
// static LGTeacher.age
sil_global hidden @$s4main9LGTeacherC3ageSivpZ : $Int

age 变成了全局变量,全局声明了age变量;static本质上就是全局变量

5.2 单例模式

class LGTeacher{
    static let sharedInstance = LGTeacher()
    private init(){}
}

初始化私有化,外部只能通过sharedInstance来进行访问


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

6.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,这里记录了v-Table相关的信息,接下来需要认识一下 typeDescriptor中的 fieldDescriptor

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 {
    MangledTypeName     int32
    Superclass                int32
    Kind                         uint16
    FieldRecordSize        uint16
    NumFields                uint32
    FieldRecords             [FieldRecord]
}  

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

struct FieldRecord{
    Flags uint32 
    MangledTypeName int32 
    FieldName int32
}

Flags:标志位;MangledTypeName:当前属性的类型信息;FieldName属性名称

6.2 Mach-o文件查找属性信息

定义一个属性结构,编译生成Mach-o文件

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

如何查找Mach-o文件?

  • 先把工程Products目录显示出来 image.png
  • MachOView打开Mach-o可执行文件.

image.png

找到DescriptorMach-o文件中的地址,相加后在减去0x100000000(虚拟内存的基地址),之后得到一个新地址: 0x3D74

0x3EFC + 0xFFFFFE78 = 0x100003D74
0x100003D74 - 0x100000000 = 0x3D74

0x3D74在Mach-o的文件的位置如下 截屏2022-01-09 18.02.46.png

0x3D74在Mach-o文件const的位置地址为50 00 00 80, 根据Descriptor类型为TargetClassDescriptor类,找到fieldDescriptor需要偏移4个成员大小(4个4字节).

50 01 00 00fieldDescriptor的偏移信息,要找到fieldDescriptor的具体地址信息, 需要加上偏移信息,具体计算流程如下(小端模式,需从右往左读):

0x3D80 + 0x4 + 0x0150 = 0x3ED4

根据计算结果0x3ED4,在Mach-O文件的位置如下:

截屏2022-01-09 18.32.21.png

0x3ED4FieldDescriptor的首地址,要找到FieldRecords的具体地址,需要偏移FieldRecords之前的成员属性大小;MangledTypeName 2字节Superclass 2字节Kind 2字节FieldRecordSize 4字节NumFields 4字节


FieldName的地址计算流程:

0x3EE4 + 0x4 + 0x4 + 0xFFFFDF = 0x100003ECB
// 减去 虚拟内存的基地址
0x100003ECB - 0x100000000 = 0x3ECB

计算结果为:0x3ECB,在Mach-o的位置如下:

截屏2022-01-09 18.51.29.png

如图,0x3ECBMach-o文件的reflStr的位置

  • 61 67 65 00: age
  • 61 67 65 31: age1