结构体
在 Swift 标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分
比如Bool、Int、Double、 String、Array、Dictionary等常见类型都是结构体
struct Date {
var year: Int
var month: Int
var day: Int
}
var date = Date(year: 2019, month: 6, day: 23)
所有的结构体都有一个编译器自动生成的初始化器(initializer,初始化方法、构造器、构造方法)
在第⑥行调用的,可以传入所有成员值,用以初始化所有成员(存储属性,Stored Property)。
结构体的初始化器
编译器会根据情况为所有结构体类型自动生成初始化器(initializer)
,有可能会生成多个初始化器.生成初始化器的原则是:保证所有成员都有值
.
比如:
struct TestStruct{
var a: Int
var b: Int
}
// 编译器自动生成了传入a,b的初始化器
let s = TestStruct(a: 10, b: 20)
如果结构体的成员有初始值,那么编译器会给结构体生成多个初始化器.
比如只有一个成员有初始值:
struct TestStruct{
var a: Int = 0
var b: Int
}
// 生成了两个初始化器
let s = TestStruct(a: 10, b: 20)
let s2 = TestStruct(b: 20)
两个成员都有初始化器:
struct TestStruct{
var a: Int = 0
var b: Int = 0
}
//生成4个初始化器
let s = TestStruct(a: 10, b: 20)
let s2 = TestStruct(b: 20)
let s3 = TestStruct(a: 10)
let s4 = TestStruct()
自定义初始化器
我们也可以给结构体自定义初始化器,但是一旦自定义初始化器后,编译器就不会再自动生成初始化器:
自定义初始化器后,只有自定义的初始化器有效,系统不会再生成初始化器
其实我们自定义的初始化器和系统自动生成的初始化器,本质是一模一样的,他们的底层汇编都是一样的.
结构体内存
结构体的内存和有关联值的枚举类型很相似:
func testStruct() {
struct Point {
var x = 10
var y = 20
var b = true
}
var p = Point(x: 11, y: 22, b: false)
p = Point(x: 22)
print(Mems.memStr(ofVal: &p))
print(MemoryLayout.size)
print(MemoryLayout.stride)
print(MemoryLayout.alignment)
}
testStruct()
0x0000000000000016 0x0000000000000014 0x0000000000000001
17
24
8
Class 类
类的定义
类的定义和结构体很相似,但是==如果类的成员没有初始值,编译器不会为类生成初始化器==:
- 如果类的成员都有初始值,编译器才会为类生成无参初始化器:
- 成员的初始化是在这个初始化器中完成的
func testClass() {
class Point {
var x = 10
var y = 20
var b = true
func test() {
}
}
let p = Point()
p.test()
}
testClass()
类和结构体的区别
从上面可以看到结构体和类真的是非常的相似,那么结构体和类有什么区别呢?
类型不同,它们的本质区别是:结构体是值类型,类是引用类型
//类
func test(){
class Person{
var name:String
var age: Int
init(){
self.name = "Jack"
self.age = 18
}
}
let person = Person()
//结构体
struct Point {
let x: Int
let y: Int
}
let point = Point(x: 10, y: 20)
}
如上所示的代码,因为point 和 person
都是在test()
函数内部定义的.所以它们的内存布局如下:
解读:变量`point , person`的内存空间都在栈空间.不同的是,`Struct`的数据也存储在变量的内存中.而`Class`类型的变量内存中存储的是一个地址,一个指向堆空间类对象的内存地址.`Class`类型的数据都存储在堆空间中.
在let point = Point(x: 10, y: 20)
打断点,可以看到Point
的初始化代码的汇编简洁如下,根本就没有调用malloc
代码:
我们可以打个断点,看看point
变量和 person
变量的内存是不是如上图分析的那样,要搞清楚变量有没有分配堆空间内存,一个很重要的指标就是是否调用了malloc
方法,因为这个方法是向堆空间申请内存的.我们在let person = Person()
打上断点,然后一步步执行,会发现最终会调用malloc
方法:
在Swift中,创建类的实例对象,要向堆空间申请内存,大概流程如下
Class.__allocating_init()
libswiftCore.dylib****:****swift_allocObject
libswiftCore.dylib****:****swift_slowAlloc
libsystem_malloc.dylib****:****malloc
**Class.__allocating_init()**
**libswiftCore.dylib****:****_swift_allocObject_**
**libswiftCore.dylib****:****swift_slowAlloc**
**libsystem_malloc.dylib****:****malloc**
直接打印出point
变量和person
变量的内容,更直观一些:
var point = Point(x: 10, y: 20)
var person = Person()
print(Mems.ptr(ofVal:&person))
print(Mems.memStr(ofVal:&person))
print("------")
print(Mems.ptr(ofVal:&point))
print(Mems.memStr(ofVal:&point))
如果想查看一个类型所创建出来的变量占用多少内存,可以使用MemoryLayout<类型>.stride
查看,比如:
查看String
类型对象占用多少内存:MemoryLayout
查看Int
类型对象占用多少内存:MemoryLayout
如果想查看创建出来的指针变量在堆空间占用多少内存,可以使用malloc_size(ptr: UnsafeRawPointer)
var ptr = malloc(16)
print(malloc_size(ptr))//16
var ptr2 = malloc(17)
print(malloc_size(ptr2))//32
mac iOS中的malloc函数分配的内存大小总是16的倍数
赋值操作不同,值类型是深拷贝,引用类型是浅拷贝
值类型的深度拷贝
看看struct
类型赋值操作的汇编语言:
func testValueType() {
struct Point {
var x: Int
var y: Int
}
let p1 = Point(x: 10, y: 20)
var p2 = p1
p2.x = 11
p2.y = 21
print("123")
}
testValueType()
0x100002653 <+19>: movl $0xa, %edi
0x100002658 <+24>: movl $0x14, %esi
0x10000265d <+29>: callq 0x100002730 ; init(x: Swift.Int, y: Swift.Int) -> Point #1 in TestSwift.testValueType() -> () in Point #1 in TestSwift.testValueType() -> () at main.swift:11
0x100002662 <+34>: movq %rax, -0x10(%rbp)
0x100002666 <+38>: movq %rdx, -0x8(%rbp)
-> 0x10000266a <+42>: movq %rax, -0x20(%rbp)
0x10000266e <+46>: movq %rdx, -0x18(%rbp)
0x100002672 <+50>: movq $0xb, -0x20(%rbp)
0x10000267a <+58>: movq $0x15, -0x18(%rbp)
0x100002682 <+66>: movq 0x5a97(%rip), %rax ; (void *)0x00007fff862faa70: type metadata for Any
0x100002689 <+73>: addq $0x8, %rax
第一步:进入Point 的 init() 函数
第二步:Point 的 init() 函数 函数内部
第三步:深拷贝
在Swift标准库中,为了提升性能,String、Array、Dictionary、Set采取了Copy On Write的技术
比如仅当有“写”操作时,才会真正执行拷贝操作
对于标准库值类型的赋值操作,Swift 能确保最佳性能,所有没必要为了保证最佳性能来避免赋值
引用类型的浅拷贝
引用赋值给var、let或者给函数传参,是将内存地址拷贝一份
类似于制作一个文件的替身(快捷方式、链接),指向的是同一个文件。属于浅拷贝(shallow copy)
func testReferenceType() {
class Size {
var width: Int
var height: Int
init(width: Int, height: Int) {
self.width = width
self.height = height
}
}
let s1 = Size(width: 10, height: 20)
let s2 = s1
s2.width = 11
s2.height = 22
print()
}
testReferenceType()
0x1000022e1 <+1>: movq %rsp, %rbp
0x1000022e4 <+4>: pushq %r13
0x1000022e6 <+6>: subq $0xa8, %rsp
0x1000022ed <+13>: movq $0x0, -0x10(%rbp)
0x1000022f5 <+21>: movq $0x0, -0x18(%rbp)
0x1000022fd <+29>: xorl %eax, %eax
0x1000022ff <+31>: movl %eax, %ecx
0x100002301 <+33>: movq %rcx, %rdi
0x100002304 <+36>: movq %rcx, -0x50(%rbp)
0x100002308 <+40>: callq 0x100002470 ; type metadata accessor for Size #1 in TestSwift.testReferenceType() -> () at
0x10000230d <+45>: movl $0xa, %edi
0x100002312 <+50>: movl $0x14, %esi
0x100002317 <+55>: movq %rax, %r13
0x10000231a <+58>: movq %rdx, -0x58(%rbp)
0x10000231e <+62>: callq 0x100002490 ; __allocating_init(width: Swift.Int, height: Swift.Int) -> Size #1 in TestSwift.testReferenceType() -> () in Size #1 in TestSwift.testReferenceType() -> () at main.swift:14
0x100002323 <+67>: movq %rax, -0x10(%rbp)
-> 0x100002327 <+71>: movq %rax, %rdi
0x10000232a <+74>: movq %rax, -0x60(%rbp)
0x10000232e <+78>: callq 0x1000072a6 ; symbol stub for: swift_retain
0x100002333 <+83>: movq -0x60(%rbp), %rcx
0x100002337 <+87>: movq %rcx, -0x18(%rbp)
0x10000233b <+91>: addq $0x10, %rcx
0x10000233f <+95>: leaq -0x30(%rbp), %rdx
0x100002343 <+99>: movl $0x21, %esi
0x100002348 <+104>: movq %rcx, %rdi
0x10000234b <+107>: movq %rsi, -0x68(%rbp)
0x10000234f <+111>: movq %rdx, %rsi
0x100002352 <+114>: movq -0x68(%rbp), %rcx
0x100002356 <+118>: movq %rdx, -0x70(%rbp)
0x10000235a <+122>: movq %rcx, %rdx
0x10000235d <+125>: movq -0x50(%rbp), %rcx
0x100002361 <+129>: movq %rax, -0x78(%rbp)
callq 0x100002490 ; __allocating_init(width: Swift.Int, height: Swift.Int) -> Size #1 in TestSwift.testReferenceType() -> () in Size #1 in TestSwift.testReferenceType() -> () at main.swift:14
movq %rax, -0x10(%rbp)
函数callq后面执行的汇编指令为movq %rax, -0x10(%rbp)
rax、rdx常作为函数返回值使用,所以-0x10(%rbp)将存放s1的地址值。
我们现在读取一下rax寄存器的值。register read rax
通过rax存储的内存值,窥探一下内存
验证是操作s1的内存$0xb, 0x10(%rax)
可以看到读出来的rax储存的是s1的地址,同样是s2的地址
movq $0xb, 0x10(%rax)
通过上面汇编代码和内存地址值剖析,s1,s2同时指向堆中同一块内存区域。在s2中修改width的值,s1的类成员变量同样被修改了。