存储属性
存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性(由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文件去计算属性存储在内存中的位置信息。