1.结构体
在 Swift 标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分
比如Bool
、Int
、Double
、String
、Array
、Dictionary
等都是结构体
struct Point {
var x: Int = 0
var y: Int = 0
var origin: Bool = false
}
var point = Point(x: 10, y:10, origin:false)
print(MemoryLayout<Point>.size) // 17
print(MemoryLayout<Point>.stride) // 24
print(MemoryLayout<Point>.alignment) // 8
所有结构体都有一个编译器自动生成可传入成员值的初始化器
(initializer,初始化方法,构造器,构造方法)
自动生成的初始化器会保证存储属性都被赋值
2.类
类的定义和结构体类似,但编译器并没有为类自动生成可以传入成员值的初始化器
class Point {
var x: Int = 0
var y: Int = 0
}
let p1 = Point()
let p2 = Point(x: 10, y: 20) // 报错
let p3 = Point(x: 10) // 报错
let p4 = Point(y: 20) // 报错
如果类的所有成员都在定义的时候指定了初始值,编译器会为类生成无参的初始化器 p成员的
class Point {
var x: Int = 0
var y: Int = 0
}
var p = Point()
class Point {
var x: Int
var y: Int
init() {
x = 0
y = 0
}
}
var p = Point()
类和结构体的本质区别
- 1.结构体是值类型,类是引用类型
struct Size {
var width = 3
var height = 4
}
class Point {
var x: Int = 1
var y: Int = 2
init(x: Int, y: Int) {
self.x = x
self.y = y
}
}
func test() {
// 0x00007ffeefbff048
var point = Point(x: 100, y: 200)
// 0x00007ffeefbff030
// 03 00 00 00 00 00 00 00
// 04 00 00 00 00 00 00 00
var size = Size()
withUnsafePointer(to: &point) { ptr in
print("point \(ptr)")
}
withUnsafePointer(to: &size) { ptr in
print("size \(ptr)")
}
// 引用类型,指针都占用8
print(MemoryLayout.size(ofValue: point)) // 8
print(MemoryLayout.stride(ofValue: point)) // 8
print(MemoryLayout.alignment(ofValue: point)) // 8
// 0x0000000102800150
// 68 77 00 00 01 00 00 00 0x10007768
// 02 00 00 00 00 00 00 00 0x00000002
// 64 00 00 00 00 00 00 00 0x00000064
// C8 00 00 00 00 00 00 00 0x000000C8
print(Unmanaged.passUnretained(point).toOpaque())
}
test()
可以看到,我们是在函数体内,栈空间创建的,如果是在外部,那就会在全局区,如果是在class里面创建,就会跟随class在堆空间,这边就举个例子。
- 2.结构体值类型给var、let或者给函数传参,是直接深copy了一份
//全局变量,程序运行过程中只有一份内存 内存地址不变
struct Space {
var x: Int
var y: Int
}
var space1 = Space(x: 10, y: 20)
var space2 = space1
space2.x = 100
space2.y = 200
//withUnsafePointer(to: &space1) { ptr in
// print("space1 \(ptr)")
//}
//
//withUnsafePointer(to: &space2) { ptr in
// print("space2 \(ptr)")
//}
这边用全局变量来做例子,你也可以放到函数体局部变量来做例子。
先看全局变量上面最简单代码的赋值汇编
0x1000013ab <+11>: movl $0xa, %eax // 10 给到 eax = rax
0x1000013b0 <+16>: movl %edi, -0x7c(%rbp)
0x1000013b3 <+19>: movq %rax, %rdi // rax 给到 rdi = 10
0x1000013b6 <+22>: movl $0x14, %eax // 20 给到 eax = rax
0x1000013bb <+27>: movq %rsi, -0x88(%rbp)
0x1000013c2 <+34>: movq %rax, %rsi // rax 给到 rsi = 20
0x1000013c5 <+37>: callq 0x100001740 ; Swift01.Space.init(x: Swift.Int, y: Swift.Int) -> Swift01.Space at main.swift:16
si 进去callq对应的init函数
Swift01`Space.init(x:y:):
-> 0x100001740 <+0>: pushq %rbp
0x100001741 <+1>: movq %rsp, %rbp
0x100001744 <+4>: movq %rdi, %rax rdi = 10 给到 rax
0x100001747 <+7>: movq %rsi, %rdx rsi = 20 给到 rdx
0x10000174a <+10>: popq %rbp
0x10000174b <+11>: retq
finish 跳出
0x1000013ca <+42>: leaq 0xe47(%rip), %rsi 0x100002a8f + 0xe47 = 0x100002218 space1的内存地址
0x1000013d3 <+51>: movq %rax, 0xe3e(%rip) rax = 10 赋值给右边内存地址 0x1000013da + 0xe3e = 0x100002218 space1的内存地址
0x1000013da <+58>: movq %rdx, 0xe3f(%rip) rdx = 20 赋值给右边内存地址 0x1000013e1 + 0xe3f = 0x100002220
0x1000013e1 <+65>: movq %rsi, %rdi
var space1 = Space(x: 10, y: 20)
到这里,space1
对象初始化后,内存赋值完毕
0x1000013f2 <+82>: movq 0xe1f(%rip), %rax 0x1000013f9 + 0xe1f = 0x100002218 把space1里面的前8个字节 (q类型) 10给到rax
0x1000013f9 <+89>: movq %rax, 0xe28(%rip) 0x100001400 + 0xe28 = 0x100002228 然后把rax的值给到 0x100002228 space2的内存地址
0x100001400 <+96>: movq 0xe19(%rip), %rax 0x100001407 + 0xe19 = 0x100002220 把space1里面的后8个字节 (q类型) 20给到rax
0x100001407 <+103>: movq %rax, 0xe22(%rip) 0x10000140e + 0xe22 = 0x100002230 然后把rax的值给到 0x100002230
0x10000140e <+110>: leaq -0x18(%rbp), %rdi
到这里 var space2 = space1 赋值操作的汇编完成
0x100001430 <+144>: callq 0x100001d1c ; symbol stub for: swift_beginAccess
0x100001435 <+149>: movq $0x64, 0xde8(%rip) // 100 赋值给 0x100001440 + 0xde8 = 0x100002228 space2的内存地址的 (q类型) 前八个字节
0x100001440 <+160>: leaq -0x30(%rbp), %rdi
0x100001444 <+164>: callq 0x100001d28 ; symbol stub for: swift_endAccess
0x100001449 <+169>: leaq 0xdd8(%rip), %rax
0x100001450 <+176>: xorl %r8d, %r8d
0x100001450 <+176>: xorl %r8d, %r8d
0x100001453 <+179>: movl %r8d, %ecx
0x100001456 <+182>: movq %rax, %rdi
0x100001459 <+185>: leaq -0x48(%rbp), %rsi
0x10000145d <+189>: movl $0x21, %edx
0x100001462 <+194>: callq 0x100001d1c ; symbol stub for: swift_beginAccess
0x100001467 <+199>: movq $0xc8, 0xdbe(%rip) // 200 赋值给 0x100001472 + 0xdbe = 0x100002230 space2的内存地址的 (q类型) 后八个字节
0x100001472 <+210>: leaq -0x48(%rbp), %rdi
space2.x = 100
space2.y = 200
到这里 space2
的赋值操作完成
0x100002228
地址二进制如下:
64 00 00 00 00 00 00 00
C8 00 00 00 00 00 00 00
可以简单看下这个局部变量对应的汇编,很简单
0x1000017b3 <+19>: movl $0xa, %edi // 10赋值给edi = rdi
0x1000017b8 <+24>: movl $0x14, %esi // 20赋值给esi = rsi
0x1000017bd <+29>: callq 0x100001790 // si 进去 rdi 赋值给rax rsi 赋值给rdx
0x1000017c2 <+34>: movq %rax, -0x10(%rbp)
0x1000017c6 <+38>: movq %rdx, -0x8(%rbp)
0x1000017ca <+42>: movq %rax, -0x20(%rbp)
0x1000017ce <+46>: movq %rdx, -0x18(%rbp)
0x1000017d2 <+50>: movq $0x64, -0x20(%rbp)
0x1000017da <+58>: movq $0xc8, -0x18(%rbp)
寄存器经验:
如果类似0xde8(%rip)
一般是全局区
如果类似-0x20(%rbp)
一般是局部变量
3.值类型的赋值操作
var s1 = "Jack"
var s2 = s1
s2.append("_Rose")
print(s1) // Jack
print(s2) // Jack_Rose
var a1 = [1, 2, 3]
var a2 = a1
a2.append(4)
a1[0] = 2
print(a1) // [2, 2, 3]
print(a2) // [1, 2, 3, 4]
var d1 = ["max" : 10, "min" : 2]
var d2 = d1
d1["other"] = 7
d2["max"] = 12
print(d1) // ["other": 7, "max": 10, "min": 2]
print(d2) // ["max": 12, "min": 2]
- 在Swift标准库中,为了提升性能,
String、Array、Dictionary、Set
采取了Copy On Write
的技术,比如仅当有“写”操作时,才会真正执行拷贝操作 - 对于标准库值类型的赋值操作,Swift 能确保最佳性能,所有没必要为了保证最佳性能来避免赋值
引用类型
这个简单,就是我们普遍认为的内存引用,举个例子看下汇编
func test1(){
class People {
var width: Int
var height: Int
init(width: Int, height: Int) {
self.width = width
self.height = height
}
}
var p1 = People(width: 10, height: 20)
// register read rax
// 98 22 00 00 01 00 00 00 ->meta info
// 02 00 00 00 00 00 00 00 ->ref
// 0A 00 00 00 00 00 00 00 ->10
// 14 00 00 00 00 00 00 00 ->20
// 0x1000010e0 <+64>: movq %rax, -0x10(%rbp)
// 0x1000010e7 <+71>: movq %rax, -0x60(%rbp)
var p2 = p1
// 0x100001127 <+135>: movq -0x60(%rbp), %rax
// 0x10000112b <+139>: movq $0xb, 0x10(%rax)
// rax是对象的地址,0x10是16个字节 也就是 0A 00 00 00 00 00 00 00 赋值11
p2.width = 11
// 0x100001162 <+194>: movq -0x60(%rbp), %rax
// 0x100001166 <+198>: movq $0xc, 0x18(%rax)
// rax是对象的地址,0x18是24个字节 也就是 14 00 00 00 00 00 00 00 赋值12
p2.height = 12
}
test1()
根据上述的汇编分析,也可以看到
1.内存地址格式为 0x4bdc(%rip),一般是全局变量,全局区(数据段)
2.内存地址格式为 -0x78(%rbp),一般是局部变量,栈空间
3.内存地址格式为 0x10(%rax),一般是堆空间
4.闭包表达式
var clouse = {
(v1: Int, v2: Int) -> Int in
return v1 + v2
}
clouse()
{
(参数列表) -> 返回值类型 in
函数体代码
}
func exec(v1: Int, v2: Int, fn:(Int, Int) -> Int) {
print(fn(v1, v2))
}
exec(v1: 10, v2: 20) {
(v1: Int, v2: Int) -> Int in
return v1 + v2
}
exec(v1: 10, v2: 20) {
v1, v2 in
return v1 - v2
}
exec(v1: 10, v2: 20) {
v1, v2 in v1 * v2
}
exec(v1: 10, v2: 20) { $0 + $1}
exec(v1: 100, v2: 200, fn:+)
5.闭包
一个函数和它所捕获的变量/常量环境组合在一起
- 一般指定义在函数内部的函数
- 一般它捕获的是外层函数的局部变量、常量
- 返回出去的函数 + 捕获堆空间的东西
5.1 概述
实例一
//typealias FNC = (Int) -> Int
//
//func getFn() -> FNC {
// var num = 0
// return {
// (i: Int) -> Int in
// num += i
// return num
// }
//}
//
//var x = getFn()
//print(x(1))
//print(x(2))
//print(x(3))
typealias KJFunc = (Int) -> Int
func getFn() -> KJFunc {
var num = 1
func plus(_ i: Int) -> Int{
num += i
return num
}
return plus
}
var fn = getFn()
print(fn(1))
print(fn(2))
print(fn(3))
print(fn(4))
print(fn(5))
这个闭包应该很简单,是个人都知道结果,但是有没有想过,var num
这个是局部变量,在getFn
函数之后,栈空间就被回收了,那么除了全局静态区变量,就只可能被分配到堆空间了。
我们在两个return
的时候打断点,看下汇编
Swift01`getFn():
0x1000018f0 <+0>: pushq %rbp
0x1000018f1 <+1>: movq %rsp, %rbp
0x1000018f4 <+4>: subq $0x20, %rsp
0x1000018f8 <+8>: leaq 0x7f9(%rip), %rdi
0x1000018ff <+15>: movl $0x18, %esi
0x100001904 <+20>: movl $0x7, %edx
0x100001909 <+25>: callq 0x100001e1c ; symbol stub for: swift_allocObject
-> 0x10000190e <+30>: movq %rax, %rdx
0x100001911 <+33>: addq $0x10, %rdx
0x100001915 <+37>: movq %rdx, %rsi
0x100001918 <+40>: movq $0x1, 0x10(%rax)
0x100001920 <+48>: movq %rax, %rdi
0x100001923 <+51>: movq %rax, -0x8(%rbp)
0x100001927 <+55>: movq %rdx, -0x10(%rbp)
可以看到,闭包是分配了堆空间的swift_allocObject
,之前 有提到过,callq
函数返回值是存储在rax
寄存器中,看下内存
(lldb) register read rax
rax = 0x000000010067fc80
(lldb) x/5xg 0x000000010067fc80
0x10067fc80: 0x00000001000020f8 0x0000000000000002
0x10067fc90: 0x000000010067000f 0x0000000000000000
0x10067fca0: 0x0000000000000000
读出rax
内存地址,然后打印该地址下5组数据,可以猜测前32个字节,有点像对象的内存分布,但是可以看到0x10067fc90: 0x000000010067000f
是脏数据,因为还没有被初始化,断点继续往下0x100001918 <+40>: movq $0x1, 0x10(%rax)
,执行完这句,内存就被初始化为1了
(lldb) x/5xg 0x000000010067fc80
0x10067fc80: 0x00000001000020f8 0x0000000000000002
0x10067fc90: 0x0000000000000001 0x0000000000000000
0x10067fca0: 0x0000000100075380
继续断到闭包内的return
里面的断点
再次读取对象的地址,可以看到第十六个字节被赋值了2,一次类推,可以看到闭包捕获的变量,变量本身是局部变量,栈空间的,被捕获到了堆空间,而且内存分布看上去和对象基本一致,前八个字节元数据,8-16是引用计数,在后面就存储捕获的变量,那么又个问题,怎么知道捕获的变量实际分配了多少内存呢。
先简单看下普通类实例化的时候,init
函数之后,实际分配了多少内存
示例二
class Test {
var test1: Int = 0
var test2: Int = 0
}
var test = Test()
0x1000013d4 <+36>: callq 0x100001810 ; Swift01.Test.__allocating_init() -> Swift01.Test at main.swift:38
si
进去
可以看到alloc
的时候,movl $0x20, %esi
分配了32个字节的内存
那么我们再回过头来看闭包的汇编
可以看到,发送了24个字节的指令去申请内存,实际上根据架构,实际上获取到了32个字节,因此上面x/5xg
打印出来的内存地址,其实只要x/4xg
就是完整的闭包捕获的对象了。
小结:
- 1.闭包就是定义在函数内部的函数,捕获外层函数的局部变量/常量
- 2.内存捕获到堆空间,捕获的局部变量、常量就是对象的成员,组成的闭包函数,就是类内部定义的方法
- 3.捕获的变量原本在栈空间,出了作用域被回收,copy了值到堆空间变成对象供函数使用
阶段一概述:
从汇编来看
1.变量捕获会在堆空间开辟内存
2.例子中开辟的内存是32个字节,前16个字节类似类的数据结构,后16个字节开始存放捕获的变量值
5.2 详述
1.先来看一下简单的函数内存分布
func sum(v1: Int, v2: Int) -> Int{
return v1 + v2
}
var fn = sum
print("1")
(lldb) p fn
() -> () $R0 = 0x0000000100000aa0 Swift01`Swift01.sum(v1: Swift.Int, v2: Swift.Int) -> Swift.Int at main.swift:11
展开的汇编如下
0x1000009e3 <+19>: leaq 0xb6(%rip), %rcx ; Swift01.sum(v1: Swift.Int, v2: Swift.Int) -> Swift.Int at main.swift:11
0x1000009ea <+26>: movq %rcx, 0x7a7(%rip) ; Swift01.fn : (Swift.Int, Swift.Int) -> Swift.Int
0x1000009f1 <+33>: movq $0x0, 0x7a4(%rip) ; Swift01.fn : (Swift.Int, Swift.Int) -> Swift.Int + 4
0x1000009fc <+44>: movl $0x1, %ecx
根据经验来看,0x10(%rip)
这种就是全局变量,也就是这边的var fn
,那么根据右侧的提示,可以看到leaq
对应的地址赋值是 rip + 0xb6 = 0x100000AA0
,和上面的po
打印出来的吻合,而且下面两个movq
分别是对 0x1000011A0``0x100001198
进行8个字节的赋值,因此,函数分配给fn变量的内存是16个字节,前8个字节是函数地址,后八个字节是0,暂时没用到。
2.看一下没捕获变量的闭包(这种也不叫闭包)
typealias KJFunc = (Int) -> Int
func getFn() -> KJFunc {
// var num = 0
func plus(_ i: Int) -> Int{
// num += i
// return num
return i
}
return plus
}
var fn = getFn()
print("1")
断点在getFn()
0x1000013c0 <+0>: pushq %rbp
0x1000013c1 <+1>: movq %rsp, %rbp
0x1000013c4 <+4>: pushq %r13
0x1000013c6 <+6>: subq $0x218, %rsp ; imm = 0x218
0x1000013cd <+13>: movl %edi, -0x84(%rbp)
0x1000013d3 <+19>: movq %rsi, -0x90(%rbp)
0x1000013da <+26>: callq 0x100001aa0 ; Swift01.getFn() -> (Swift.Int) -> Swift.Int at main.swift:47
si 进去
Swift01`getFn():
-> 0x100001aa0 <+0>: pushq %rbp
0x100001aa1 <+1>: movq %rsp, %rbp
0x100001aa4 <+4>: leaq 0x15(%rip), %rax ; plus #1 (Swift.Int) -> Swift.Int in Swift01.getFn() -> (Swift.Int) -> Swift.Int at main.swift:49
0x100001aab <+11>: xorl %ecx, %ecx
0x100001aad <+13>: movl %ecx, %edx
0x100001aaf <+15>: popq %rbp
0x100001ab0 <+16>: retq
可以看到函数plus
赋值给了rax,xorl %ecx, %ecx
清0操作,然后把0赋值给edx
也就是rdx
,然后看外部
0x1000013df <+31>: movq 0xc32(%rip), %rsi ; (void *)0x00007fff9abdeb18: type metadata for Any
0x1000013e6 <+38>: addq $0x8, %rsi
0x1000013ea <+42>: movq %rax, 0xdc7(%rip) ; Swift01.fn : (Swift.Int) -> Swift.Int
0x1000013f1 <+49>: movq %rdx, 0xdc8(%rip) ; Swift01.fn : (Swift.Int) -> Swift.Int + 8
0x1000013f8 <+56>: movl $0x1, %edi
这里可以看到把rax、rdx
里面的值赋值给了对应的地址,
(lldb) p fn
() -> () $R4 = 0x0000000100001ac0 Swift01`plus #1 (Swift.Int) -> Swift.Int in Swift01.getFn() -> (Swift.Int) -> Swift.Int at main.swift:49
(lldb) register read rax
rax = 0x0000000100001ac0 Swift01`plus #1 (Swift.Int) -> Swift.Int in Swift01.getFn() -> (Swift.Int) -> Swift.Int at main.swift:49
在没有捕获变量的情况下,和普通函数没什么区别,给外部变量前八个字节就是函数地址,后八个字节是0
3.看下完整的闭包是怎么样的
typealias KJFunc = (Int) -> Int
func getFn() -> KJFunc {
var num = 0
func plus(_ i: Int) -> Int{
num += i
return num
}
return plus
}
var fn = getFn()
print(fn(1))
print(fn(2))
print(fn(3))
print(fn(4))
print(fn(5))
0x01
先在return plus
那打上断点,然后进入汇编
看到闭包的返回值,rax
中存储着函数地址,rdx
中存储着堆对象的地址
0x02
return plus
后,回到外部调用,这边的函数地址存储在rax
中,因此调用就会变成callq *%rax
在callq
上面,fn(1)
的1存入rdi
,-0xb0(%rbp)
追溯到上面的rcx
(变量的后8个字节,堆空间地址)地址存入r13
,-0xa8(%rbp)
追溯到上面的rax
(变量的前8个字节),函数地址,所以调用callq
的时候,参数1,参数self
,也就是堆空间变量的地址,函数指针都有了,
rdi
= 1rax
= 函数地址r13
= 堆空间地址
0x03
si
进去验证下
Swift01`partial apply for plus #1 (_:) in getFn():
-> 0x100001a90 <+0>: pushq %rbp
0x100001a91 <+1>: movq %rsp, %rbp
0x100001a94 <+4>: movq %r13, %rsi
0x100001a97 <+7>: popq %rbp
0x100001a98 <+8>: jmp 0x100001930 ; plus #1 (Swift.Int) -> Swift.Int in Swift01.getFn() -> (Swift.Int) -> Swift.Int at main.swift:51
进入闭包函数地址,可以看到r13
赋值给了rsi
,然后jmp到 plus
函数
rdi
= 1rax
= 函数地址rsi
= 堆空间地址
0x04
plus
函数
首先红色部分,把对应的rdx
对象0x10
后字节的数字 加上 rcx
中存储的值,然后存储在rcx
中。蓝色部分就是把存储在寄存器中计算好的值,写入rax
对应的地址,可以看到rax
对应的地址就是上面分析出来,堆对象地址后移16个字节所处的地址,写入寄存器中的值,完成一次fn(1)
的调用
6.自动闭包
func getNumber() -> Int {
let a = 100
let b = 200
return a + b
}
func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {
return v1 > 0 ? v1 : v2
}
print(getFirstPositive(10, 20)) // 10
print(getFirstPositive(-2, 20)) // 20
print(getFirstPositive(0, -4)) // -4
print(getFirstPositive(0, getNumber())) // 300
print(getFirstPositive(10, getNumber())) // 10
正常写法,参数如果是函数的话,虽然有时候不需要,也会执行
func getNumber() -> Int {
let a = 100
let b = 200
return a + b
}
func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int {
return v1 > 0 ? v1 : v2()
}
print(getFirstPositive(10, {
let a = 1000
let b = 2000
print("111")
return a + b
})) // 10
print(getFirstPositive(0, {
let a = 1000
let b = 2000
print("222")
return a + b
})) // 222 3000
print(getFirstPositive(0, getNumber)) // 300
print(getFirstPositive(0, {100})) // 100
print(getFirstPositive(0){200}) // 200
这种写法就可以让函数晚点执行,但是如果是() -> Int
类型,还有个@autoclosure
让调用更加优雅一点
func getNumber() -> Int {
let a = 100
let b = 200
print("333")
return a + b
}
func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
return v1 > 0 ? v1 : v2()
}
print(getFirstPositive(100, getNumber()))
print(getFirstPositive(0, getNumber()))
print(getFirstPositive(0, 99))
@autoclosure
会自动将 99 封装成闭包{ 99 }
@autoclosure
只支持() -> T
格式的参数@autoclosure
并非只支持最后一个参数- 空合运算符
??
使用了@autoclosure
技术 - 有
@autoclosure
无@autoclosure
构成函数重载
第二天结束0.0