Swift进阶-属性
Swift进阶-类与结构体
Swift-函数派发
Swift进阶-属性
Swift进阶-指针
Swift进阶-内存管理
Swift进阶-TargetClassMetadata和TargetStructMetadata数据结构源码分析
Swift进阶-Mirror解析
Swift进阶-闭包
Swift进阶-协议
Swift进阶-泛型
Swift进阶-String源码解析
Swift进阶-Array源码解析
一、存储属性
存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性 (由 var 关键字引入)要么是常量存储属性(由 let 关键字引入)。
class Teacher {
let age: Int
var name: String
init(_ age: Int, _ name: String) {
self.age = age
self.name = name
}
}
struct Student {
let age: Int
var name: String
}
let 和 var 的区别:
从定义上:
- let 用来声明常量,常量的值一旦设置好便不能再被更改;
- var 用来声明变量,变量的值可以在将来设置为不同的值。
汇编角度:
看不出有什么不同的,无非把值赋值给对应的寄存器,大可自行调试看看。
开启汇编模式:Debug -> Debug Workflow -> Always Show Disassembly
var age = 18
let name = 20
print("断点打在这,运行")
从 SIL的角度:
swiftc xxx.swift -emit-sil // 生成sil(已优化)
sil的常量变量声明部分
存储属性在编译的时候,编译器默认会合成get/set
方式,而我们访问/赋值 存储属性的时候,实际上就是调用get/set
。
let
声明的属性默认不会提供setter
二、计算属性
类、结构体和枚举也能够定义计算属性,计算属性并不存储值,他们提供 getter
和 setter
来修改和获取值。
计算属性必须定义为变量。计算属性时候必须包含类型,因为编译器需 要知道期望返回值是什么。
struct Square {
var width: Double // 存储属性,在实例中Double占据8字节
var area: Double{ // 计算属性,不占据内存空间
get {
return width * width
}
set {
self.width = newValue
}
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
var s = Square(width: 10.0)
s.area = 30.0 //断点打在着,看汇编
}
}
看看汇编给计算属性赋值是个什么过程
image.png可以看到这是一个静态调用,按住 control
点击下一步进去
给计算属性赋值,其实是调用setter
来看看sil的Square声明
image.png计算属性area的本质就是getter和setter,不一样的,不占用实例的内存。
以下这种声明是存储属性:
struct Square {
var width: Double
private(set) var area: Double = 10.0
}
sil
虽然有set方法,但是超过了类作用域,外界是访问不到的。
三、属性观察者
属性观察者会观察用来观察属性值的变化:
willSet
当属性将被改变调用,即使这个值与 原有的值相同;
didSet
在属性已经改变之后调用。
class SubjectName{
// 它是存储属性
var subjectName: String = "" {
willSet{
print("subjectName will set value \(newValue)")
}
didSet{
print("subjectName has been changed \(oldValue)")
}
}
}
class ViewController: UIViewController{
override func viewDidLoad() {
let n = SubjectName()
n.subjectName = "wj"
}
}
sil
在赋值的前后去调用willSet
和didSet
方法。
class SubjectName{
// 它是存储属性
var subjectName: String = "" {
willSet{
print("subjectName will set value \(newValue)")
}
didSet{
print("subjectName has been changed \(oldValue)")
}
}
init(_ subjectName: String) {
self.subjectName = subjectName
}
}
sil
在初始化期间设置属性时不会调用 willSet
和 didSet
观察者。
继承关系demo:
class Teacher {
var name: String {
willSet {
print("will set newValue: \(newValue)")
}
didSet {
print("did set oldValue \(oldValue)")
}
}
init(name: String) {
self.name = name
}
}
class ParTimeTeacher: Teacher {
override var name: String {
willSet {
print("override will set newValue: \(newValue)")
}
didSet {
print("override did set oldValue \(oldValue)")
}
}
override init(name: String) {
super.init(name: name)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
let t = ParTimeTeacher(name: "未知")
t.name = "林老师"
}
}
打印结果:
override will set newValue: 林老师 // 子类will set
will set newValue: 林老师 // 父类will set
did set oldValue 未知 // 父类 did set
override did set oldValue 未知 // 子类did set
sil
实际上ParTimeTeacher
实例调用getter/setter
实际就是调用父类Teacher
的getter/setter
四、延迟存储属性
- 必须有初始值
- 延迟存储属性的初始值是在它第一次被使用时才进行计算;
- 用关键字 lazy 来标识一个延迟存储属性。
在没有第一次访问延迟存储属性时,它的值是0
image.png访问过一次后,它才会有值。
再去看看sil:
所以苹果做到懒加载属性第一次访问前没有值,访问后就有值?
本质就是在编译后生成一个可选型存储属性
在初始化的时候给的是 nil
getter拿到生成的属性 $__lazy_storage_$_name
的地址,然后去switch:如果有值,走bb1的代码块;如果没有值走bb2代码块
bb2没有加载过:去构建初始值,给到变量$__lazy_storage_$_name
bb1已加载过:已经有值了,直接给他返回出去
没什么好说的,给属性$__lazy_storage_$_name
赋值
五、类型属性
- 类型属性本质上就是一个全局变量
- 类型属性只会被初始化一次
class Teacher {
static var name: String = "未知"
}
class ViewController: UIViewController{
override func viewDidLoad() {
Teacher.name = "林老师"
}
}
sil
编译后的sil里多了两个全局变量
一个是@$s14ViewController7TeacherC4name_Wz
- token
一个是@$s14ViewController7TeacherC4nameSSvpZ
- Teacher.name
看看viewDidLoad
里面
做了一个全局变量内存地址的转换,它是怎么转换的呢?
搜一下这个函数名 @$s14ViewController7TeacherC4nameSSvau
而builtin "once"
在sil降级之后,其实底层调用的就是swift_once
,而swift_once
的源码底层调用的就是GCD的dispatch_once
单例的写法
class Teacher {
static let share = Teacher()
private init() {}
}
单例sil
六、属性在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
是记录类的描述:
struct TargetClassDescriptor{
var flags: UInt32
var parent: UInt32
var name: Int32 // class/struct/enum 的名称
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 (methods)
}
其中 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 // 属性名称
}
了解完数据结构之后,现在从Mach-O来分析属性在Mach-O文件的位置信息
在 main.swift
声明一个对象
class Teacher {
var age = 18
var age1 = 20
}
编译后,找到其可执行文件exe,将其拖拽到 MachOView
软件中
在data区找到__TEXT,__swift5_types
这是记录所有的 struct/enum/类的Descriptor
的信息,现在我们只有一个Teacher类
可以拿到 Teacher类的Descriptor
在 Mach-O
里的位置:
0xFFFFFE28 + 0x3ED8 = 0x100003D00
所以Dscriptor在mach-o的data区的偏移量,需要再减去虚拟内存地址:
0x100003D00 - 0x100000000 = 0x3D00
然后再data区找到 __TEXT,__const
里边,去找0x3D00的位置:
接下来要找到 fieldDescriptor
它的地址,因为fieldDescriptor
在TargetClassDescriptor
的数据结构前面还有4个UInt32的成员:
struct TargetClassDescriptor{
var flags: UInt32
var parent: UInt32
var name: Int32 // class/struct/enum 的名称
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 (methods)
}
所以还需要向后偏移4个4字节:
fieldDescriptor在Mach-O的偏移信息得到的位置也就是fieldDescriptor
在Mach-O
的偏移信息
求出fieldDescriptor
在Mach-O
的的信息:
0x3D10 + 0x1A0 = 0x3EB0
而属性存放在data区的 __TEXT,__swift5_fieldmd
,并在上面找到0x3EB0:
从0x3EB0开始后面的就是 FieldDescriptor
属性描述的成员内容,而我要找到 FieldRecords [FieldRecord]
struct FieldDescriptor {
MangledTypeName int32 // 混写后的类型名称
Superclass int32
Kind uint16
FieldRecordSize uint16
NumFields uint32 // 属性个数
FieldRecords [FieldRecord] // 记录了每个属性的信息
}
就要往后偏移4个4字节(其中有2个uint16,3个int32):
FieldRecords因为我们只有一个Teacher类,这后面的连续的存储空间就是 FieldRecords数组
的内容:
而 FieldRecord
的数据结构:
struct FieldRecord{
Flags uint32 // 标志位
MangledTypeName int32 // 属性的类型名称
FieldName int32 // 属性名称
}
`FieldRecord` 对应成员的位置
我这里画出找到 第一个FieldRecord
对应成员的位置
我拿到 第一个FieldRecord
的FieldName属性名称
在Mach-O上的信息
0x3EC0 + 0x8 + 0xFFFFFFDF = 0x100003EA7
需要减去虚拟内存地址:
0x100003EA7 - 0x100000000 = 0x3EA7
再去找data区中的 __TEXT,__swift5_reflstr
找到0x3EA7:
直接可以找到Teacher类的第一个成员变量age的16进制