# 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 Byte,Bool 占用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)) ``` 代码段: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 ``` 简单修改下代码 ```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 ``` 可以看到上面有 `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` 下面下个断点 第一次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 Debug- Debug Workflow - View Memory 查看内存信息`0x0000600000210000`。此时还没值。 敏感点,查看到字面量1,给汇编代码15行加断点,执行完15行,继续查看内存信息 可以看到内存数据发生了改变。绿色框内有了值1。 第二次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 Debug- Debug Workflow - View Memory 查看内存信息`0x00006000002042c0`。此时还没值。 敏感点,查看到字面量1,给汇编代码15行加断点,执行完15行,继续查看内存信息 第三次:可以看到 `alloc` 在堆空间申请后的内存被存放到寄存器 `rax` 中,此时只是申请内存,没有赋值的。利用 Debug- Debug Workflow - View Memory 查看内存信息`0x000060000020d460`。此时还没值。 敏感点,查看到字面量1,给汇编代码15行加断点,执行完15行,继续查看内存信息 打印结果也说明了问题,因为调用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` 处下断点,可以看到下面汇编 我们知道 `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位。符合猜想。 直奔主题,研究闭包内存 可以看到在调用完第六行的函数后将寄存器 `rax`、`rdx` 里的值取出来使用了。进入函数内部看看发生了什么,LLDB 输入 `si` 可以看到 `rax` 里面存放了 `plus` 函数地址。第7行汇编是抑或运算,2个 `ecx` 抑或,结果为0,写入到 `ecx` 里。然后第8行汇编将 `ecx` 里的0写入到 `edx`,`edx` 也就是 `rdx`。走完第6行的汇编,继续看第7、8行 将第6行函数的返回结果 `rax` 里的函数地址赋值给 `rip +0x89e7 = 0x100003819 + 0x89e7 = 0x10000C200 `, 第7行的`rdx` 的值赋值个给 `rip +0x89e8 = 0x100003820 + 0x89e8 = 0x10000C208`。 也就是 fn1 前8个字节存放 plus 的函数地址,后8个字节存放0. 继续对比实验,查看闭包放了什么东西(注意和上面的实验不同,下面存在闭包) 基本可以断定:函数会返回一个长度为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 ` 继续走,走到真正的 `plus` 方法内,可以看到函数地址是 `0x100003970` 。所以返回的 `rax` 里面可能是间接调用真正的 `plus` 函数的。 问题变得微妙起来了,`getFn` 方法返回一个地址,占用16个字节,但是前8个字节存储 `plus` 方法地址,后8个字节存储闭包捕获后在堆上申请内存存放的数据地址。那如何根据一个地址的前8位去真正调用方法? Tips:由于地址是动态生成的,所以真正去调用 plus 的时候一定不是写死的地址,一定是动态生成的。这种属于间接调用。 - call 后面直接加一个函数地址,属于直接调用。类似 `callq 0x100003970` - call 后面的函数地址不是固定的,而是动态生成的叫间接调用。在 `at&t` 汇编里面,间接调用前面要加 `*` 。类似 `callq *%rax`,意思是从 `%rax` 里面取出一个地址值出来,当成函数地址,去调用 顺着思路,分析下汇编: 我们看到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` 可以看到在方法内部,第6行汇编处,直接调用一个代码段的函数地址 `jmp 0x1000039f0 ` 指令 `jmp`、`call` 的区别在于: - `jmp` 指令控制程序直接跳转到目标地址执行程序,程序总是顺序执行,指令本身无堆栈操作过程。 - `call` 指令跳转到指定目标地址执行子程序,执行完子程序后,会返回 `call` 指令的下一条指令处执行程序,执行 `call` 指令有堆栈操作过程 LLDB 输入 `si`,可以看到是 `plus` 函数真正的地址 `fn1` 函数调用的时候,参数如何传递? 汇编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 内部 可以看到 `movq %r13, %rsi` 将寄存器 r13 的值写入到 `rsi` 了。目前为止:`rdi` 保存参数1,`rsi` 保存堆地址值。 继续输入 `si` 可以看到汇编的第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相加。 可以看到第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 的时候操作的是被捕获的新的一个堆地址空间所指向的变量。 ## 自动闭包 自动闭包是一种自动创建的,用来把作为实际参数传递给函数的表达式打包的闭包。它不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值 这个语法的好处在于通过写普通表达式代替显示闭包,而使你省略包围函数的形式参数的括号. 比如系统的断言 `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`,在调用函数的时候,传参没有加 `{}` 编译器是会报错的 正确的做法是:要么加 `@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)) ```