窺探閉包的內(nèi)存
閉包:一個(gè)函數(shù)和它所捕獲的變量\常量環(huán)境組合起來,稱為閉包
- 首先先看一下下面這段代碼绿语,getFn()返回了一個(gè)函數(shù)钮孵,然后調(diào)用4次這個(gè)函數(shù),我們來看一下getFn()的內(nèi)部是怎么用匯編實(shí)現(xiàn)的
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
- 其他匯編可以先忽略睡陪,我們重點(diǎn)看一下第三行的匯編
leaq 0x15(%rip), %rax
, 其實(shí)看后面的注釋我們就可以猜到這是什么意思了,注釋是這么寫的:; plus #1 (Swift.Int) -> Swift.Int in TestSwift.getFn() -> (Swift.Int) -> Swift.Int at main.swift:92
匿情,意思就是說這句匯編的作用是算出plus函數(shù)
的地址并且賦值給rax寄存器
兰迫,第一篇的時(shí)候說過rax寄存器
經(jīng)常用來存放函數(shù)的返回值,所以賦值給rax寄存器
的目的就是要當(dāng)做getFn()方法
的返回值炬称,把函數(shù)地址返回出去汁果。
- 那么函數(shù)地址究竟是什么呢?
leap
的意思直接賦值玲躯,也就是取出rip寄存器
的值加上0x15
之后据德,直接把算出來的地址賦值給rax寄存器,我們第一篇的時(shí)候講過rip寄存器
存放的是下一行指令的地址跷车,也就是0x10000132b
棘利,加上0x15
也就是0x100001340
,我們也可以通過LLDB命令register read rax
來讀取rax寄存器
的值朽缴,結(jié)果同樣是0x100001340
赡译,所以我們就可以知道getFn()方法
的返回值就是0x100001340
- 接下來,我們對(duì)上述代碼做一點(diǎn)點(diǎn)小的改動(dòng)不铆,代碼如下蝌焚,我們要注意此時(shí),plus函數(shù)捕捉了num變量 誓斥,也就是說
返回的plus函數(shù)與num變量的值
組成了閉包只洒,這個(gè)時(shí)候再來看一下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函數(shù)和捕獲num產(chǎn)生的堆空間形成了閉包
}
var fn = getFn()
print(fn(1)) 輸出1
print(fn(2)) 輸出3
print(fn(3)) 輸出6
print(fn(4)) 輸出10
- 此時(shí),
getFn()
的匯編代碼是下面這個(gè)樣子
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
- 上下的匯編代碼對(duì)比可知劳坑,僅僅因?yàn)榻M成了閉包毕谴,
getFn()
的匯編代碼就多了很多,現(xiàn)在我們來觀察一下這個(gè)匯編的第七句callq 0x10000543a
距芬,這句匯編的注釋是symbol stub for: swift_allocObject
涝开,也就是說這句匯編開辟了堆空間,前邊說過返回值是存儲(chǔ)在rax寄存器
中的框仔,也就是說現(xiàn)在的rax寄存器
存放的是開辟的這段堆空間舀武。
- 我們用LLDB命令
register read rax
得到了這段堆空間的地址是rax = 0x0000000100697b10
,然后我們用LLDB命令x/5xg
來查看一下這段堆空間到底存放了什么离斩,存放的數(shù)據(jù)如下:
(lldb) x/5xg 0x0000000100697b10
0x100697b10: 0x0000000100006138 0x0000000000000002
0x100697b20: 0x0000000000000000 0x0002000000000000
0x100697b30: 0x0000000000000000
- 我們大膽猜測(cè)一下银舱,這段堆空間究竟存放這什么東西瘪匿,由于是
plus函數(shù)
中捕獲了num變量
,之后匯編中才增加了開辟堆空間的指令寻馏,所以堆空間的東西一定和num相關(guān)棋弥,從輸出結(jié)果是1、3诚欠、6顽染、10
可以看出來,訪問的num是同一個(gè)num轰绵,所以很有可能是開辟了一段堆空間來存放num變量的值
粉寞,也就是把num的值復(fù)制了一份放到了堆空間,方便以后的訪問藏澳,num是局部變量仁锯,在函數(shù)調(diào)用之后局部變量num就會(huì)被銷毀掉耀找。
- 我們?cè)趐lus函數(shù)內(nèi)部再打一個(gè)斷點(diǎn)翔悠,觀察一下每次
num = num + 1
后 ,剛才那段堆空間的值是否發(fā)生了變化
調(diào)用fn(1)后野芒,堆空間的數(shù)據(jù)變成了下面的樣子
(lldb) x/5xg 0x0000000100697b10
0x100697b10: 0x0000000100006138 0x0000000200000002
0x100697b20: 0x0000000000000001 0x0002000000000000
0x100697b30: 0x0000000000000000
調(diào)用fn(2)后蓄愁,堆空間的數(shù)據(jù)變成了下面的樣子
(lldb) x/5xg 0x0000000100697b10
0x100697b10: 0x0000000100006138 0x0000000200000002
0x100697b20: 0x0000000000000003 0x0002000000000000
0x100697b30: 0x0000000000000000
調(diào)用fn(3)后,堆空間的數(shù)據(jù)變成了下面的樣子
(lldb) x/5xg 0x0000000100697b10
0x100697b10: 0x0000000100006138 0x0000000200000002
0x100697b20: 0x0000000000000006 0x0002000000000000
0x100697b30: 0x0000000000000000
調(diào)用fn(4)后狞悲,堆空間的數(shù)據(jù)變成了下面的樣子
(lldb) x/5xg 0x0000000100697b10
0x100697b10: 0x0000000100006138 0x0000000200000002
0x100697b20: 0x000000000000000a 0x0002000000000000
0x100697b30: 0x0000000000000000
- 從上面可以看出來我們的1撮抓、3、6摇锋、10丹拯,確實(shí)在這段堆空間里,也就證實(shí)了我們的想法荸恕,形成閉包之后
getFn()
內(nèi)部會(huì)開辟一段堆空間乖酬,用來存放捕獲的變量。
- 那么這段堆空間究竟有多大呢融求,首先我們知道堆空間分配的內(nèi)存是以16字節(jié)為單位的咬像,也就是說是16的倍數(shù),然后我們觀察
callq 0x10000543a
分配堆空間的前兩句匯編:movl $0x18, %esi
生宛,$0x7, %edx
县昂,我們以前說過rsi寄存器
、rdx寄存器
都是用來存放參數(shù)的陷舅,而esi寄存器
不就是rsi寄存器的的其中4個(gè)字節(jié)的空間嘛倒彰,所以esi寄存器
中存放的0x18
就是要傳給swift_allocObject函數(shù)
的參數(shù),同理莱睁,edx寄存器
中存放的0x7
也是swift_allocObject函數(shù)
的參數(shù)狸驳,轉(zhuǎn)化成十進(jìn)制预明,也就是說把24和7
作為參數(shù)給swift_allocObject函數(shù)
,可以直接告訴大家耙箍,這里的就是堆空間實(shí)際占用的字節(jié)數(shù)撰糠,由于堆空間的內(nèi)存必須是16的倍數(shù),所以這塊堆空間一共分配了32個(gè)字節(jié)辩昆。
- 其實(shí)
閉包產(chǎn)生的這段堆空間
和初始化類對(duì)象產(chǎn)生的堆空間
阅酪,非常相似,前8個(gè)字節(jié)存儲(chǔ)的都是類型信息汁针,再往后8個(gè)字節(jié)存儲(chǔ)的是引用計(jì)數(shù)相關(guān)术辐,剩下的才是我們要存儲(chǔ)的數(shù)據(jù),所以上面的閉包代碼施无,你可以認(rèn)為與下面的代碼是等價(jià)的辉词。
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
-
- 我們要分清
閉包
和閉包表達(dá)式
的區(qū)別
- 1>. 閉包:一個(gè)函數(shù)和它所捕獲的變量\常量環(huán)境組合起來,稱為閉包猾骡,本文章中瑞躺,plus函數(shù)
和它為了存儲(chǔ)num的值而分配的堆空間
組合起來稱之為閉包。
- 2>. 閉包表達(dá)式:用簡(jiǎn)潔語(yǔ)法構(gòu)建內(nèi)聯(lián)閉包的方式兴想,可以用閉包表達(dá)式來定義一個(gè)函數(shù)幢哨,閉包表達(dá)式的格式是這樣的:{ (參數(shù)列表) -> 返回值類型 in 函數(shù)體代碼}
15. 總結(jié)
- 閉包會(huì)對(duì)用到的局部變量進(jìn)行捕獲,也就是會(huì)把局部變量的值放到開辟的堆空間中嫂便,以防止局部變量銷毀了導(dǎo)致值無(wú)法使用
- 閉包會(huì)對(duì)用到的對(duì)象引用計(jì)數(shù)+1捞镰,防止對(duì)象被提前釋放掉,不會(huì)再分配堆空間了毙替,岸售。