在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
的信息。