从零学习Swift 06:汇编分析闭包本质

在上一篇我们已经了解了闭包表达式和闭包.今天我们就通过汇编分析一下闭包的本质.
我们通过普通的函数类型的变量和闭包对比,看看他们的变量内存地址中的数据有何不同:

先用汇编窥探一下普通函数类型变量fn
中的内存:

rax
中存储的sum
函数地址:

结合上面两张图,总结一下:普通函数类型的变量,占用16个字节,它的前8个字节直接存放的就是函数地址,后8个字节是0
接下来我们再用汇编窥探一下闭包变量的内存:
首先调用testClosure()
向堆空间申请内存:

然后会将一个函数地址和堆空间的内存分别放入closure1
的前8个字节和后8个字节:

第三步就是传参调用函数:

进入rax 中存储的 0x0000000100000f00 函数地址:

有上图可以看到,在rax
存放的函数内部,会直接调用sum
函数,进入sum
函数:

现在我们就通过汇编语言分析了闭包的底层是如何捕获变量,以及如何访问堆空间地址的.总结如下:
调用testClosure
函数会向堆空间申请一段内存用来存放捕获的变量/常量,testClosure
函数的返回值占用16个字节,其中前8个字节存放的是一个函数地址,这个函数内部会直接调用sum
函数;后8个字节存放的是向堆空间申请的内存地址;当我们通过闭包调用函数时,会传入两个参数.第一个参数就是调用方法传入的常规参数;第二个参数就是堆空间的内存地址.闭包就是通过传入的堆空间的内存地址访问变量的.
到目前为止我们搞清楚了闭包的本质以及闭包是如何捕获变量,如何访问堆空间内存的.现在我们思考一下,闭包捕获外层函数的变量时,是什么时候开始捕获的?
看看一下代码,num
的值是什么时候被捕获的?
func testClosure() -> (Int) -> Int{
var num = 0
func sum(_ a: Int) -> Int{
num += a
return num
}
return sum
}
var closure1 = testClosure()
closure1(1)
closure1(2)
我们在函数返回之前,修改一下num
的值,看看结果:

最后的结果是11 , 13.说明最后捕获的是num = 10
的值.也就是说在函数返回之前会捕获num
的值.为什么会这样呢?我们先看一下它的汇编:

看闭包的汇编语言会发现,闭包会向堆空间申请内存,并且捕获num
的值.
现在我们把testClosure()
的返回值修改如下:
func testClosure() -> (Int) -> Int{
var num = 0
func sum(_ a: Int) -> Int{
num += a
return num
}
num = 10
return {
(v1: Int) -> Int in
return v1
}
}
让testClosure ()
函数不返回sum
函数,在来看看它的汇编:

通过上面的对比可以发现,只有当返回的函数访问了外层函数的变量时,才会捕获变量.所以捕获变量的根本就是看函数的返回值.所以,现在可以下结论:闭包什么时候捕获变量? 当函数返回之前捕获.
看看下面代码运行结果是什么:
typealias Fn = (Int) -> (Int, Int)
func getFn() -> (Fn, Fn){
var num1: Int = 0
var num2: Int = 0
func plus(_ i: Int) -> (Int, Int){
num1 += I
num2 += i << 1
return (num1, num2)
}
func minus(_ i: Int) -> (Int, Int){
num1 -= I
num2 -= i << 1
return (num1, num2)
}
return (plus, minus)
}
var (plus, minus) = getFn()
print(plus(2))//2,4
print(minus(4))//-2, -4
print(plus(6))//4, 8
print(minus(3))//1,2
从结果上可以看到,plus(), minus()
都共用一个num1, num2
.我们看看它的汇编:


从上面两幅图可以看到,调用
getFn()会向堆空间申请两块内存分别存放
num1,
num2.并且把
num1,
num2的地址一起放到另一个内存中,然后再存放到闭包对象中.之前闭包只有一个返回值的时候,闭包对象的内存中直接存放的就是用来捕获的堆空间地址.
自动闭包
//自动闭包
func getNum() -> Int{
var num = 10
num -= 1
print("getNum")
return num
}
//获取第一个大于0的书
func getPositiveNum(_ num1: Int, _ num2: Int) -> Int{
return num1 > 0 ? num1 : num2
}
print(getPositiveNum(10, getNum()))
//打印结果
getNum
10
像上面的代买,获取第一个大于0的数,如果第一个参数符合条件,那么第二个参数的函数其实就无需执行了,但是按照上面的写法,第二个参数的函数每次都会执行.
那怎么才能让第二个参数在有需要的时候才去执行呢?只需要把第二个参数定义成函数类型的参数即可:

当然也可使用闭包表达式的写法来实现,别忘了闭包表达式也是定义函数的一种方式:

但是像这种闭包表达式,每次都要我们手动写大括号{}
,过于繁琐.编译器考虑到程序员的烦恼,就有了自动闭包
这种语法糖:
使用自动闭包实现只需要在参数标签后面加上autoclosure
即可:
