汇编窥探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 <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,防止对象被提前释放掉,不会再分配堆空间了,。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352