在Swift中,属性将值与特定的类、结构体或枚举关联。其中分为存储属性和计算属性。属性也可以直接与类型本身关联,这种属性称为类型属性。
存储属性
存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性(由var关键字引入)要么是常量存储属性(由let关键字引入)。存储属性可以说是类或结构体的成员变量,因为它和类或者结构体的内存结构相关。
let用来声明常量,常量的值一旦设置好便不能再被更改。var用来声明变量,变量的值可以在将来设置为不同的值。 我们开通过下面的代码进行一个对比:- 从汇编角度去分析这两个关键字
let age = 18
var y = 12
从汇编的角度看,这两个没什么区别,都是把值存储到寄存器中。
使用lldb调试内存
从内存地址来看,这两个变量存储的内存地址是连续的,而且都存储在DATA中。
- 从sil文件去分析
通过sil文件可以看出来,
var关键字修饰的变量age里面有get、set方法,而let关键字修饰的变量x里面没有set方法,因此变量x无法被修改。
计算属性
存储的属性是最常见的,除了存储属性,类、结构体和枚举也能够定义计算属性,计算属性并不存储值,他们提供getter和setter来修改和获取值。对于存储属性来说可以是常量或变量,但计算属性必须定义为量。于此同时我们书写计算属性时候必须包含类型,因为编译器需要知道期望返回值是什么。
我们查看一下这个结构体的sil文件:
我们发现这个计算属性
area没有@_hasStorage,也就是说这个属性可能是个方法,没有存储在内存中,我们通过lldb调试验证一下。
调试结果显示,
square实例里面确实没有存储area属性。因此计算属性area是方法,不会占用实例内存。
我们使用sil文件查看area的getter和setter方法
在
getter方法中,直接是调用了square实例对象的width存储属性的值进行相关操作。而在setter方法中,如果没有手动新增输入参数,那么会自动生成一个默认参数newValue,外部的值传进来的时候是直接赋值给newValue。所以计算属性根本不会有存储在实例的成员变量,那也就意味着计算属性不占内存,并且在计算属性中直接修改存储属性的值,也是直接修改了,并没有调用存储属性的setter方法。
属性观察者
属性观察者用来观察属性的值的变化,当属性值将要改变时,会调用
willSet,即使这个值与原有的值相同。当属性值已经改变之后就会调用didSet。
我们查看一下sil文件,查看subjectName的setter方法
我们可以看到,在对属性值进行修改前,会先去调用
willSet。然后就行属性值修改。在修改完成后,调用didSet。
这里就会引出一个问题,既然属性值改变就会调用属性观察者,那么对这个属性进行初始化时,属性值发生变化,是否会调用属性观察者?
代码运行后,我们可以看到,当进行初始化时,并没有调用
willSet和didSet,这是什么原因呢?我们去查看sil文件,找到文件里面的init方法。
我们可以看到,首先会使用
ref_element_addr去获取subjectName的内存地址。然后在初始化的时候,是把字符串直接拷贝到subjectName的内存地址里面,没有调用setter方法。当初始化还未完成的时候,某些属性还未初始化,去访问存储属性可能会造成一些内存上的问题。
继承属性观察者
如果一个属性观察者被继承,那么调用顺序会是怎么样?
通过打印log我们可以看到,首先调用的是子类的
willSet,然后是父类的willSet,接着是父类的didSet,最后是子类的didSet。为什么是这个顺序呢?我们通过sil文件来分析一下
通过查看
LGPartTimeTeacher.age.setter方法,我们可以看到,在调用完子类的willSet方法后,会去调用父类的setter方法,然后在调用子类的didSet。同时在父类的setter方法中,先调用了父类的
willSet方法,最后调用父类的didSet方法。
延迟属性
延迟属性使用lazy关键字来定义,当你第一次使用到这个属性的时候才会进行初始化。
延迟属性必须使用var关键字来修饰,因为let必须在实例的初始化方法完成之前就拥有值。
我们通过lldb打印来看一下延迟属性在使用之后,变量
s的内存变化。
我们通过
x/8g命令查看内存地址,前16字节存储的是类的metaData和referenceCount。后面8字节则是存储的是age的值,我们可以发现,在未开始使用属性时,没有存储age的值。当打印完s.age属性后,再去查看s的内存地址,发现存储了初始值18。
延迟属性时怎么实现的呢?我们使用sil文件来分析一下。
通过属性分析我们可以看到延迟属性被声明为可选类型,初始化的时候被赋值一个
Optional.none,同时存储属性还会被final关键字修饰,也就是说延迟属性不允许被重写。
我们再去看一下lazy属性的getter方法里面发生了什么
我们看一下第一个红框里面的代码:
switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6
这里面对延迟属性的可选值进行判断,如果该属性有值,那么执行bb1的代码,然后执行bb3,直接返回这个值。如果这个属性没有值,那么就执行bb2的代码,把初始值赋值给属性,并执行bb3返回值。
使用延迟属性注意点
- 如果多条线程同时第一次访问延迟属性,无法保证属性只被初始化1次。因此,延迟属性不是线程安全的。
- 当结构体包含一个延迟属性时,只有
var修饰实例 才能访问延迟属性,因为延迟属性初始化时需要改变结构体的内存。
类型属性
属性根据是否使用实例去访问可分为实例属性(Instance Property)和类型属性(Type Property)。类型属性和实例属性一样,也可以分为存储属性和计算属性。类型属性需要使用static关键字来声明。
类型存储属性
和实例存储属性不同,类型存储属性必须要有初始值,因为类型没有像实例那样的 init 初始化器来初始化存储属性。
类型计算属性
在类里面类型计算属性不仅可以用static修饰也可以使用class修饰。
类型属性sil文件分析
类型属性代码如下:
类型属性的sil文件如下:
类型属性和实例属性定义差不多,只是在属性前会被
static修饰。从上面文件中我们看到,Swift会为每一个类型属性声明一个全局变量,以及类型属性初始化的token。
我们可以得到类型属性的2个特点:
- 类型属性其实就是一个全局变量
- 类型属性只会被初始化一次
那么类型属性怎么保证只会被初始化一次呢?
我们先在main函数里面找到
LGTeacher.age,我们发现在函数里面,会调用到LGTeacher.age.unsafeMutableAddressor这个方法,也就是去访问age的内存地址。
然后我们去寻找LGTeacher.age.unsafeMutableAddressor方法。
从方法里面我们可以看到,首先去获取
age属性对应的全局变量token的内存地址,转成RawPointer指针,然后去初始化一次age属性。最后返回全局变量age属性的内存地址。
接着我们去看一下function_ref one-time initialization function for age方法。
我们可以看到,在这个方法里面,先是获取全局变量
age的内存地址,然后把Int类型值18存到这个内存地址里面。至此,我们完成了age属性的初始化。
最后我们看一下 builtin “once” 是什么函数。我们把sil指令代码转成IR代码看一下。调用的方法名 s4main10HWLTeacherC1h_WZ 可以看到这里builtin "once" 指的是 swift_once
通过源码查找
swift_once的实现
我们看到了
dispatch_once_f,也就是说和OC一样,都是使用了dispatch_once确保static属性只会被初始化一次。
Swift单例模式
由于Swift不允许使用dispatch_once来使用单例。因此我们可以利用Static属性只初始化一次的特性来设计单例模式。代码如下:
通过把
init函数私有,保证外部想使用LGTeacher实例只能去调用shareInstance,达到单例模式的效果。
属性在Mahco文件的位置信息
我们通过swift源码的分析,已经知道了swift类的本质是HeapObject,他有两个成员变量metadata和refcount,在metadata中存放了Description。
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:TargetClassDescriptor
var iVarDestroyer: UnsafeRawPointer
}
而在Swift进阶(二) —— 方法探究中,我们知道了TargetClassDescriptor,也知道了TargetClassDescriptor的结构如下:
class 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
其中,
FieldRecordIterator实际上是一个数组,我们查看FieldRecordIterator的结构体,发现里面存储的是一个FieldRecord类。因此,我们可以得出fieldDescriptor的具体结构。
struct FieldDescriptor {
MangledTypeName int32
Superclass int32
Kind uint16
FieldRecordSize uint16
NumFields uint32
FieldRecords [FieldRecord]
}
其中,NumFields代表了当前有几个属性,而FieldRecord则是记录了每个属性的信息。
接下来我们查看一下FieldRecord类
通过源码,我们可以得到
FieldRecord类的结构如下:
struct FieldRecord{
Flags uint32
MangledTypeName int32
FieldName int32
}
在Swift进阶(二) —— 方法探究中,我们知道了如何查找V-table的地址,这次我们使用同样的方法来查找属性地址。
首先,我们要先找到classDescriptor
计算
ClassDescriptor在Mach-O 中的偏移地址,然后减去虚拟基地址,找到ClassDescriptor的地址
0x3EE0 + 0xFFFFFE14 = 0x100003CF4
0x100003CF4 - 0x100000000 = 0x3CF4
然后找到3CF4在Mach-O 中的位置
红框标识的地址就是
ClassDescriptor的首地址。
因为fieldDescriptor是ClassDescriptor的第五个属性,因此我们要向后偏移16个字节
红框标识的就是
fieldDescriptor的偏移地址。所以fieldDescriptor的实际地址为
3D04 + 01B4 = 0x3EB8
在Mach-O中找到0x3EB8,这就是fieldDescriptor的首地址,后面的地址存储的就是fieldDescriptor的成员变量。我们通过上面fieldDescriptor的成员变量的字节大小可知,往后面偏移16个字节就是FieldRecords的地址。
因此
0x3EC8就是FieldRecords的首地址。而FieldRecords里面是FieldRecord,因此我们可以按照FieldRecord的结构Flags、MangledTypeName、FieldName顺序寻找这三个变量的偏移地址。我们要获取属性名称,因此我们要去寻找FieldName的偏移地址,FieldName的偏移地址如下:
由此我们可以计算出
FieldName的地址
3EC8 + 4 + 4 + FFFFFFDD = 0x100003EAD
0x100003EAD - 0x100000000 = 3EAD
找到 3EAD 可以看到属性的名称
最终在Macho文件reflstr字段中找到属性名称的地址,其中
61 67 65 00是属性age的地址,61 67 65 31是属性age1的信息。