美文网首页swift开发知识收集
汇编窥探Swift底层(四):闭包

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

作者: 冰风v落叶 | 来源:发表于2020-02-27 17:03 被阅读0次

窥探闭包的内存

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

    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 <compiler-generated>
    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底层(四):闭包

    窥探闭包的内存 闭包:一个函数和它所捕获的变量\常量环境组合起来,称为闭包 首先先看一下下面这段代码,getFn(...

  • 汇编窥探Swift底层(一):汇编基础

    汇编基础 一、程序的本质 1. 程序的执行过程 如下图所示,执行软件的时候,会将软件从硬盘装载到内存中,然后由C...

  • 汇编窥探Swift底层(二):枚举

    窥探枚举的内存 我们先来看一个枚举,代码如下,初始化一个枚举之后,打印变量memory的内存地址,以便窥探内存究竟...

  • 汇编窥探Swift底层(六):数组

    窥探数组的内存 我们知道Swift中的数组是结构体,也就是值类型,那么数组的内存究竟是什么样子呢,会不会像自定义的...

  • Swift汇编分析闭包-调用原理

    在《Swift汇编分析闭包-内存布局》[https://www.jianshu.com/p/bc5c595950c...

  • Swift-闭包

    Swift 闭包 函数 ()->() Swift 中的闭包和 Objective-C 中的 block 类似,闭包...

  • Swift闭包和函数

    函数在Swift中只是一种特殊的闭包,闭包在Swift语言中是一等公民,支持闭包嵌套和闭包传递。Swift中的闭包...

  • Swift底层探索:闭包

    闭包是可以在你的代码中被传递和引用的功能性独立代码块。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址...

  • Swift 闭包底层探究

    闭包 闭包:一个函数和它所捕获的变量\常量环境组合起来,称为闭包(一般它捕获的是外层函数的局部变量\常量) 可以把...

  • Swift汇编分析闭包扩展

    在上篇文章中我们分析了闭包中捕获了一个外部变量时其底层的参数传递逻辑,那么如果捕获两个外部变量时呢,其又是怎么传参...

网友评论

    本文标题:汇编窥探Swift底层(四):闭包

    本文链接:https://www.haomeiwen.com/subject/dpwmchtx.html