1、存储属性
- 存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是
变量存储属性(由 var 关键字引入)要么是常量存储属性(由 let 关键字引入)。
var和let的区别
- 在汇编角度看两者看不出区别,但在 SIL 文件中可以看到, let 属性比 var 缺少 set 方法
@_hasStorage @_hasInitialValue var age: Int { get set } @_hasStorage @_hasInitialValue let x: Int { get } ...
private()
private(set)修饰后,依然是存储属性,只不过set方法是私有的struct Square { // 实例中占据内存 var width: Double let height: Double private(set) var area: Double = 40 }- 拿到 SIL 看
struct Square { @_hasStorage var width: Double { get set } @_hasStorage let height: Double { get } @_hasStorage @_hasInitialValue private(set) var area: Double { get set } }
2、计算属性
- 除了存储属性, 类、结构体和枚举 也能够定义计算属性,计算属性并不存储值,他们提供
getter和setter来修改和获取值。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量。于此同时我们书写 计算属性必须包含类型 ,因为编译器需要知道期望返回值是什么。
3、属性观察者
3.1、存储属性观察者
- 属性观察者会观察属性值的变化,
willSet当属性将被改变时调用,即使 这个值与原有的值相同 ,而didSet在属性已经改变之后调用。它们的语法类似于 getter 和 setter。- 需要注意的一点是 在初始化期间设置属性时不会调用 willSet 和 didSet 观察者;只有在
为完全初始化的实例分配新值时才会调用它们。
- 需要注意的一点是 在初始化期间设置属性时不会调用 willSet 和 didSet 观察者;只有在
3.2、计算属性观察者
- 上面的属性观察者只是对存储属性起作用,如果我们想对计算属性起作用怎么办?很简单,只需将相关代码添加到属性的 setter
class Square { var width: Double var area: Double { get { return width * width } set { self.width = sqrt(newValue) } } init(width: Double) { self.width = width } }
3.3、继承属性观察者
- 涉及到继承时,属性观察者调用顺序按照: 子willSet --> 父willSet --> 父didSet --> 子didSet 执行
4、延迟存储属性
- 延迟属性必须有初始值
- 延迟存储属性的初始值在其 第一次使用时才进行计算。
- 必须用 var 定义属性,因为 let 必须在实例的初始化方法完成之前就拥有值,而延迟属性类似于 懒加载 用到时才赋值
- 线程不安全
- 用关键字
lazy来标识一个 延迟存储属性class Person { lazy var age: Int = 20 }- SIL文件:被 final 修饰、并且带 ? 是一个可选属性
class Person { @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set } init() @objc deinit }- 判断有值时走bb1,没有值时走bb2
switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %7
5、类型属性
-
类型属性 其实就是一个 全局变量( SIL 中创建,通过
LZPerson.age.unsafeMutableAddressor进行内存地址访问)unsafeMutableAddressor:返回全局变量的内存地址
-
类型属性 只会被初始化一次(底层是GCD的
dispatch_once_t封装的swift_once方法,所以线程安全) -
通过
static定义类型属性,如果是类,也可以用关键字 class,访问时直接通过类型访问struct LZPerson { static var age: Int = 20 } LZPerson.age += 1
5.1、单例
5.1.1、OC单例
5.1.2、Swift单例
6、属性在Mach-O中
- 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 } - Swift 类的属性存放在
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 }
- 其中
Mach-O找属性位置
-
拉入可执行文件到 MachOView 中,先看
Section64(__TEXT,swift5_types)这项,先找 TargetClassDescriptor ,小端模式计算 -
减去基地址后得到 6BA8,在
Section64(__TEXT,__const)中找到了囊括 6BA8 的 6BA0 这排数据,再偏移8字节就是 6BA8 中的数据50 00 00 80也就是 TargetClassDescriptor 的起始地址 -
fieldDescriptor 在 TargetClassDescriptor 结构中前边有4个 Int32 的属性,是4个4字节,所以往后查4个4字节,得到
D4 03 00 00 -
D4 03 00 00其实是 fieldDescriptor 的偏移地址而不是直接地址,所以要找到 fieldDescriptor 在 Mach-O 文件当中的具体位置,还需要加上0x3D4(小端):0x6BB0 + 0x8 + 0x3D4 = 0x6F8C -
FieldRecords 在 FieldDescriptor 结构体中还要偏移3个4字节、2个2字节
-
得到了 FieldRecords 结构体的信息,但是注意这里存的都是偏移地址不是直接地址,还要算一下
-
我们获取一下 FieldName 的地址信息,计算后减去基地址得到
6F590x6F9C + 0x8 + 0xFFFFFFB5 = 0x100006F59 -
可以在
Section64(__TEXT,swift5_reflstr)中找到 age 和 name