属性
存储属性
定义: 存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性 想由 var 关键字引入)要么是常量存储属性(由 let 关键字引入)。存储属性这里没有什么特 别要强调的,因为随处可见
class LGTeacher {
var age: Int
var name: String
}
比如这里的 age 和 name 就是我们所说的存储属性,这里我们需要加以区分的是 let 和 var 两者的区别:从定义上: let 用来声明常量,常量的值一旦设置好便不能再被更改; var 用来声明变量,变量的值可以在将来设置为不同的值。
我们来更直观的认识一下let 和var
class MJYTeacher{
let age: Int
var name: String
init(age: Int, name: String){
self.age = age
self.name = name
}
}
struct MJYStudent{
let age: Int
var name: String
}
相信以上说明已经对let和var有了比较清晰的认识接下来我们从汇编的角度来认识一下let和var
汇编角度认识let和var
var age = 18
let x = 20
从汇编上看这个两个值并没有什么不同都是一个立即数
我们也可以看一下age和x的存储上会不会有什么不同
可以看到它们是紧挨着相差8个字节的存储,也并无什么不同
SIL角度认识let和var
我们可以看到age和x都是存储属性,age拥有get和set方法,x只有get方法。
总结:可以推测出let和var本身也是一种语法糖,为属性自动生成相对应的set、get方法。
计算属性
存储的属性是最常见的,除了存储属性,类、结构体和枚举也能够定义计算属性,计算属性并不存储值,他们提供 getter 和 setter 来修改和获取值。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量。于此同时我们书写计算属性时候必须包含类型,因为编译器需 要知道期望返回值是什么。
struct square{
// 实例当中占据内存
var width: Double
// 不占用内存空间
var area: Double{
get{
return width * width
}
set{
self.width = newValue
}
}
}
var s = square(width: 10.0)
s.area = 30
s.area的本质是调用方法,我们将以上代码转换成sil文件
可以看到area是没有出存储属性的,但是width是有存储属性的,但是他们都有get和set方法。顾名思义可得,他们之间的最大差别就是一个记录存储值,一个只仅仅调用get、set并不存储该属性的值。
变换一下代码如果我们将存储属性area的set方法私有化,并且定义一个let修饰的height,我们将这段代码转换成sil文件看一下这两个有什么区别吗?
struct square{
var width: Double = 30.0
private(set) var area: Double = 40
let height: Double = 20.0
func test(){
self.area
}
}
var s = square(width: 10.0)
我们可以看到area是一个存储属性,只是我们在结构体外部访问不到set方法,看起来是只读的。
总结: 计算属性的本质就是set和get方法
属性观察者
属性观察者会观察用来观察属性值的变化,一个 willSet 当属性将被改变调用,即使这个值与 原有的值相同,而 didSet 在属性已经改变之后调用。它们的语法类似于 getter 和 setter。
class SubectName{
var subjectName: String = ""{
willSet{
print("subjectName will set value (newValue)")
}
didSet{
print("subjectName has been changed (oldValue)")
}
}
}
let s = SubectName()
s.subjectName = "Swift"
将以上代码转换成sil文件
subjectName是一个存储属性,在subjectName的set方法中我们可以看到该方法调用了willset和didiset方法,这两个方法是有set方法触发的
如果是init方法中的赋值,不会触发willset和didiset方法,那可以猜测压根没有调用set方法。
总结: 这里我们在使用属性观察器的时候,需要注意的一点是在初始化期间设置属性时不会调用 willSet 和 didSet 观察者;只有在为完全初始化的实例分配新值时才会调用它们。
上面的属性观察者只是对存储属性起作用,如果我们想对计算属性起作用怎么办?很简单,只需 将相关代码添加到属性的 setter。
以上代码是有问题的,因为没有必要提供,直接在set方法里面进行赋值前后进行一个观察就好了,没有必要单独提供。
直接这样就行了,如果有继承关系的话在子类赋值的话,会先调用自己的willset,再调用父类的willset 然后复制,在调父类的didset,最后才是自己的didset。
总结: 属性观察者和计算属性不能共存,观察者是在set方法中触发的
延迟存储属性
class Subject{
lazy var age:Int = 18
}
var s = Subject()
print(s.age)
在var s = Subject()处增加一个断点,查看一下age的值
可以看到 lazy 修饰的的age在初始化的时候值是一个nil
同样可以查看该位置的存储显示是0(0x100732e90位置)
访问过age的值以后该位置已经变成12(16进制)(0x100732e90位置)
sil文件查看一下结构
可以看到lazy修饰的age已经变成了一个可选类型,可选类型默认初始化为nil。延迟存储属性并不是线程安全的。
总结:
- 延迟存储属性就是将属性变成一个可选类型。
- 用关键字 lazy 来标识
- 一个延迟存储属性 延迟存储属性的初始值在其第一次使用时才进行计算。
类型属性
class Subject{
static var age:Int = 18
}
Subject.age = 20
用static修饰的就是类型属性,查看一下改代码转换成的sil
static本质就是一个全局变量,只会被初始化一次,但是是可以修改的,通过类名来修改age的值,只能初始化一次,但是可以多次修改。
如果在Swift中创建一个单例呢?
class MJYTeacher{
static let sharedInstance = MJYTeacher()
// 将初始化器私有化
private init(){
}
}
私有化初始化器,只能调用我们给定的sharedInstance,let的修饰说明该类的实例地址只有一份不可修改,但是sharedInstance是能被继承的如果想要不被继承 该类需要用final修饰。
class Subject{
static var age: Int = 18
static func test(){
print("test")
}
}
Subject.test()
类型方法同理 用static修饰的方法也只能用类名调用,我们在Subject.test()处打一个断点,查看一下汇编
可以看到callq 一个地址,是直接的地址调用,属于静态派发。
总结: 类型属性其实就是一个全局变量 ; 类型属性只会被初始化一次
属性在Mahco文件的位置信息
在类与结构体二中我们知道了metadata的 typeDescriptor在Mahco的 __swifts_types中,并且我们知道了desc中的v-table的相关信息,接下来我们需要认识一下typeDescriptor 中的 fieldDescripto
我们先来看一下TargetClassDescriptor的结构
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]
}
在以上认知的基础上我们将以下代码转换为marcho文件
class MJYTeacher{
var age = 18
var age1 = 20
}
首先定位到__swift5_types,这个是我们的metadata中typeDescriptor的所在位置 我们用FFFFFEC0 + 3F20 = 0x100003DE0,再减去marcho的虚拟内存基地址得到3DE0这个地址接下来我们在section中寻找到该地址
根据desc结构体偏移4个字节到0x00000108,可得fieldDescriptor的地址是0x00000108,该值记录的是偏移量 0x00000108+ 3DE0 = 0x3EF8。这个地址我们在__swift5_filedmd中
我们需要找到fieldDescriptor中的FieldRecords,根据上方结构我们可以知道我们需要偏移4个4字节,那我们定位到00003F08这个地址,在__swift5_fieldmd中找到该地址那里面存储的是FieldRecords这个结构体
struct FieldRecord{
Flags uint32
MangledTypeName int32
FieldName int32
}
根据以上结构我们继续偏移2个4字节 得到偏移后地址中的值是0xFFFFFFDD也是偏移量所以:0x3F08+0x8+0xFFFFFFDD = 0x100003EED,去掉marcho中的虚拟内存基地址得到0x3EED,继续快速定位到0x3EED这个地址我们在__swift5_refstr中找到
可以看到age、age1在此处
总结:
- _swift5_types存储着我们的typeDescriptor
- __swift5_filedmd存储这我们的fieldDescriptor
- __swift5_refstr存储这我们的属性名称
lg:swift有内存独占的概念
lg:class修饰的方法 class修饰的方法是类方法,static修饰的是类型方法
class MJYTeacher{
class func test(){
print("test")
}
static func test1(){
print("test")
}
}
MJYTeacher.test()
MJYTeacher.test1()
汇编可以看到callq直接一个地址的调用但是在sil文件中我们可以看到class修饰的方法还是会注册到v-table中的
class关键字能修饰我们的值类型吗?
答曰:不能!!!
class修饰的方法能被子类重写吗?
答曰:可以,可能大概是因为class修饰的方法还是在v-table中,在vtable中就能被继承。