一. 存储属性
存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性(由 var
关键字引入),要么是常量存储属性(由 let
关键字引入)。存储属性这里没有什么特别要强调的,因为随处可⻅。
class CXTeacher {
var age: Int
var name: String
}
从定义上区分 var 与 let
比如这里的 age
和 name
就是我们所说的存储属性,这里我们需要加以区分的是 let
和 var
两者的区别,从定义上 let
用来声明常量,常量的值一旦设置好便不能再被更改;var
用来声明变量,变量的值可以在将来设置为不同的值。
这里我们来看几个案例:
从汇编的角度分析 var 与 let 的区别
var age = 20
let x = 10
print(age,x)
这里通过汇编调试可以看到,14 行与 18 行分布代表将 0x14
存入 w8
跟 将 0xa
存入 w8
,可以看出在汇编上 var
与 let
并没有区别。
通过内存的读取也可以看到 age
与 x
就相差 8 个字节,并没有别的区别。
从 SIL 的角度分析 var 与 let 的区别
-
swift
代码
var age = 20
let x = 10
-
sil
代码
// _hasStorage 代表是存储属性,_hasInitialValue 代表都有初始值,{ get set } : 编译器默认会给存储属性生成 get 跟 set 方法,当我们访问存储属性的时候就是访问 get 方法,当我们改变属性的值的时候就是访问 set 方法
@_hasStorage @_hasInitialValue var age: Int { get set }
// 当用 let 修饰的时候,编译器没有生成 set 方法,所以修改属性值的时候编译器会报错
@_hasStorage @_hasInitialValue let x: Int { get }
通过 sil
代码可以看到,var
与 let
其实也是一种语法糖,let
修饰的属性不会生成 set
方法。
二. 计算属性
存储的属性是最常⻅的,除了存储属性,类、结构体和枚举也能够定义计算属性,计算属性并不存储值,他们提供 getter
和 setter
来修改和获取值。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量。于此同时我们书写计算属性时候必须包含类型,因为编译器需要知道期望返回值是什么。
-
swift
代码
struct square {
// 在实例当中需要占用内存空间
var width: Double
var hegith: Double
// 代表将 set 方法私有,只能在结构体内部通过 self.area 修改 area 的值
// private(set) var area: Double
var area: Double {
get {
return width * hegith
}
// newValue 是编译器默认生成的,自己也可以通过 set(自定义名称) {} 修改
set {
self.width = newValue
}
}
}
-
sil
代码
struct square {
@_hasStorage var width: Double { get set }
@_hasStorage var hegith: Double { get set }
var area: Double { get set }
init(width: Double, hegith: Double)
}
通过 sil
代码可以看到,计算属性的本质就是 set
跟 get
方法。
三. 属性观察者
属性观察者会观察用来观察属性值的变化,一个 willSet
当属性将被改变调用,即使这个值与原有的值相同,而 didSet
在属性已经改变之后调用。它们的语法类似于 getter
和 setter
。
-
swift
代码
class SubjectName {
var subjectName: String = "" {
willSet {
print("subjectName will set value \(newValue)")
}
didSet {
print("subjectName has been changed \(oldValue)") }
}
}
-
sil
代码
// 在赋值之前会调用 willset 方法
// function_ref SubjectName.subjectName.willset
%10 = function_ref @$s4main11SubjectNameC07subjectC0SSvw : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> () // user: %11
%11 = apply %10(%0, %1) : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> ()
// 给 subjectName 属性设置新的值
retain_value %0 : $String // id: %12
%13 = ref_element_addr %1 : $SubjectName, #SubjectName.subjectName // user: %14
%14 = begin_access [modify] [dynamic] %13 : $*String // users: %16, %15, %18
%15 = load %14 : $*String // user: %17
store %0 to %14 : $*String // id: %16
release_value %15 : $String // id: %17
end_access %14 : $*String // id: %18
// 在赋值之后会调用 didset 方法
// function_ref SubjectName.subjectName.didset
%19 = function_ref @$s4main11SubjectNameC07subjectC0SSvW : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> () // user: %20
%20 = apply %19(%6, %1) : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> ()
这里我们在使用属性观察器的时候,需要注意的一点是在初始化期间设置属性时不会调用 willSet
和 didSet
观察者;只有在为完全初始化的实例分配新值时才会调用它们。运行下面这
段代码,你会发现当前并不会有任何的输出。
-
swift
代码
class SubjectName{
var subjectName: String = "[unnamed]"{
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
代码
// SubjectName.subjectName.getter
sil hidden [transparent] @$s4main11SubjectNameC07subjectC0SSvg : $@convention(method) (@guaranteed SubjectName) -> @owned String {
// %0 "self" // users: %2, %1
bb0(%0 : $SubjectName):
debug_value %0 : $SubjectName, let, name "self", argno 1 // id: %1
// 拿到 subjectName 这个属性的内存地址
%2 = ref_element_addr %0 : $SubjectName, #SubjectName.subjectName // user: %3
%3 = begin_access [read] [dynamic] %2 : $*String // users: %4, %6
// 将要赋值的字符串的值直接拷贝到内存地址,并没有调用 setter 方法
%4 = load %3 : $*String // users: %7, %5
retain_value %4 : $String // id: %5
end_access %3 : $*String // id: %6
return %4 : $String // id: %7
} // end sil function '$s4main11SubjectNameC07subjectC0SSvg'
通过 sil
代码可以看到,这个时候是直接将字符串的值拷贝到 subjectName
属性的内存地址,并没有调用 setter
方法。编译器这样做的原因可能是这个时候有些属性并没有初始化完成,通过 setter
方法赋值可能会造成内存错误。
上面的属性观察者只是对存储属性起作用,如果我们想对计算属性起作用怎么办?很简单,只需将相关代码添加到属性的 setter
。我们先来看这段代码:
这里可以看到在计算属性中添加 willSet
跟 didSet
会报错,因为这里已经实现 set
方法了,可以直接在 set
中属性赋值前跟属性赋值后进行添加监听。
如果子类继承于父类的情况下,willSet
和 didSet
方法调用是什么样的呢?
这里可以看到当子类继承父类的时候,修改子类实例对象 age
属性的值会先调用子类的 willSet
方法,然后调用父类的 willSet
,赋值完成后会先调用父类的 didSet
方法,然后再调用子类的 didSet
方法。
四. 延迟存储属性
- 延迟存储属性的初始值在其第一次使用时才进行计算。
- 用关键字
lazy
来标识一个延迟存储属性。
通过案例可以看到,在访问 age
属性之前内存中的值是 0,当访问之后才会对内存空间进行初始化。
-
sil
代码
class CXPerson {
lazy var age: Int { get set }
// 这里延时属性本质上是可选类型
@_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
@objc deinit
init()
}
// variable initialization expression of CXPerson.$__lazy_storage_$_age
sil hidden [transparent] @$s4main8CXPersonC21$__lazy_storage_$_age029_12232F587A4C5CD8B1EEDF696793G2FCLLSiSgvpfi : $@convention(thin) () -> Optional<Int> {
// Optional.none 相当于 nil,初始化的时候 age 为 0
bb0:
%0 = enum $Optional<Int>, #Optional.none!enumelt // user: %1
return %0 : $Optional<Int> // id: %1
} // end sil function '$s4main8CXPersonC21$__lazy_storage_$_age029_12232F587A4C5CD8B1EEDF696793G2FCLLSiSgvpfi'
// 访问 age 的时候会调用 getter 方法
// CXPerson.age.getter
sil hidden [lazy_getter] [noinline] @$s4main8CXPersonC3ageSivg : $@convention(method) (@guaranteed CXPerson) -> Int {
// %0 "self" // users: %14, %2, %1
bb0(%0 : $CXPerson):
debug_value %0 : $CXPerson, let, name "self", argno 1 // id: %1
%2 = ref_element_addr %0 : $CXPerson, #CXPerson.$__lazy_storage_$_age // user: %3
%3 = begin_access [read] [dynamic] %2 : $*Optional<Int> // users: %4, %5
%4 = load %3 : $*Optional<Int> // user: %6
end_access %3 : $*Optional<Int> // id: %5
// 这里会进行枚举判断,当age有值的时候走 bb1,没值的时候走 bb2
switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6
// 返回原因的值
// %7 // users: %9, %8
bb1(%7 : $Int): // Preds: bb0
debug_value %7 : $Int, let, name "tmp1" // id: %8
br bb3(%7 : $Int) // id: %9
// 构建一个 Int 类型的值并存储到 age 属性的内存地址
bb2: // Preds: bb0
%10 = integer_literal $Builtin.Int64, 6 // user: %11
%11 = struct $Int (%10 : $Builtin.Int64) // users: %18, %13, %12
debug_value %11 : $Int, let, name "tmp2" // id: %12
%13 = enum $Optional<Int>, #Optional.some!enumelt, %11 : $Int // user: %16
%14 = ref_element_addr %0 : $CXPerson, #CXPerson.$__lazy_storage_$_age // user: %15
%15 = begin_access [modify] [dynamic] %14 : $*Optional<Int> // users: %16, %17
store %13 to %15 : $*Optional<Int> // id: %16
end_access %15 : $*Optional<Int> // id: %17
br bb3(%11 : $Int) // id: %18
这里延迟属性也可以理解为懒加载,我们使用 lazy
修饰属性的时候可以帮我们节省内存空间。但是延迟属性不能保证变量只能被访问一次,因为会涉及到多个线程同时访问的情况,所以并不是线程安全的。
内存独占
-
swift
代码
class CXPerson {
lazy var age: Int = 6
// lazy var age: Int {
// return 6
// }()
}
var p = CXPerson()
//
let t = p.age
p.age = 30
-
sil
代码
// CXPerson.age.setter
sil hidden @$s4main8CXPersonC3ageSivs : $@convention(method) (Int, @guaranteed CXPerson) -> () {
// %0 "value" // users: %4, %2
// %1 "self" // users: %5, %3
bb0(%0 : $Int, %1 : $CXPerson):
debug_value %0 : $Int, let, name "value", argno 1 // id: %2
debug_value %1 : $CXPerson, let, name "self", argno 2 // id: %3
%4 = enum $Optional<Int>, #Optional.some!enumelt, %0 : $Int // user: %7
%5 = ref_element_addr %1 : $CXPerson, #CXPerson.$__lazy_storage_$_age // user: %6
%6 = begin_access [modify] [dynamic] %5 : $*Optional<Int> // users: %7, %8
store %4 to %6 : $*Optional<Int> // id: %7
end_access %6 : $*Optional<Int> // id: %8
%9 = tuple () // user: %10
return %9 : $() // id: %10
}
在我们修改 age
属性的前后会调用 begin_access
跟 end_access
,保证在赋值的过程中独占内存,也是为了内存访问安全。
五. 类型属性
- 类型属性其实就是一个全局变量
- 类型属性只会被初始化一次
下面我们通过将以下 swift
代码转成 sil
代码来看一下。
-
swift
代码
class CXPerson {
static var age: Int = 18
}
CXPerson.age = 30
-
sil
代码、IR
代码、源码
下面我们通过阅读 sil
代码、IR
代码及源码,看一下类型属性的底层实现步骤。
-
CXPerson
声明
class CXPerson {
@_hasStorage @_hasInitialValue static var age: Int { get set }
@objc deinit
init()
}
// one-time initialization token for age
sil_global private @$s4main8CXPersonC3age_Wz : $Builtin.Word
// static CXPerson.age
sil_global hidden @$s4main8CXPersonC3ageSivpZ : $Int
在 CXPerson
类的声明中 age
还是一个存储属性,只是多了 static
修饰。而且 age
会被声明成一个全局变量。
-
age
属性的访问
// function_ref CXPerson.age.unsafeMutableAddressor
%3 = function_ref @$s4main8CXPersonC3ageSivau : $@convention(thin) () -> Builtin.RawPointer // user: %4
%4 = apply %3() : $@convention(thin) () -> Builtin.RawPointer // user: %5
这里注释说明可以看出这里是通过 CXPerson.age
的内存地址进行访问,所以我们搜索 s4main8CXPersonC3ageSivau
。
// CXPerson.age.unsafeMutableAddressor
sil hidden [global_init] @$s4main8CXPersonC3ageSivau : $@convention(thin) () -> Builtin.RawPointer {
bb0:
// 通过步骤 1 中声明的 token,拿到 age 属性的内存地址,并把地址赋值给 %1
%0 = global_addr @$s4main8CXPersonC3age_Wz : $*Builtin.Word // user: %1
// 指针类型转换
%1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
// function_ref one-time initialization function for age 调用 initialization 函数,并把函数地址赋值给 %2
%2 = function_ref @$s4main8CXPersonC3age_WZ : $@convention(c) () -> () // user: %3
// 在这里传入 %1 %2 作为参数
%3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
//拿到全局变量的内存地址
%4 = global_addr @$s4main8CXPersonC3ageSivpZ : $*Int // user: %5
//将全局变量的内存地址转为原生指针
%5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
// 这里返回上面转换得到的原生指针
return %5 : $Builtin.RawPointer // id: %6
} // end sil function '$s4main8CXPersonC3ageSivau'
通过 sil
代码可以看到,在 CXPerson
类的声明中 age
还是一个存储属性,只是多了 static
修饰。而且 age
会被声明成一个全局变量。
- 找到
initialization
函数
xcrun swift-demangle
命令执行结果:
xcrun swift-demangle s4main8CXPersonC3ageSivpZ
$s4main8CXPersonC3ageSivpZ ---> static main.CXPerson.age : Swift.Int
// one-time initialization function for age
sil private [global_init_once_fn] @$s4main8CXPersonC3age_WZ : $@convention(c) () -> () {
bb0:
// 创建全局变量 age
alloc_global @$s4main8CXPersonC3ageSivpZ // id: %0
// 获取到全局变量内存地址
%1 = global_addr @$s4main8CXPersonC3ageSivpZ : $*Int // user: %4
// 构建 Int 类型的结构体
%2 = integer_literal $Builtin.Int64, 18 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // user: %4
// 初始化 age 变量
store %3 to %1 : $*Int // id: %4
%5 = tuple () // user: %6
return %5 : $() // id: %6
} // end sil function '$s4main8CXPersonC3age_WZ'
- 将
swift
代码转为IR
代码并找到once
define hidden swiftcc i8* @"$s4main8CXPersonC3ageSivau"() #0 {
entry:
%0 = load i64, i64* @"$s4main8CXPersonC3age_Wz", align 8
%1 = icmp eq i64 %0, -1
%2 = call i1 @llvm.expect.i1(i1 %1, i1 true)
br i1 %2, label %once_done, label %once_not_done
once_done: ; preds = %once_not_done, %entry
%3 = load i64, i64* @"$s4main8CXPersonC3age_Wz", align 8
%4 = icmp eq i64 %3, -1
call void @llvm.assume(i1 %4)
ret i8* bitcast (%TSi* @"$s4main8CXPersonC3ageSivpZ" to i8*)
once_not_done: ; preds = %entry
//这里可以看到会调用 swift_once 函数
call void @swift_once(i64* @"$s4main8CXPersonC3age_Wz", i8* bitcast (void ()* @"$s4main8CXPersonC3age_WZ" to i8*), i8* undef)
br label %once_done
}
通过 xcrun swift-demangle
命令可以看到 s4main8CXPersonC3ageSivau
就是 sil
代码中的 unsafeMutableAddressor
。
xcrun swift-demangle s4main8CXPersonC3ageSivau
$s4main8CXPersonC3ageSivau ---> main.CXPerson.age.unsafeMutableAddressor : Swift.Int
- 在源码中搜索
swift_once
函数。
通过源码可以看到,全局变量本质上还是使用了 GCD
的 dispatch_once_f
,确保类型属性只会被初始化一次,但是可以在外部修改类型属性的值。
swift 中单例写法
final class CXPerson {
static let sharedInstance = CXPerson()
private init() {}
}
类型方法
class CXPerson {
static func staticFunc() {
print("staticFunc")
}
class func classFunc() {
print("staticFunc")
}
}
CXPerson.staticFunc()
CXPerson.classFunc()
以上在方法前用 static
修饰是类型方法的写法,通过汇编可以看到 static
修饰的方法跟 class
修饰的方法调度的时候都是通过静态派发的方式。
sil_vtable CXPerson {
#CXPerson.classFunc: (CXPerson.Type) -> () -> () : @$s4main8CXPersonC9classFuncyyFZ // static CXPerson.classFunc()
#CXPerson.init!allocator: (CXPerson.Type) -> () -> CXPerson : @$s4main8CXPersonCACycfC // CXPerson.__allocating_init()
#CXPerson.deinit!deallocator: @$s4main8CXPersonCfD // CXPerson.__deallocating_deinit
}
将以上代码转成 sil
代码可以看到,class
修饰的方法会被注册到 vtable
中,这也是 class
修饰的方法能被子类重写的原因。
如图可以看到 static
修饰的方法在子类中重写会报错。
这里可以看到 class
不能用来修饰 struct
(值)类型。
六. 属性在 MachO 文件中的位置信息
在 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
}
在 Swift 中类与结构体(二)中讲到方法调度的过程中我们认识了 typeDescriptor
,这里面记录了 V-Table
的相关信息,接下来我们需要认识一下 typeDescriptor
中的 fieldDescripto
。
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
//V-Table
}
fieldDescriptor
记录了当前的属性信息,其中 fieldDescriptor
在源码中的结构如下:
struct FieldDescriptor {
var MangledTypeName: Int32
var Superclass: Int32
var Kind: uint16
var FieldRecordSize: uint16
var NumFields: UInt32
var FieldRecords: [FieldRecord]
}
其中 NumFields
代表当前有多少个属性,FieldRecords
记录了每个属性的信息,FieldRecord
的结构体如下:
struct FieldRecord{
var Flags: uint32
var MangledTypeName: Int32
var FieldName: Int32
}
基于以上认知,下面我们来看一下相关属性在 MachO
中对应的信息。
class CXPerson {
var age = 10
var age1 = 20
}
将以上代码编译后的可执行文件用 MachOView
工具打开。
这里 0xFFFFFE78 + 0x00003EFC - 0x100000000 = 3D74
就是 CXPerson
的 typeDescriptor
在 MachO
文件中的位置。
这 _TEXT, _const
文件中找到 3D74
,向后偏移 4 个字节之后就是 fieldDescriptor
在 MachO
中的信息,所以 0x00003D84 + 0x00000150 = 0x3ED4
就是 typeDescriptor
的 fieldDescriptor
属性在 MachO
文件中的位置。
在 _TEXT,__swift5_fieldmd
文件中可以看到 FieldDescriptor
的结构体的首地址就是 0x3ED4
,所以 0x3ED4
向后偏移 4 个字节之后的连续存储空间存储的就是 FieldRecords
属性的信息。所以 0x3EEC + FFFFFFDD - 0x100000000 = 0x3EC9
就是 FieldName
在 MachO
中的信息。
FieldName
代表属性的名称,如上图所示,0x3EC9
位置确实存储的就是 CXPerson
类的属性名称,0x00656761
就是 age
的 16 进制,0x31656761
就是 age1
的 16 进制。