一、存储属性
存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性
(由 var
关键字引入)要么是常量存储属性
(由 let
关键字引入)。存储属性这里没有什么特别要强调的,因为随处可见。
class ZGTeacher {
let age: Int = 32
var name: String = "Zhang"
}
比如这里的 age
和 name
就是我们所说的存储属性,这里我们需要加以区分的是 let
和 var
两者的区别:从定义上: let
用来声明常量
,常量的值一旦设置好便不能再被更改;var
用来声明变量
,变量的值可以在将来设置为不同的值。
1.1 代码案例
这里我们来看几个案例:
class ZGTeacher {
let age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
struct ZGStudent {
let age: Int
var name: String
}
let t = ZGTeacher(age: 18, name: "Hello")
t.age = 20
t.name = "Logic"
t = ZGTeacher(age: 30, name: "Kody")
var t1 = ZGTeacher(age: 18, name: "Hello")
t1.age = 20
t1.name = "Logic"
t1 = ZGTeacher(age: 30, name: "Kody")
let s = ZGStudent(age: 18, name: "Hello")
s.age = 25
s.name = "Doman"
s = ZGStudent(age: 18, name: "Hello")
var s1 = ZGStudent(age: 18, name: "Hello")
s1.age = 25
s1.name = "Doman"
s1 = ZGStudent(age: 18, name: "Hello")
- 第25行代码:t.age因为age属性是被let修饰的,是一个常量存储属性,被赋值为18后,不可以再被更改
- 第26行代码: t.name因为name属性是被var修饰的,是一个变量存储属性,被赋值为"hello"后,可以被"Logic"这个值修改替换
- 第27行代码: t被let修饰并赋值为ZGTeacher(age: 18, name: "Hello")后,也是一个常量存储属性,不可以再次更改为ZGTeacher(age: 30, name: "Kody")
- 第30行代码:t1.age因为age属性是被let修饰的,是一个常量存储属性,被赋值为18后,不可以再被更改为20
- 第31行代码: t1这个类是被var修饰的,t1中的name属性也是被var修饰的,t1中的name属性可以被再次赋值
- 第32行代码:t1这个类是被var修饰的,可以被再次赋值
- 第35行代码:s结构体是被let修饰的,其中的age也是被let修饰的,被赋值为18后,不可以更改为25
- 第36行代码:s结构体是被let修饰的,其中的name虽然是被var修饰的,但只可以被赋值一次,被赋值为"Hello"后不可以被再次赋值
- 第37行代码:s结构体是被let修饰的,赋值一次后,不可以再次赋值
- 第40行代码:s1结构体中的age属性是被let修饰的,赋值18后,不可以再次赋值
- 第41行代码:s1结构体是被var修饰的,结构体中的name也是被var修饰的,被赋值为"Hello"后,也可以被再次赋值为"doman"
- 第42行代码:s1结构体是被var修饰的,可以被多次赋值
1.2 let和var的区别
1.2.1 从汇编的角度
通过上面的截图可以看到,常量和变量的存储没有明显的区别。
1.2.2 从SIL的角度
///@_hasStorage代表是存储属性
///@_hasInitialValue代表有初始值
///有get和set方法
@_hasStorage @_hasInitialValue var age: Int { get set }
///只有get方法
@_hasStorage @_hasInitialValue let x: Int { get }
代表var修饰的age是变量存储属性
,可以调用get方法获取值,也可以调用set方法赋值,而let 修饰的x是常量存储属性
,只可以调用get方法获取值,一旦被赋值后不可以再更改。
二、计算属性
存储的属性是最常见的,除了存储属性,类、结构体和枚举也能够定义计算属性
,计算属性并不存储值
,他们提供 getter
和setter
来修改和获取值
。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量
。于此同时我们书写计算属性时候必须包含类型
,因为编译器需要知道期望返回值是什么。
struct square {
///实例当中占据内存的
var width: Double
///本质是一个方法,不占据内存
var area: Double {
get {
return width * width
}
set {
self.width = newValue
}
}
}
通过上图可以发现square.area.setter,就是一个方法的静态调用,而不是对属性的存储。
下面我们看一下只读的计算属性和let 有什么区别
struct square {
var width: Double = 30
var area: Double {
get {
return width * width
}
}
let height: Double = 20
}
编译成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)
}
var area: Double { get }和@_hasStorage @_hasInitialValue let height: Double { get } 相同点是都有get方法,但它们的本质是不一样的。area是方法,height是let修饰的属性。
三、属性观察者
属性观察者会用来观察属性值的变化,一个 willSet
当属性将被改变调用,即使这个值与原有的值相同,而 didSet
在属性已经改变之后调用。它们的语法类似于 getter
和 setter
。
class Subject {
var subjectName: String = "" {
///系统默认生成的newValue
willSet {
print("subject will set value \(newValue)")
}
didSet {
print("subject will set value \(oldValue)")
}
// ///这里newBody=newValue,如果你不想用系统默认创建的值,可以自己定义一个别名
// willSet (newBody) {
// print("subject will set value \(newBody)")
// }
// ///这里oldBody=oldBody
// didSet(oldBody) {
// print("subject will set value \(oldBody)")
// }
}
}
let s = Subject()
s.subjectName = "Swift"
这里我们在使用属性观察器的时候,需要注意的一点是在初始化期间设置属性时不会调用 willSet
和 didSet
观察者;只有在为完全初始化的实例分配新值时才会调用它们。运行下面这段代码,你会发现当前并不会有任何的输出。
class Subject {
var subjectName: String = "" {
///系统默认生成的newValue
willSet {
print("subject will set value \(newValue)")
}
didSet {
print("subject will set value \(oldValue)")
}
}
init(subjectName: String) {
///初始化的操作
self.subjectName = subjectName
}
}
let s = Subject(subjectName: "Swift")
上面的属性观察者只是对存储属性起作用,如果我们想对计算属性起作用怎么办?很简单,只需将相关代码添加到属性的 setter。我们先来看这段代码:
class Square {
var width: Double
var area: Double {
get {
return width * width
}
set {
self.width = sqrt(newValue)
}
willSet {
print("area will set value \(newValue)")
}
didSet {
print("area has been changed \(oldValue)")
}
}
init(width: Double) {
self.width = width
}
}
对于计算属性的观察者:分为基本计算属性
和带有继承的计算属性
因为没有初始化方法,不需要添加willSet,didSet观察者,如果想被观察者访问到,只需要将观察者需要实现的代码添加到自己本身自带的setter方法里。
class ZGTeacher {
var age: Int {
willSet {
print("age will set value \(newValue)")
}
didSet {
print("age has been changed \(oldValue)")
}
}
var height: Double
init(_ age: Int, _ height: Double) {
self.age = age
self.height = height
}
}
class ZGPartTimeTeacher: ZGTeacher {
override var age: Int {
willSet {
print("override age will set value \(newValue)")
}
didSet {
print("override age has been changed \(oldValue)")
}
}
var subjectName: String
init(_ subjectName: String) {
self.subjectName = subjectName
super.init(18, 30.0)
self.age = 20
}
}
let t = ZGPartTimeTeacher("Swift")
打印结果如下:
override age will set value 20
age will set value 20
age has been changed 18
override age has been changed 18
可以得出以下结论:继承属性调用setter --> 调用自身willset --> 调用父类setter --> 父类调用自身willset --> 赋值 --> 父类调用自身didset --> 继承属性调用自身didset
四、延迟存储属性
- 延迟存储属性的初始值在其第一次使用时才进行计算。
- 用关键字
lazy
来标识一个延迟存储属性。
class Subject {
lazy var age: Int = 18
}
var s = Subject()
print(s.age)
print("end")
我们来看下lldb的对应打印
po s
<Subject: 0x101b32fc0>
(lldb) x/8g 0x101b32fc0
0x101b32fc0: 0x0000000100008160 0x0000000200000003
0x101b32fd0: 0x0000000000000000 0x0000000101b33101
0x101b32fe0: 0x0000000000000000 0x0000000000000000
0x101b32ff0: 0x0000000101b30006 0x0000000100000001
18
(lldb) po s
<Subject: 0x101b32fc0>
(lldb) x/8g 0x101b32fc0
0x101b32fc0: 0x0000000100008160 0x0000000400000003
0x101b32fd0: 0x0000000000000012 0x0000000101b33100
0x101b32fe0: 0x0000000000000000 0x0000000000000000
0x101b32ff0: 0x0000000101b30006 0x0000000100000001
这是一个存储属性,前16个字节存储我们的Metadata(0x0000000100008160)
和refro(0x0000000400000003)
16个字节后开始存储属性对应的值0x0000000000000000
过掉s.age断点值存储进去变为0x0000000000000012
,所以age被lazy
修饰后,所以它是在第一次访问之后才会对它进行初始化操作。
那么添加lazy
和不添加lazy
对我们的内存大小有没有影响哪?我们把代码编译成sil文件看一下。
class Subject {
lazy var age: Int { get set }
@_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
@objc deinit
init()
}
可以看到Int?
,意味着添加了lazy
以后,属性被修饰成了一个可选类型。
// variable initialization expression of Subject.$__lazy_storage_$_age
sil hidden [transparent] @$s4main7SubjectC21$__lazy_storage_$_age029_12232F587A4C5CD8B1EEDF696793G2FCLLSiSgvpfi : $@convention(thin) () -> Optional<Int> {
bb0:
%0 = enum $Optional<Int>, #Optional.none!enumelt // user: %1
return %0 : $Optional<Int> // id: %1
}
lazy_storage
初始化表达式中 给了一个枚举值Optional.none!enumelt
,可以看作OC中的nil
%18 = class_method %15 : $Subject, #Subject.age!getter : (Subject) -> () -> Int, $@convention(method) (@guaranteed Subject) -> Int // user: %19
Subject.age!getter调用age的getter方法, class_method
是一个vtable函数表
的调用。
// Subject.age.getter
sil hidden [lazy_getter] [noinline] @$s4main7SubjectC3ageSivg : $@convention(method) (@guaranteed Subject) -> Int {
// %0 "self" // users: %14, %2, %1
bb0(%0 : $Subject):
debug_value %0 : $Subject, let, name "self", argno 1 // id: %1
%2 = ref_element_addr %0 : $Subject, #Subject.$__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
switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6
Subject.age.setter方法首先访问我们的Subject.$__lazy_storage_$_age
地址ref_element_addr
,然后将这个地址的值给到我们的常量寄存器%4
, switch_enum %4
进行一个枚举模式的匹配,如果有值,走bb1
代码块,没值,就走bb2
代码块。
延迟存储属性的初始值在其第一次使用时才进行计算,相当于节省了我们的内存空间。我们的延迟存储属性并不是线程安全的,这点大家要注意。
五、类型属性
- 类型属性其实就是一个全局变量
class ZGTeacher {
static var age: Int = 18
}
类型属性的调用方式是 类名+属性,如上面代码调用ZGTeacher.age
我们转译成sil文件看一下上面的代码
class ZGTeacher {
@_hasStorage @_hasInitialValue static var age: Int { get set }
@objc deinit
init()
}
// one-time initialization token for age
sil_global private @$s4main9ZGTeacherC3age_Wz : $Builtin.Word
// static ZGTeacher.age
sil_global hidden @$s4main9ZGTeacherC3ageSivpZ : $Int
我们看sil文件代码,发现生成了一个token和一个static修饰的全局变量。
所以看到这里,我们应该可以理解static本质上就是一个全局变量。
- 类型属性只会被初始化一次
// ZGTeacher.age.unsafeMutableAddressor
sil hidden [global_init] @$s4main9ZGTeacherC3ageSivau : $@convention(thin) () -> Builtin.RawPointer {
bb0:
%0 = global_addr @$s4main9ZGTeacherC3age_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
%2 = function_ref @$s4main9ZGTeacherC3age_WZ : $@convention(c) () -> () // user: %3
%3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
%4 = global_addr @$s4main9ZGTeacherC3ageSivpZ : $*Int // user: %5
%5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
return %5 : $Builtin.RawPointer // id: %6
}
// one-time initialization function for age
sil private [global_init_once_fn] @$s4main9ZGTeacherC3age_WZ : $@convention(c) () -> () {
bb0:
alloc_global @$s4main9ZGTeacherC3ageSivpZ // id: %0
%1 = global_addr @$s4main9ZGTeacherC3ageSivpZ : $*Int // user: %4
%2 = integer_literal $Builtin.Int64, 18 // user: %3
%3 = struct $Int (%2 : $Builtin.Int64) // user: %4
store %3 to %1 : $*Int // id: %4
%5 = tuple () // user: %6
return %5 : $() // id: %6
}
builtin "once"
表示执行一次
我们降级成IR文件再来看一下
once_not_done: ; preds = %entry
call void @swift_once(i64* @"$s4main9ZGTeacherC3age_Wz", i8* bitcast (void ()* @"$s4main9ZGTeacherC3age_WZ" to i8*), i8* undef)
br label %once_done
}
这里调用了@swift_once
,那么@swift_once
是做什么的哪,我们到Swift源码搜一下。我们在once.cpp文件中看到如下代码:
void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
void *context) {
#ifdef SWIFT_STDLIB_SINGLE_THREADED_RUNTIME
if (! *predicate) {
*predicate = true;
fn(context);
}
#elif defined(__APPLE__)
dispatch_once_f(predicate, context, fn);
#elif defined(__CYGWIN__)
_swift_once_f(predicate, context, fn);
#else
std::call_once(*predicate, [fn, context]() { fn(context); });
#endif
可以看出,是调用了GCD,确保只被初始化一次。只初始化一次,就和我们的OC中的单例很像。
我们先来回顾一下OC单例:
+ (instancetype)sharedInstance {
static Test1 * t = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
t = [[Test1 alloc]init];
});
return t;
}
那么Swift中的单例又是怎么样的哪?
class ZGTest {
static let sharedInstance = ZGTest()
private init(){}
}
六、属性在Mach-o文件中的位置信息
在第一节课的过程中我们讲到了 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
//V-Table
}
上节课我们提到,我们的typeDescriptor
存储在__TEXT,__Swift_types
里,将自己项目的mach-o文件放到machoView应用里,可以看到:
fieldDescriptor
记录了当前的属性信息,其中fieldDescriptor
在源码中的结构如下:
struct FieldDescriptor {
MangledTypeName int32
Superclass int32
Kind uint16
FieldRecordSize uint16
NumFields uint32
FieldRecords [FieldRecord]
}
我们用 0xFFFFFF2C+0x00003F4C
得到0x100003E78
,这个就是typeDescriptor
在当前Mach-o文件中的地址,减去我们的虚拟内存地址0x100000000
,得到一个0x3E78
,我们可以直接定位到该地址
看一下这个
typeDescriptor
结构体,如果我们想要找到fieldDescriptor
地址,需要从当前地址偏移4个4字节。这个9C存储的是偏移信息,所以我们用
0x00003E88
加上偏移信息0x9C
得到0x3F24
这个地址0x3F24
就就代表了我们的fieldDescriptor
,而它后面的地址就是这个结构体存储的内容。
其中 NumFields
代表当前有多少个属性, FieldRecords
记录了每个属性的信息,FieldRecords
的结构体如下:
struct FieldRecord {
Flags uint32
MangledTypeName int32
FieldName int32
}
我们想要拿到FieldRecords
的地址,只需要0x3F24
偏移4个4字节。
我们想要拿到
FieldRecords
结构体中的FieldName
来验证一下拿到的对不对,那么我们只需要偏移2个4字节。用0x00003F34 + 0x8
得到0x00003F3C
再加上0xFFFFFFDD
,得到0x100003F19
,减去我们的虚拟内存地址0x100000000
,得到0x3F19
是的,这里我们拿到并验证了age和age1两个属性存放的地址。