汇编窥探Swift底层(四):闭包

窥探闭包的内存

闭包:一个函数和它所捕获的变量\常量环境组合起来,称为闭包

    1. 首先先看一下下面这段代码,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
    1. 下面就是getFn()方法的汇编代码
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   

    1. 其他汇编可以先忽略,我们重点看一下第三行的汇编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()方法的返回值,把函数地址返回出去。
    1. 那么函数地址究竟是什么呢?leap的意思直接赋值,也就是取出rip寄存器的值加上0x15之后,直接把算出来的地址赋值给rax寄存器,我们第一篇的时候讲过rip寄存器存放的是下一行指令的地址,也就是0x10000132b,加上0x15也就是0x100001340,我们也可以通过LLDB命令register read rax来读取rax寄存器的值,结果同样是0x100001340,所以我们就可以知道getFn()方法的返回值就是0x100001340
    1. 接下来,我们对上述代码做一点点小的改动,代码如下,我们要注意此时,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
    1. 此时,getFn()的汇编代码是下面这个样子
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   

    1. 上下的汇编代码对比可知,仅仅因为组成了闭包,getFn()的汇编代码就多了很多,现在我们来观察一下这个汇编的第七句callq 0x10000543a,这句汇编的注释是symbol stub for: swift_allocObject,也就是说这句汇编开辟了堆空间,前边说过返回值是存储在rax寄存器中的,也就是说现在的rax寄存器存放的是开辟的这段堆空间。
    1. 我们用LLDB命令register read rax得到了这段堆空间的地址是rax = 0x0000000100697b10,然后我们用LLDB命令x/5xg来查看一下这段堆空间到底存放了什么,存放的数据如下:
(lldb) x/5xg  0x0000000100697b10
0x100697b10: 0x0000000100006138 0x0000000000000002
0x100697b20: 0x0000000000000000 0x0002000000000000
0x100697b30: 0x0000000000000000
    1. 我们大胆猜测一下,这段堆空间究竟存放这什么东西,由于是plus函数中捕获了num变量,之后汇编中才增加了开辟堆空间的指令,所以堆空间的东西一定和num相关,从输出结果是1、3、6、10可以看出来,访问的num是同一个num,所以很有可能是开辟了一段堆空间来存放num变量的值,也就是把num的值复制了一份放到了堆空间,方便以后的访问,num是局部变量,在函数调用之后局部变量num就会被销毁掉。
    1. 我们在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. 从上面可以看出来我们的1、3、6、10,确实在这段堆空间里,也就证实了我们的想法,形成闭包之后getFn()内部会开辟一段堆空间,用来存放捕获的变量
    1. 那么这段堆空间究竟有多大呢,首先我们知道堆空间分配的内存是以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个字节。
    1. 其实闭包产生的这段堆空间初始化类对象产生的堆空间,非常相似,前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. 我们要分清闭包闭包表达式区别
    - 1>. 闭包:一个函数和它所捕获的变量\常量环境组合起来,称为闭包,本文章中,plus函数和它为了存储num的值而分配的堆空间组合起来称之为闭包。
    - 2>. 闭包表达式:用简洁语法构建内联闭包的方式,可以用闭包表达式来定义一个函数,闭包表达式的格式是这样的:{ (参数列表) -> 返回值类型 in 函数体代码}
15. 总结
    1. 闭包会对用到的局部变量进行捕获,也就是会把局部变量的值放到开辟的堆空间中,以防止局部变量销毁了导致值无法使用
    1. 闭包会对用到的对象引用计数+1,防止对象被提前释放掉,不会再分配堆空间了,。

你可能感兴趣的:(汇编窥探Swift底层(四):闭包)