一、存储属性
存储属性是一个作为特定类和结构体实例一部分的常量或变量
。存储属性要么是变量存储属性
(由 var
关键字引入)要么是常量存储属性
(由 let
关键字引入)。存储属性这里没有什么特 别要强调的,因为随处可⻅
class LGTeacher{
var age: Int
var name: String
}
比如这里的 age
和 name
就是我们所说的存储属性,这里我们需要加以区分的是 let
和 var
两者的区别: let
用来声明常量
,常量的值一旦设置好便不能再被更改
; var
用来声明变量
,变量的值可以在将来设置为不同的值
。
1 let 和 var 的区别:
var age = 18
let x = 20
1.1 汇编的⻆度分析
就是无论是let
还是 var
,从汇编来看本质上let
和var
都是地址,两者无任何区别
1.2 SIL的⻆度分析
@_hasStorage @_hasInitialValue var age: Int { get set }
@_hasStorage @_hasInitialValue let x: Int { get }
var x : 只有get方法
都是存储属性,都有初始值
默认存储属性都很合成get/set
方法 存储属性访问是get,赋值是set,let
没有生成set
方法; 使用的时候不会更改值用let修饰,会更改值用var。
二、计算属性(Computed Property
)
存储的属性是最常⻅的,除了存储属性,类、结构体和枚举也能够定义计算属性,计算属性并不 存储值,他们提供 getter
和 setter
来修改和获取值。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量
。于此同时我们书写计算属性时候必须包含类型,因为编译器需要知道期望返回值是什么。
struct square{
// 实例中占据内存 8 字节
var width: Double
var area: Double {
get{
return width * width
}
set{
self.width = newValue
}
}
}
2.1 只读计算属性
无
set
,只有get
- 模式一:
struct square{
var width: Double = 30
var area: Double {
get{
return width * width
}
}
}
area对于内部和外部都不能进行赋值,只能读取数据
- 模式二:
struct square{
var width: Double = 30
private(set) var area: Double = 40
func test() {
self.area
}
}
SIL分析
struct square {
@_hasStorage @_hasInitialValue var width: Double { get set }
@_hasStorage @_hasInitialValue private(set) var area: Double { get set }
func test()
init()
init(width: Double = 30, area: Double = 40)
}
对于外部来说,只有get
属性,不能赋值;内部即可读取也可赋值
三、属性观察者
3.1 观察者 willSet && didSet
属性观察者会观察用来观察属性值的变化,一个 willSet
当属性将被改变调用,即使这个值与 原有的值相同,而 didSet
在属性已经改变之后调用。它们的语法类似于 getter
和 setter
。
class SubjectName {
var subjectName: String = "" {
willSet {
print("subjectName will set value \(newValue)")
}
didSet{
print("subjectName has been chaged \(oldValue)")
}
}
}
let s = SubjectName()
s.subjectName = "Swift"
SIL分析
3.2 观察者初始化操作
这里我们在使用属性观察器的时候,需要注意的一点是在初始化期间设置属性时不会调用 willSet
和 didSet
观察者;只有在为完全初始化的实例分配新值时才会调用它们。运行下面这段代码,你会发现当前并不会有任何的输出。
class SubjectName {
var subjectName: String = "" {
willSet {
print("subjectName will set value \(newValue)")
}
didSet{
print("subjectName has been chaged \(oldValue)")
}
}
init(subjectName: String) {
// 初始化操作
self.subjectName = subjectName
}
}
let s = SubjectName(subjectName: "Swift 进阶")
SIL
分析
初始化,直接将值从内存中拷贝到该属性
3.3 观察者 -> 计算属性
只需将相关代码添加到属性的
setter
不需要写观察属性,多此一举,如下示例:
3.4 观察者 -> 继承
先写一个LGTeacher
类,然后继承LGTeacher
这个类,改变继承类的值
class LGTeacher {
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 LGParTimeTeacher: LGTeacher {
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, 180.0)
self.age = 20
}
}
let t = LGParTimeTeacher("Swift")
执行结果:
override age will set value 20
age will set value 20
age has been changed 18
override age has been changed 18
Program ended with exit code: 0
可以得出调用顺序为:
override
willSet(子类)
->willSet(父类)
->didSet(父类)
-> overridedidSet(子类)
SIL分析:
四、延迟存储属性
- 延迟存储属性的初始值在其第一次使用时才进行计算。
- 用关键字
lazy
来标识一个延迟存储属性
class Subject {
lazy var age: Int = 18
}
var s = Subject()
print(s.age)
print("end")
LLDB
调试分析:
如上图所示:未使用
到已使用
由0x0000000000000000
变成了0x0000000000000012
,所以第一次使用时才进行计算。
SIL
分析:
初始化时,lazy
修饰,并且它是可选值,默认未nil
;final
修饰,不能被重写。
观察Subject.age.getter
的实现
先访问
__lazy_storage_
的地址
把内存地址的值给寄存器%4
枚举的模式匹配,有值走bb1
的代码块,没有值走bb2
的代码块
bb2
模块,没有值先进行取地址赋值操作
bb1
模块,直接将原有的值给返回出去
lazy
修饰是可选值,第一次使用的时候才会把值赋值给属性
五、类型属性
5.1 类型属性
- 类型属性其实就是一个全局变量
- 类型属性只会被初始化一次
class LGTeacher{
static var age: Int = 18
}
LGTeacher.age = 30
SIL分析
// one-time initialization token for age
sil_global private @$s4main9LGTeacherC3age_Wz : $Builtin.Word
// static LGTeacher.age
sil_global hidden @$s4main9LGTeacherC3ageSivpZ : $Int
age
变成了全局变量,全局声明了age变量;static
本质上就是全局变量
5.2 单例模式
class LGTeacher{
static let sharedInstance = LGTeacher()
private init(){}
}
初始化私有化,外部只能通过sharedInstance
来进行访问
六、属性在Mahco文件的位置信息
6.1 属性的信息结构分析
第一篇文章,知道了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
}
fieldDescriptor
记录了当前的属性信息,其中 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
}
Flags:标志位;MangledTypeName:当前属性的类型信息;FieldName属性名称
6.2 Mach-o文件查找属性信息
定义一个属性结构,编译生成Mach-o文件
class LGTeacher {
var age = 18
var age1 = 20
}
如何查找Mach-o文件?
- 先把工程Products目录显示出来
- 用MachOView打开
Mach-o
可执行文件.
找到Descriptor
在Mach-o
文件中的地址,相加后在减去0x100000000
(虚拟内存的基地址),之后得到一个新地址: 0x3D74
0x3EFC + 0xFFFFFE78 = 0x100003D74
0x100003D74 - 0x100000000 = 0x3D74
0x3D74
在Mach-o的文件的位置如下
0x3D74
在Mach-o文件const的位置地址为50 00 00 80
, 根据Descriptor
类型为TargetClassDescriptor
类,找到fieldDescriptor
需要偏移4个成员大小(4个4字节).
50 01 00 00
为fieldDescriptor
的偏移信息,要找到fieldDescriptor
的具体地址信息,
需要加上偏移信息,具体计算流程如下(小端模式,需从右往左读):
0x3D80 + 0x4 + 0x0150 = 0x3ED4
根据计算结果0x3ED4
,在Mach-O文件的位置如下:
0x3ED4
为 FieldDescriptor
的首地址,要找到FieldRecords
的具体地址,需要偏移FieldRecords
之前的成员属性大小;MangledTypeName 2字节
、Superclass 2字节
、Kind 2字节
、FieldRecordSize 4字节
、NumFields 4字节
FieldName
的地址计算流程:
0x3EE4 + 0x4 + 0x4 + 0xFFFFDF = 0x100003ECB
// 减去 虚拟内存的基地址
0x100003ECB - 0x100000000 = 0x3ECB
计算结果为:0x3ECB
,在Mach-o的位置如下:
如图,0x3ECB
在Mach-o
文件的reflStr
的位置
61 67 65 00
: age61 67 65 31
: age1