存储属性
存储属性
是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性(由var
关键字引入),要么是常量存储属性(由let
关键字引入)。存储属性这里没有什么特别要强调的,因为随处可⻅,先看下面的定义:
class ATTeacher {
var age: Int
var name: String
}
比如这里的age
和name
就是我们所说的存储属性,这里需要加以区分的是let
和var
两者的区别,从定义上:let
用来声明常量,常量的值一旦设置好便不能再被更改;var
用来声明变量,变量的值可以在将来设置为不同的值。
示例
再看下面的示例:
汇编代码分析
我们在main.swift
文件分别用var
和let
定义。
var age = 18
let height = 20
然后以汇编的模式打个断点,连接真机运行:
把
0x12
放入w8
寄存器,把0x14
放在w8
寄存器中,从汇编的角度来分析,两者没有什么区别。在打印age
的地址信息可看出它存放在Mach-O
文件数据区__DATA.__common
的Section
里,以8字节格式化(x/8g
)打印输出前8字节存放age
,后8字节存放height
。
SIL分析
把上面的声明直接导出SIL文件,生成的定义如下:
@_hasStorage @_hasInitialValue var age: Int { get set }
@_hasStorage @_hasInitialValue let height: Int { get }
可以看出2个都是存储属性
,只是用var
声明的有get
方法和set
方法,而通过let
声明的只有get
方法。因此用let
声明属性赋值后就不能再更改了。
计算属性
存储的属性是最常⻅的,除了存储属性,类、结构体和枚举也能够定义计算属性
,计算属性并不存储值,他们提供getter
和setter
来修改和获取值。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量。于此同时我们书写计算属性时必须包含类型,因为编译器需要知道期望返回值是什么。
struct square {
// 存储属性:实例当中占据内存 (Double:8字节)
var width: Double
// 计算属性
var area: Double {
get {
return width * width
}
set {
self.width = newValue // newValue: 编译器默认给的新值名称
}
}
}
实例中就定义了计算属性的方法,需要提供get
和set
方法,再看一下下面这个例子:
struct square {
var width: Double = 30
var area: Double {
get {
return width * width
}
}
let height: Double = 20
}
当前这个struct和上面的有什么区别呢,我们通过生成SIL文件看一下
struct square {
@_hasStorage @_hasInitialValue var width: Double { get set }
var area: Double { get }
@_hasStorage @_hasInitialValue let height: Double { get }
init()
init(width: Double = 30)
}
area
和height
相同点是都没有set
方法,但本质是有区别的,area
本质是方法,而height
是存储值。
属性观察者
属性观察者会观察用来观察属性值的变化,一个willSet
当属性将被改变调用,即使这个值与原有的值相同,而didSet
在属性已经改变之后调用。它们的语法类似于getter
和setter
。
存储属性
class SubjectName {
var subjectName: String = ""{
willSet {
print("subjectName will set value \(newValue)")
}
didSet {
print("subjectName has been changed \(oldValue)")
}
}
}
let s = SubjectName()
s.subjectName = "Swift"
以上代码运行就调用了subjectName
的willSet
和didSet
方法,可以看到输出了print
的打印信息。
再把上面代码编译成
SIL
文件,然后找到它的setter
方法的定义。
从编译的
SIL
文件可以看到,在setter
方法里先调用了willset
,然后设值,再触发了didset
。这里我们在使用属性观察器的时候,需要注意的一点是在初始化期间设置属性时不会调用willSet
和didSet
观察者;只有在为完全初始化的实例分配新值时才会调用它们。运行下面这段代码,你会发现当前并不会有任何的输出。
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
}
}
let s = SubjectName(subjectName: "Swift")
再把上面代码编译成
SIL
文件,然后找到init
方法
可以看出
init
方法没有调用willset
和didset
方法,而是直接把subjectName
拷贝到内存中。
计算属性
上面的属性观察者只是对存储属性起作用,如果想对计算属性起作用,只需将相关代码添加到属性的setter
中。代码如下:
struct square {
var width: Double
var area: Double {
get {
return width * width
}
set {
// 这里添加观察代码
self.width = newValue
}
}
init(width: Double) {
self.width = width
}
}
在set
方法里设值之前添加类似willset
方法,设值之后添加类似didset
的代码即可。
继承
当在继承的时候,属性的观察者的调用方式是什么样的?下面通过一个示例来看一下调用流程。
从打印日志可以看出:
- 调用子类的的willset
- 调用父类的willset
- 调用父类的didset
- 调用子类的didset
延迟存储属性
延迟存储属性
是指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用lazy
来标示一个延迟存储属性。
定义一个延迟存储属性,代码如下:
class Subject {
lazy var age: Int = 18
}
var s = Subject()
print(s.age)
在print(s.age)打个断点,然后运行
前
16字节
存的是metadata
和refCount
,然后才存age
,这里看到age
还没有值,我们再往下走一个断点,再输出内存地址看看结果。
这时可以看到已经读到了内存中
age
的值。
SIL分析
我们再通过SIL
文件分析一下,还是把上面代码编译成SIL
文件,然后找到它的定义:
可以看到用
lazy
修饰的属性编译出来的是个可选类型,而且是final
修饰的。再看一下__lazy_storage_
的初始化表达式
__lazy_storage_
是个枚举,默认是Optional.none
,相当于nil
。然后再看一下age
的getter
方法。
从
getter
的SIL
代码可以看到,延迟存储属性
大概流程如下:
- 当初始化时会给个默认值
nil
- 第一次访问时由于没有值走
bb2
代码块bb2
: 把当前的值构建出来给到枚举变量,存储在__lazy_storage_
的地址中- 第二次访问有值就不需要做赋值操作,调用
bb1
代码块,直接从原有的值返回出去 如果延迟存储属性
存在闭包表达式,修饰的是个引用类型,在实际开发中就可以节省内存,只有在需要的时候才调用。还有一点,延迟存储属性
不是线程安全的。
类型属性
类型属性
用于定义某个类型所有实例共享的数据,比如所有实例都能用的一个常量(就像C语言中的静态常量),或者所有实例都能访问的一个变量(就像C语言中的静态变量)。
- 类型属性其实就是一个全局变量
- 类型属性只会被初始化一次
先看一下类型属性的写法,其实就是在存储属性前加个static
关键字:
// 定义
class ATTeacher {
static var age: Int = 18
}
// 类型属性的访问
ATTeacher.age = 20
还是把它编译成SIL
文件,定义如下:
class ATTeacher {
@_hasStorage @_hasInitialValue static var age: Int { get set }
@objc deinit
init()
}
// one-time initialization token for age
sil_global private @$s4main9ATTeacherC3age_Wz : $Builtin.Word
// static ATTeacher.age
sil_global hidden @$s4main9ATTeacherC3ageSivpZ : $Int
可以看出age
是个全局变量
。再看一下main
函数的定义:
这里有个函数地址,再搜索一下可以看到下面的定义:
所以这个
ATTeacher.age.unsafeMutableAddressor
实际返回的就是全局变量的地址。再看一下上面调用函数的定义:
从上面的函数调用可以看到有
builtin "once"
,再把上面的代码编译成IR
,可以找到下面这段代码:
通过
xcrun swift-demangle
命令还原一下上面的方法:
可以看到就是上面的
age.unsafeMutableAddressor
,这时调用了swift_once
,我们在Swift
源码中搜索一下,在Once.cpp
中找到相关的定义:
swift_once
实际上也是使用了GCD
的dispatch_once
函数,所以static
修饰的本质上也是只调用一次。因此在Swift
中创建单例代码就很简单了,示例如下:
class ATTeacher {
static let sharedInstance = ATTeacher()
private init(){}
}
在Mach-O的位置信息
在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
}
上篇在方法调度的过程中我们认识了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
// VTable
}
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
}
我们结合示例来验证一下。
示例
首先在main.swift定义一个类,定义2个属性。代码如下:
class ATTeacher {
var age = 18
var age1 = 20
}
然后编译,把编译后的可执行文件拖入到MachOView
工具中。
上一篇中已经介绍了
TargetClassDescriptor
存放在Section64(__TEXT,__swift5_types)
中,这里计算一下TargetClassDescriptor
在Mach-O
文件中的内存地址
0xFFFFFF2C + 0x3F40 = 0x100003E6C
# 减去虚拟基地址
0x100003E6C - 0x100000000 = 0x3E6C
0x3E6C
就是TargetClassDescriptor
的地址,在Section64(__TEXT,__const)
找到了0x3E6C
,也就是0x3E68
偏移4个字节,根据fieldDescriptor
在TargetClassDescriptor
的结构位置,需要往后偏移4个4字节
# FieldDescriptor的首地址
0x3E7C + 0x9C = 0x3F18
计算得到的结果我们在Section64(__TEXT,__swift5_fieldmd)
找到了,根据FieldDescriptor
的结构,要得到FieldRecords
的值,需要偏移4个4字节
根据
FieldRecords
的结构,要得到FieldName
的地址,需要偏移8个字节,然后根据地址+偏移量得出
0x3F28 + 0x8 + 0xFFFFFFDD = 0x100003F0D
# 减去基地址
0x100003F0D - 0x100000000 = 0x3F0D
在Section64(__TEXT,__swift5_reflstr)
就找到了0x3F0D
的地址,也就找到了变量age
和age1
对应的地址信息。
以上就是通过示例代码结合
Mach-O
文件计算得出变量在内存中的地址信息。
总结
本篇主要分析了不同类型属性的定义,以及通过SIL
代码了解不同属性的异同,结合案例去验证;最后通过编译导出的Mach-O
文件去计算属性存储在内存中的位置信息。