Files
knowledge-kit/Chapter1 - iOS/1.114.md
2025-06-23 01:18:55 +08:00

591 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Swift 闭包研究
## 方法占用对象内存吗?
实验一:
```swift
class Point {
var test = true
var age = 29
var height = 175
}
var p = Point()
print(Mems.size(ofRef: p)) // 48
```
为什么是48而不是40
Point 类前16位的前8位表示类信息后8位表示引用计算信息2个 Int 占2*8 = 16 ByteBool 占用1 Byte。所以实际占用 8 + 8 + 2 * 8 + 1 = 33 Byte。但由于存在内存对齐内存对齐以8为 base都是8的整数倍但 malloc 函数分配的内存都是 16的倍数所以占用48 Byte。
Demo:
```swift
class Person {
var age = 29
func sayHi () {
var height = 175
print("局部变量", Mems.ptr(ofVal: &height))
print("I am \(age) old")
}
}
func sayOuterHi () {
print("Hello world")
}
var p = Person()
p.sayHi()
sayOuterHi()
print("全局变量", Mems.ptr(ofVal: &p))
print("堆空间", Mems.ptr(ofRef: p))
```
<img src="./../assets/ClassMemoryLayoutExcludeFunction.png" style="zoom:25%">
<img src="./../assets/ClassMemoryLayoutExcludeFunction2.png" style="zoom:25%">
代码段Person.sayHi 0x1000034d0
代码段sayOuterHi 0x1000038e0
全局变量: 0x000000010000c388
堆空间: 0x20c820
局部变量(栈): 0x00007ff7bfeff2f8
可以看到写在类里面的方法地址和写在 Class 外部的方法地址差不多,都在代码段。
结论:方法不占用对象的内存。方法、函数存放于代码段。
## 闭包
### 定义
什么是闭包?一个函数和它所捕获的变量/常量环境组合起来,称为闭包。
- 一般指定义在函数内部的函数
- 一般捕获的是外层函数的局部变量、常量
### 原理窥探
Demo
```swift
func exec(a: Int, b: Int, fn: (Int, Int) -> Int) {
print(fn(a, b))
}
// 写法1
func sum(a: Int, b: Int) -> Int { return a + b }
exec(a: 1, b: 2, fn: sum)
// 写法2:闭包
exec(a: 1, b: 2, fn: {
(a: Int, b: Int) -> Int in
return a + b
})
// 写法3:闭包简写
exec(a: 1, b: 2, fn: {
a,b in return a + b
})
// 写法4:闭包简写
exec(a: 1, b: 2, fn: {
a,b in a + b
})
// 写法5:闭包简写。用$0、$1来获取参数。
exec(a: 1, b: 2, fn: { $0 + $1 })
// 写法5:闭包简写。用 + 来代表操作,让编译器进行推断
exec(a: 1, b: 2, fn: + )
```
如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性。尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式
上面的写法等价于
```swift
// 写法6:尾随闭包
exec(a: 1, b: 2) {
$0 + $1
}
```
如果闭包表达式是函数的唯一实参,而且使用饿尾随闭包的语法,那就不需要在函数名后面写圆括号
```swift
func exec(fn: (Int, Int) -> Int) {
print(fn(1, 2))
}
exec(fn: { $0 + $1 }) // 3
exec() { $0 + $1 } // 3
exec{ $0 + $1 } // 3
```
来个 Demo 看看系统数组的排序
```swift
var array = [1, 8, 9, 12, 32, 2]
//array.sort()
func compare(a: Int, b: Int) -> Bool {
return a < b
}
// 写法1
//array.sort(by: compare)
// 写法2
// array.sort { $0 < $1 }
// 写法3
//array.sort { a, b in
// return a < b
//}
// 写法4
//array.sort(by: {
// (a: Int, b: Int) -> Bool in
// return a < b
//})
// 写法5
//array.sort(by: <)
// 写法6
array.sort() { $0 < $1 }
print(array) // [1, 2, 8, 9, 12, 32]
```
Demo2
闭包的变量捕获
```swift
typealias Fn = (Int) -> Int
func getFn() -> Fn {
var num = 0
func plus(_ i: Int) -> Int {
return i
}
return plus
}
var fn = getFn()
print(fn(1)) // 1
print(fn(2)) // 2
print(fn(3)) // 3
```
<img src="./../assets/ClosureCaptureVariableDemo1.png" style="zoom:25%">
简单修改下代码
```swift
typealias Fn = (Int) -> Int
func getFn() -> Fn {
var num = 0
func plus(_ i: Int) -> Int {
num += i
return num
}
return plus
}
var fn = getFn()
print(fn(1)) // 1
print(fn(2)) // 3
print(fn(3)) // 6
```
<img src="./../assets/ClosureCaptureVariableDemo2.png" style="zoom:25%">
可以看到上面有 `allocObject` 方法调用,说明产生了堆空间对象,用于存放 `num` 。是由 `var fn = getFn()` 造成的调用1次 `getFn` 则产生1次堆空间分配用于保存 num。
也就是说如果一个内层函数访问了外层函数的局部变量,也就会发生变量捕获,做法就是延长局部变量的生命周期,在堆空间申请一段内存,用户保存局部变量。
对代码进行修改
```swift
typealias Fn = (Int) -> Int
func getFn() -> Fn {
var num = 1
func plus(_ i: Int) -> Int {
num += i
return num
}
return plus
}
var fn1 = getFn()
var fn2 = getFn()
var fn3 = getFn()
print(fn1(1)) // 2
print(fn2(2)) // 3
print(fn3(3)) // 4
```
我们在汇编 `swift_allocObject` 下面下个断点
<img src="./../assets/ClosureCaptureVariableDemo3.png" style="zoom:25%">
第一次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x0000600000210000`。此时还没值。
敏感点查看到字面量1给汇编代码15行加断点执行完15行继续查看内存信息
<img src="./../assets/ClosureCaptureVariableDemo4.png" style="zoom:25%">
可以看到内存数据发生了改变。绿色框内有了值1。
第二次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x00006000002042c0`。此时还没值。
敏感点查看到字面量1给汇编代码15行加断点执行完15行继续查看内存信息
<img src="./../assets/ClosureCaptureVariableDemo5.png" style="zoom:25%">
第三次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 `Debug -> Debug Workflow -> View Memory` 查看内存信息`0x000060000020d460`。此时还没值。
敏感点查看到字面量1给汇编代码15行加断点执行完15行继续查看内存信息
<img src="./../assets/ClosureCaptureVariableDemo6.png" style="zoom:25%">
打印结果也说明了问题因为调用3次 `getFn()` 所以会在堆上 alloc 3块内存用于保存捕获的变量。所以调用 fn1 得到 2调用 fn2 得到 3调用 fn3 得到 4。
BTW堆空间分配的内存如果没有 `init` 或者赋特定的值,数据会是随机的。因为有可能是之前别的地方使用过的内存。
### 闭包内存结构
先来个简单的函数,看看指针内存结构
```swift
func sum(_ a: Int, _ b: Int) -> Int { return a + b }
var fn = sum
print(fn(1, 2)) // 3
```
在 `var fn = sum` 处下断点,可以看到下面汇编
<img src="./../assets/ClouserMemoryLayoutExploreDemo1.png" style="zoom:25%">
我们知道 `leaq` 是从 `%rip + 0x10f` 算出来的地址,赋值给 `%rax`。所以大概可以推测 `%rip + 0x10f` 就是 `sum` 函数地址。LLDM p 打印 `sum` 地址为 `0x0000000100003a30`。
`%rip` 是下一条指令的地址 `0x100003921``%rip + 0x10f` 也就是 `0x0000000100003a30`。和猜想一致。
第六行汇编的意思就是将 `sum` 函数的地址赋值给 `%rax`。
第七行汇编的意思是将保存在 `%rax` 内的地址,从 `%rip + 0x88d8` 处取8个字节用来保存 `%rax` 的地址。`0x100003928 + 0x88d8 = 0x10000C200`
第八行汇编的意思是将保存在 `%rax` 内的地址,从 `%rip + 0x88d85` 处取8个字节用来保存 `$0x0` 。`0x100003933 + 0x88d5 = 0x10000C208`
`0x10000C200` 到 `0x10000C208` 差8位也是连续的。说明分配了一个函数指针长度为16位。通过`MemoryLayout.stride(ofValue: sum)` 看到也是16位。符合猜想。
直奔主题,研究闭包内存
<img src="./../assets/ClouseExploreNotCaptureVariableDemo1.png" style="zoom:25%">
可以看到在调用完第六行的函数后将寄存器 `rax`、`rdx` 里的值取出来使用了。进入函数内部看看发生了什么LLDB 输入 `si`
<img src="./../assets/ClouseExploreNotCaptureVariableDemo2.png" style="zoom:25%">
可以看到 `rax` 里面存放了 `plus` 函数地址。第7行汇编是异或运算2个 `ecx` 异或结果为0写入到 `ecx` 里。然后第8行汇编将 `ecx` 里的0写入到 `edx``edx` 也就是 `rdx`。走完第6行的汇编继续看第7、8行
<img src="./../assets/ClouseExploreNotCaptureVariableDemo1.png" style="zoom:25%">
将第6行函数的返回结果 `rax` 里的函数地址赋值给 `rip +0x89e7 = 0x100003819 + 0x89e7 = 0x10000C200 ` 第7行的`rdx` 的值赋值个给 `rip +0x89e8 = 0x100003820 + 0x89e8 = 0x10000C208`。
也就是 fn1 前8个字节存放 plus 的函数地址后8个字节存放0.
继续对比实验,查看闭包放了什么东西(注意和上面的实验不同,下面存在闭包)
<img src="./../assets/ClouseExploreNotCaptureVariableDemo3.png" style="zoom:25%">
基本可以断定函数会返回一个长度为16 Byte 的内存。分别保存在 `rax` 和 `rdx`上。所以针对性的研究 `rdx` 和 `rax`
汇编第10行经过在堆上为捕获的变量 alloc 内存后,将内存保存到 `rax` 中,然后赋值给 `rdi`第11行将 `rdi` 再赋值给 `rbp - 0x10` 的地址。所以汇编19行的 `rbp - 0x10 ` 保存的也就是堆内存,赋值给 `rdx ` 了。
20行的 `rax` 保存了一个看似是 `plus` 的函数地址。具体是:`rip + 0x167 = 0x100003969 + 0x167= 0x100003C37 `
<img src="./../assets/ClouseExploreNotCaptureVariableDemo3.png" style="zoom:25%">
继续走,走到真正的 `plus` 方法内,可以看到函数地址是 `0x100003970` 。所以返回的 `rax` 里面可能是间接调用真正的 `plus` 函数的。
问题变得微妙起来了,`getFn` 方法返回一个地址占用16个字节但是前8个字节存储 `plus` 方法地址后8个字节存储闭包捕获后在堆上申请内存存放的数据地址。那如何根据一个地址的前8位去真正调用方法
Tips由于地址是动态生成的所以真正去调用 plus 的时候一定不是写死的地址,一定是动态生成的。这种属于间接调用。
- call 后面直接加一个函数地址,属于直接调用。类似 `callq 0x100003970`
- call 后面的函数地址不是固定的,而是动态生成的叫间接调用。在 `at&t` 汇编里面,间接调用前面要加 `*` 。类似 `callq *%rax`,意思是从 `%rax` 里面取出一个地址值出来,当成函数地址,去调用
顺着思路,分析下汇编:
<img src="./../assets/ClouseExploreCaptureVariableDemo1.png" style="zoom:25%">
我们看到25行 `callq *%rax` 存在动态调用,所以需要找到 `%rax` 里的值是哪里来的。然后顺着向上找找到23行 `movq -0x30(%rbp), %rax`。然后继续向上看到16行 `movq %rax, -0x30(%rbp)`。继续向上看到15行 `movq 0x88db(%rip), %rax`。相当于就是在 `rip + 0x88db ` 处取出8个字节出来当作函数地址调用汇编代码的右边写了`fn1` ),地址为:`0x88db(%rip) = rip + 0x88db = 0x10000392d + 0x88db = 0x10000C208` 。
断点继续放开在汇编25行处加断点 `callq *%rax`
<img src="./../assets/ClouseExploreCaptureVariableDemo2.png" style="zoom:25%">
可以看到在方法内部第6行汇编处直接调用一个代码段的函数地址 `jmp 0x1000039f0 `
指令 `jmp`、`call` 的区别在于:
- `jmp` 指令控制程序直接跳转到目标地址执行程序,程序总是顺序执行,指令本身无堆栈操作过程。
- `call` 指令跳转到指定目标地址执行子程序,执行完子程序后,会返回 `call` 指令的下一条指令处执行程序,执行 `call` 指令有堆栈操作过程
LLDB 输入 `si`,可以看到是 `plus` 函数真正的地址
<img src="./../assets/ClouseExploreCaptureVariableDemo3.png" style="zoom:25%">
`fn1` 函数调用的时候,参数如何传递?
<img src="./../assets/ClouseExploreCaptureVariableDemo4.png" style="zoom:25%">
汇编17行 `movq 0x88d8(%rip), %r13 ; SwiftDemo.fn1 : (Swift.Int) -> Swift.Int + 8`可以看到将返回的 fn1 本身16字节的后8个字节也就是堆地址空间值保存到寄存器 `edi` 也就是寄存器 `rdi` 上了。
汇编24行 `movl $0x1, %edi` 将参数1传给寄存器 `rdi` 了。
然后 LLDB 输入 `si` 去分析 callq 内部
<img src="./../assets/ClouseExploreCaptureVariableDemo5.png" style="zoom:25%">
可以看到 `movq %r13, %rsi` 将寄存器 r13 的值写入到 `rsi` 了。目前为止:`rdi` 保存参数1`rsi` 保存堆地址值。
继续输入 `si`
<img src="./../assets/ClouseExploreCaptureVariableDemo6.png" style="zoom:25%">
可以看到汇编的第5行 `movq %rsi, -0x50(%rbp)` 将 `rsi` 里的1保存到 `rbp - 0x50` 处。第6行汇编 `movq %rdi, -0x58(%rbp)` 将 `rdi` 堆地址值保存到 `rbp - 0x58` 处。
汇编26行 `movq -0x58(%rbp), %rdi` 将 `rbp - 0x58` 的值写入到 `rdi`也就是堆地址值。第27行 `movq -0x50(%rbp), %rsi` 将 `rbp - 0x50` 的值写入到 `rsi`也就是参数值1。
然后真正做 `plus` 加法运算的就是28行的 `addq 0x10(%rsi), %rdi` 从 `rsi` 堆地址值的第16个字节的地方取出8个字节的值也就是捕获的外部变量 `num` 再和参数1相加。
<img src="./../assets/ClouseExploreCaptureVariableDemo7.png" style="zoom:25%">
可以看到第6行堆地址空间的值写入到 `rbp -0x58` 第26行又将 `rbp -0x58` 写入到 `rdi`29行将 `rdi` 的值,写入到 `rbp - 0x48`34行将 `rbp - 0x48` 写入到 `rcx`35行将 `rcx` 的地址值,写入到 `rax` 对应的存储空间。也就是堆上的 `num` 值已经修改,被覆盖了。
总结:当 `getFn` 内部没有发生闭包的时候fn1 的地址就是16 Byte前8 Byte就是 `plus` 的函数地址。当发生函数闭包的时候,`fn1` 的16 Byte前8 Byte 存储间接调用 `plus` 函数的中转函数后8 Byte 存储着捕获的且在堆上分配内存的地址值。真正调用 `plus` 的时候会通过寄存器传递2个参数1个是 fn1 函数的参数1个是堆空间的地址值。
```swift
var fn1 = getFn()
fn1(1) // 2
fn1(3) //4
```
因为只调用1次 `getFn` 所以堆内存分配了1个被捕获的变量地址调用过1次 fn1 后,堆地址所指向的内存上的数被修改了。当第二次调用的时候操作的还是同一块堆内存地址,也就是同一个被捕获的堆地址所指向的变量。
```swift
var fn1 = getFn()
fn1(1) // 2
fn1(3) //4
var fn2 = getFn()
fn2(2) // 3
fn2(4) // 5
```
因为调用了2次 `getFn` 所以堆内存分配了2个被捕获的变量地址调用过1次 fn1 后,堆地址所指向的内存上的数被修改了。当第二次调用的时候操作的还是同一块堆内存地址,也就是同一个被捕获的堆地址所指向的变量。当调用 fn2 的时候操作的是被捕获的新的一个堆地址空间所指向的变量。
且 fn1 所占用的16个字节fn2 所占用16个字节。2者的前8字节内容相同都是包装了 `plus` 函数的一个地址。
### “闭包就是对象”
捕获了外部变量的闭包类似于一个类,里面存在存储属性和方法
```swift
class {
var num: Int
func fn(_ i: Int) -> Int {
return i + num
}
}
```
## 自动闭包
**自动闭包是一种自动创建的,用来把作为实际参数传递给函数的表达式打包的闭包**。它不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值
这个语法的好处在于通过写普通表达式代替显示闭包,而使你省略包围函数的形式参数的括号.
比如系统的断言 `assert`
```swift
public func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = String(), file: StaticString = #file, line: UInt = #line)
```
```swift
// 语法糖。自动闭包
func getPositiveValue(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
return v1 > 0 ? v1 : v2()
}
//print(getPositiveValue(10, {20}))
//print(getPositiveValue(-10) {20})
//print(getPositiveValue(-20) {
// let a = 10
// return a + 1
//}
//)
print(getPositiveValue(-10, 22))
```
`@autoclosure` 会自动将 22 封装成闭包 `{ 22 }`。
`@autoclosure` 只支持 `() -> T` 无参数,并且有一个返回值的闭包。
`@autoclosure` 并非只支持最后1个参数。
`??` 函数的本质就是自动闭包
自动闭包允许延迟处理,因此任何闭包内部的代码直到你调用的时候才会运行。对于有副作用或者占用资源的代码来说很有用。因为它可以允许你控制代码何时进行求值
```swift
var group = ["zhangsan", "lisi", "wangwu"]
//print(group.count)
//let groupRemover = { group.remove(at: 0) }
//print(group.count)
//
////print("execute remove function \(groupRemover())")
//print(group.count)
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: group.remove(at: 0))
// Now serving zhangsan!
```
如果一个闭包作为参数,是可以去掉 `{}` 的,参数加了 `@autoclosure` 后,是会自动转换为闭包的。
但如果函数参数没有加 `@autoclosure`,在调用函数的时候,传参没有加 `{}` 编译器是会报错的
<img src="./../assets/SwiftAutoClosureError.png" style="zoom:25%">
正确的做法是:要么加 `@autoclosure` 要么在函数调用的时候加 `{}`
```swift
// 改法1
func collectCustomerProviders(_ customerProvider: @escaping () -> String) {
customerProoviders.append(customerProvider)
}
collectCustomerProviders( { group.remove(at: 0) })
// 改法2
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
customerProoviders.append(customerProvider)
}
collectCustomerProviders(group.remove(at: 0))
```
如果你的自动闭包允许逃逸,就可以同时使用 `@autoclosure` 和 `@escaping `
```swift
var customerProoviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
customerProoviders.append(customerProvider)
}
collectCustomerProviders(group.remove(at: 0))
```
## 闭包和闭包表达式的区别
### 闭包
定义:闭包是自包含的功能代码块,可以在代码中被传递和使用。它能捕获并存储其所在上下文的常量和变量(即“闭合”环境)
种类:
- 全局函数(有名称,不捕获任何值)
- 嵌套函数(有名称,可捕获外曾函数的变量)
- 闭包表达式(匿名,轻量语法,可以捕获上下文变量)
闭包强调的是:捕获上下文的能力。不管是函数还是闭包表达式
### 闭包表达式
定义:是 Swift 中一种简洁的语法格式,用于编写匿名闭包。是闭包的一种实现方式之一(函数和闭包表达式都可以实现闭包)
特点:
- 没有函数名
- 语法清凉,支持参数类型推断、隐式返回、简写参数名(如 $0、$1等特性
- 通常用于作为函数的参数传递
### 总结
- **闭包**是广义概念,包含所有能捕获上下文的代码块(如函数、闭包表达式)。
- **闭包表达式**是闭包的一种具体实现方式,专指用轻量语法编写的匿名闭包。