结构体和类
结构体和类都是我们经常使用的类型,那么它们究竟有什么区别
class Person {
var height = 180
}
struct People {
var age : Int
}
对于上面一个类一个结构体的代码得到SIL
可以看到结构体直接提供了属性的初始化方法,而类并没有主动提供,但是如果结构体成员变量给出默认值,系统则不会提供合成的初始化方法
那么之前已经分析过类的初始化过程,类是使用从堆上分配的内存空间,那么现在同样来分析一下结构体的初始化过程,还是老样子先写一段很简单的代码然后进入其SIL文件来分析
struct People {
var age : Int = 18
var height : Int = 180
}
var p = People()
结构体的初始化过程相比类就简单了许多,直接进入了
init()方法,没有发现分配内存空间的痕迹,直接进入init()内部
从文档里可以知道,结构体的初始化方法直接是使用了
alloc_stack分配到了在栈上的一块内存空间
结构体初始化流程:
- 使用了
alloc_stack分配到了在栈上的一块内存空间 - 将
18作为初始化为Int类型的结构体的默认值初始化结构体并存入3% struct_element_addr得到结构体People中的age在内存中的地址,并将3%的值存入- 同理将
180存入height - 返回结构体
值类型和引用类型
值类型
我们一直听到的是,类是引用类型,结构体是值类型,这里不再赘述随处可查到的何时使用类或者何时使用结构体,接下来就来看看它们究竟有什么区别。同样从一段代码切入:
var a = 10
var b = a
a += 1
结果a=11``b=10,赋值操作明显就是值拷贝,那么这里可以说变量不仅是个名字如:a,b,c...这么简单,它表示的是内存中的一个位置,在这个位置中包含了某个值。那么如上代码所示,我们使用a来指向内存中的一个位置,这个位置中有一个类型为Int的值10,b指向了内存中的另一个位置同样这个位置有一个类型为Int的值10,所以a指向内存中的值进行了加一操作后重新写回a指向的内存,而b指向的内存并没有任何操作。现在来验证一下
正如我们上面分析的一样,a和b是指向不同的内存空间,并且直接将值存入其中,所以修改了其中一方并不影响另一方。
结论
值类型的特征:在变量的背后,值直接保存在变量指向的内存位置。
引用类型
class Person {
var age: Int = 18
}
var p = Person()
var p1 = p
p1.age = 10
从上面类的这段代码及地下调试结果图来看,p和p1指向同一块内存地址0x100727ef0,所以改变p1的属性值同样会导致p内部的改变,这就是引用类型的本质:变量不含有"事物"本身,而是持有一个对"事物"的引用。其他变量也可以含有对同一个实例的引用,并可以通过任意一个指向它的变量对其做修改。
mutating
struct Person {
var age: Int = 18
func changeAge(_ modifyAge: Int){
//age = modifyAge
}
}
如果在上述代码中的结构体内函数想修改其自身的属性,编译器会报错,一定要让加上关键字mutating,为了弄清为什么会这样,来看看其SIL代码有何不同
这里可以看到函数
changeAge会包含一个隐藏参数self回指到它本身,同时可以看到有一个关键字let意味着不可变,所以如果想修改自身属性是不允许的,那么加上mutating会有什么不同呢
Person加上了关键字@inout- 对
self不是直接取值了,而是改成了取地址 - 使用
var来修饰self,意味此时可进行修改
总结
mutating关键字的本质其实是inout,类并没有也不需要可变方法,在类的方法中,self表现得就像一个用let声明的变量。不过虽然我们不能对self重新赋值,但可以通过使用self来修改其所指向的实例的属性,同时只需要该属性是用var来修饰的。
延迟存储属性
先来看一段简单代码
class Person {
lazy var height = 180
}
var p = Person()
var h = p.height
p.height = 200
那么在属性前加上关键字lazy到底有什么影响呢,使用方法class_getInstanceSize(Person.self)发现结果是32,根据之前对实例对象的分析,发现与现在得到的结果居然不匹配,只好进入SIL文件中,看看能否得到答案
SIL文件分析
可以看到这里有个
__lazy_storage_$_height变量,同时后面有个?,那么这里是否代表它是一个可选类型呢,在我们创建Person的实例对象后,使用了h来接收实例对象的height值
那么其实就是调用了
height的get方法
在
get方法中大致的流程:
- 拿到
__lazy_storage_$_height的地址 - 得到
__lazy_storage_$_height的值 - 对该值进行判断,如果有值走入分支
bb1,如果为空则走入bb2那么我们此时应当是没有值的,则走入bb2的流程: - 将
180赋值给10% - 开始构建
Int结构体,并将10%中的值作为初始值 - 拿到
__lazy_storage_$_height的地址赋值给14% - 把初始值
180给到__lazy_storage_$_height
get方法中发生了什么我们已经有了大致的了解,接下来看看对应的set方法中是如何呢
set方法第一个参数是200第二个参数是Person的实例对象
其中流程大致与get方法中一样,且仅仅是更为简单的赋值操作而已。从这两个方法中不难看出__lazy_storage_$_height这个类型其实为Optional<Int>,所以它的内存结构是如何呢,通过MemoryLayout方法可以得到Optional<Int>实际占有的内存为9字节,那么一开始的问题得解,16+9=25,并且通过字节对齐后可得32字节。
总结
⽤lazy修饰的存储属性:
- 延迟存储属性必须有默认初始值
- 延迟存储在第⼀次访问之前是
nil - 延迟存储属性是非线程安全的
- 延迟存储属性对实例对象⼤⼩有影响
类型属性
class Person {
static var height = 180
}
let h = Person.height
Person.height = 200
同样,直接切入SIL文件
首先可以看到声明在
Person中的height属性变成了一个全局变量
在用
h来接收类型属性height的过程中会调用unsafeMutableAddressor
这里会调用到一个全局的初始化方法
globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0
显然这个方法就是将初始值赋值给前面说
height的全局变量,现在通过lldb调试回到unsafeMutableAddressor这个方法中,看看能不能找到其他的符号
这里发现的
swift_once应当与SIL文件中的builtin "once" 相对应,看到这里是不是有点单例的意思在里面,直接去Swift源码中寻找
看到这里轻松得到类型属性
height只会被初始化一次的结论。
总结
用static修饰的类型属性:
- 必须有一个默认初始值
- 类型属性是线程安全的,因为只会被初始化一次
结构体的方法调用
struct Person {
func study(){
print("study")
}
}
var p = Person()
p.study()
结构体中的函数调用是静态调用,函数地址在编译链接之后就已经确定了,方法并不存储在结构体中。同时可以注意到后面有一串文字swift_2.Person.study() -> (),这个就是符号,符号存在于符号表中,但符号表又不直接存储符号,符号表存储的是相对于字符串表的偏移,符号都以字符串的形式存储于字符串表中。
同时存于字符串表中的符号是进行过命名重整的(tip:swift通过复杂的命名重整规则来确保swift可以通过不同的参数来重载同一个函数)可以在mach-o文件中找到
使用以下命令:
nm+当前mach-o文件地址得到当前符号表后在其中找到字符串进行xcrun swift-demangle s7swift_26PersonV5studyyyF解码便可得到真实的符号。