窥探闭包的内存
闭包:一个函数和它所捕获的变量\常量环境组合起来,称为闭包
-
- 首先先看一下下面这段代码,getFn()返回了一个函数,然后调用4次这个函数,我们来看一下getFn()的内部是怎么用汇编实现的
typealias Fn = (Int) -> Int
func getFn() -> Fn{
func plus(_ i: Int) -> Int{
return i
}
return plus
}
var fn = getFn()
print(fn(1)) 输出1
print(fn(2)) 输出2
print(fn(3)) 输出3
print(fn(4)) 输出4
TestSwift`getFn():
0x100001320 <+0>: pushq %rbp
0x100001321 <+1>: movq %rsp, %rbp
0x100001324 <+4>: leaq 0x15(%rip), %rax ; plus #1 (Swift.Int) -> Swift.Int in TestEnumMemory.getFn() -> (Swift.Int) -> Swift.Int at main.swift:92
-> 0x10000132b <+11>: xorl %ecx, %ecx
0x10000132d <+13>: movl %ecx, %edx
0x10000132f <+15>: popq %rbp
0x100001330 <+16>: retq
-
- 其他汇编可以先忽略,我们重点看一下第三行的汇编
leaq 0x15(%rip), %rax
, 其实看后面的注释我们就可以猜到这是什么意思了,注释是这么写的:; plus #1 (Swift.Int) -> Swift.Int in TestSwift.getFn() -> (Swift.Int) -> Swift.Int at main.swift:92
,意思就是说这句汇编的作用是算出plus函数
的地址并且赋值给rax寄存器
,第一篇的时候说过rax寄存器
经常用来存放函数的返回值,所以赋值给rax寄存器
的目的就是要当做getFn()方法
的返回值,把函数地址返回出去。
-
- 那么函数地址究竟是什么呢?
leap
的意思直接赋值,也就是取出rip寄存器
的值加上0x15
之后,直接把算出来的地址赋值给rax寄存器,我们第一篇的时候讲过rip寄存器
存放的是下一行指令的地址,也就是0x10000132b
,加上0x15
也就是0x100001340
,我们也可以通过LLDB命令register read rax
来读取rax寄存器
的值,结果同样是0x100001340
,所以我们就可以知道getFn()方法
的返回值就是0x100001340
-
- 接下来,我们对上述代码做一点点小的改动,代码如下,我们要注意此时,plus函数捕捉了num变量 ,也就是说
返回的plus函数与num变量的值
组成了闭包,这个时候再来看一下getFn()
的汇编代码
typealias Fn = (Int) -> Int
func getFn() -> Fn{
var num = 0 //增加了这一行
func plus(_ i: Int) -> Int{
num = num + i //增加了这一行
return num //返回值变成了num+i
}
return plus //返回的plus函数和捕获num产生的堆空间形成了闭包
}
var fn = getFn()
print(fn(1)) 输出1
print(fn(2)) 输出3
print(fn(3)) 输出6
print(fn(4)) 输出10
TestSwift`getFn():
0x100001120 <+0>: pushq %rbp
0x100001121 <+1>: movq %rsp, %rbp
0x100001124 <+4>: subq $0x20, %rsp
0x100001128 <+8>: leaq 0x5009(%rip), %rdi
0x10000112f <+15>: movl $0x18, %esi
0x100001134 <+20>: movl $0x7, %edx
0x100001139 <+25>: callq 0x10000543a ; symbol stub for: swift_allocObject
0x10000113e <+30>: movq %rax, %rdx
0x100001141 <+33>: addq $0x10, %rdx
0x100001145 <+37>: movq %rdx, %rsi
0x100001148 <+40>: movq $0x0, 0x10(%rax)
-> 0x100001150 <+48>: movq %rax, %rdi
0x100001153 <+51>: movq %rax, -0x8(%rbp)
0x100001157 <+55>: movq %rdx, -0x10(%rbp)
0x10000115b <+59>: callq 0x1000054b2 ; symbol stub for: swift_retain
0x100001160 <+64>: movq -0x8(%rbp), %rdi
0x100001164 <+68>: movq %rax, -0x18(%rbp)
0x100001168 <+72>: callq 0x1000054ac ; symbol stub for: swift_release
0x10000116d <+77>: movq -0x10(%rbp), %rax
0x100001171 <+81>: leaq 0x1e8(%rip), %rax ; partial apply forwarder for plus #1 (Swift.Int) -> Swift.Int in TestSwift.getFn() -> (Swift.Int) -> Swift.Int at
0x100001178 <+88>: movq -0x8(%rbp), %rdx
0x10000117c <+92>: addq $0x20, %rsp
0x100001180 <+96>: popq %rbp
0x100001181 <+97>: retq
-
- 上下的汇编代码对比可知,仅仅因为组成了闭包,
getFn()
的汇编代码就多了很多,现在我们来观察一下这个汇编的第七句callq 0x10000543a
,这句汇编的注释是symbol stub for: swift_allocObject
,也就是说这句汇编开辟了堆空间,前边说过返回值是存储在rax寄存器
中的,也就是说现在的rax寄存器
存放的是开辟的这段堆空间。
-
- 我们用LLDB命令
register read rax
得到了这段堆空间的地址是rax = 0x0000000100697b10
,然后我们用LLDB命令x/5xg
来查看一下这段堆空间到底存放了什么,存放的数据如下:
(lldb) x/5xg 0x0000000100697b10
0x100697b10: 0x0000000100006138 0x0000000000000002
0x100697b20: 0x0000000000000000 0x0002000000000000
0x100697b30: 0x0000000000000000
-
- 我们大胆猜测一下,这段堆空间究竟存放这什么东西,由于是
plus函数
中捕获了num变量
,之后汇编中才增加了开辟堆空间的指令,所以堆空间的东西一定和num相关,从输出结果是1、3、6、10
可以看出来,访问的num是同一个num,所以很有可能是开辟了一段堆空间来存放num变量的值
,也就是把num的值复制了一份放到了堆空间,方便以后的访问,num是局部变量,在函数调用之后局部变量num就会被销毁掉。
-
- 我们在plus函数内部再打一个断点,观察一下每次
num = num + 1
后 ,刚才那段堆空间的值是否发生了变化
调用fn(1)后,堆空间的数据变成了下面的样子
(lldb) x/5xg 0x0000000100697b10
0x100697b10: 0x0000000100006138 0x0000000200000002
0x100697b20: 0x0000000000000001 0x0002000000000000
0x100697b30: 0x0000000000000000
调用fn(2)后,堆空间的数据变成了下面的样子
(lldb) x/5xg 0x0000000100697b10
0x100697b10: 0x0000000100006138 0x0000000200000002
0x100697b20: 0x0000000000000003 0x0002000000000000
0x100697b30: 0x0000000000000000
调用fn(3)后,堆空间的数据变成了下面的样子
(lldb) x/5xg 0x0000000100697b10
0x100697b10: 0x0000000100006138 0x0000000200000002
0x100697b20: 0x0000000000000006 0x0002000000000000
0x100697b30: 0x0000000000000000
调用fn(4)后,堆空间的数据变成了下面的样子
(lldb) x/5xg 0x0000000100697b10
0x100697b10: 0x0000000100006138 0x0000000200000002
0x100697b20: 0x000000000000000a 0x0002000000000000
0x100697b30: 0x0000000000000000
-
- 从上面可以看出来我们的1、3、6、10,确实在这段堆空间里,也就证实了我们的想法,形成闭包之后
getFn()
内部会开辟一段堆空间,用来存放捕获的变量。
-
- 那么这段堆空间究竟有多大呢,首先我们知道堆空间分配的内存是以16字节为单位的,也就是说是16的倍数,然后我们观察
callq 0x10000543a
分配堆空间的前两句汇编:movl $0x18, %esi
,$0x7, %edx
,我们以前说过rsi寄存器
、rdx寄存器
都是用来存放参数的,而esi寄存器
不就是rsi寄存器的的其中4个字节的空间嘛,所以esi寄存器
中存放的0x18
就是要传给swift_allocObject函数
的参数,同理,edx寄存器
中存放的0x7
也是swift_allocObject函数
的参数,转化成十进制,也就是说把24和7
作为参数给swift_allocObject函数
,可以直接告诉大家,这里的就是堆空间实际占用的字节数,由于堆空间的内存必须是16的倍数,所以这块堆空间一共分配了32个字节。
-
- 其实
闭包产生的这段堆空间
和初始化类对象产生的堆空间
,非常相似,前8个字节存储的都是类型信息,再往后8个字节存储的是引用计数相关,剩下的才是我们要存储的数据,所以上面的闭包代码,你可以认为与下面的代码是等价的。
class Closure{
var num = 0
func plus(_ i: Int) -> Int{
num = num + i
return num
}
}
var closure = Closure()
print(closure.plus(1)) 输出1
print(closure.plus(2)) 输出3
print(closure.plus(3)) 输出6
print(closure.plus(4)) 输出10
-
- 我们要分清
闭包
和闭包表达式
的区别
- 1>. 闭包:一个函数和它所捕获的变量\常量环境组合起来,称为闭包,本文章中,plus函数
和它为了存储num的值而分配的堆空间
组合起来称之为闭包。
- 2>. 闭包表达式:用简洁语法构建内联闭包的方式,可以用闭包表达式来定义一个函数,闭包表达式的格式是这样的:{ (参数列表) -> 返回值类型 in 函数体代码}
15. 总结
-
- 闭包会对用到的局部变量进行捕获,也就是会把局部变量的值放到开辟的堆空间中,以防止局部变量销毁了导致值无法使用
-
- 闭包会对用到的对象引用计数+1,防止对象被提前释放掉,不会再分配堆空间了,。