匯編窺探Swift底層(四):閉包

窺探閉包的內(nèi)存

閉包:一個函數(shù)和它所捕獲的變量\常量環(huán)境組合起來,稱為閉包

    1. 首先先看一下下面這段代碼,getFn()返回了一個函數(shù),然后調(diào)用4次這個函數(shù),我們來看一下getFn()的內(nèi)部是怎么用匯編實現(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
    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函數(shù)的地址并且賦值給rax寄存器 ,第一篇的時候說過rax寄存器經(jīng)常用來存放函數(shù)的返回值,所以賦值給rax寄存器的目的就是要當(dāng)做getFn()方法的返回值,把函數(shù)地址返回出去。
    1. 那么函數(shù)地址究竟是什么呢?leap的意思直接賦值,也就是取出rip寄存器的值加上0x15之后,直接把算出來的地址賦值給rax寄存器,我們第一篇的時候講過rip寄存器存放的是下一行指令的地址,也就是0x10000132b,加上0x15也就是0x100001340,我們也可以通過LLDB命令register read rax來讀取rax寄存器的值,結(jié)果同樣是0x100001340,所以我們就可以知道getFn()方法的返回值就是0x100001340
    1. 接下來,我們對上述代碼做一點點小的改動,代碼如下,我們要注意此時,plus函數(shù)捕捉了num變量 ,也就是說返回的plus函數(shù)與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函數(shù)和捕獲num產(chǎn)生的堆空間形成了閉包
}

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()的匯編代碼就多了很多,現(xiàn)在我們來觀察一下這個匯編的第七句callq 0x10000543a,這句匯編的注釋是symbol stub for: swift_allocObject,也就是說這句匯編開辟了堆空間,前邊說過返回值是存儲在rax寄存器中的,也就是說現(xiàn)在的rax寄存器存放的是開辟的這段堆空間。
    1. 我們用LLDB命令register read rax得到了這段堆空間的地址是rax = 0x0000000100697b10,然后我們用LLDB命令x/5xg來查看一下這段堆空間到底存放了什么,存放的數(shù)據(jù)如下:
(lldb) x/5xg  0x0000000100697b10
0x100697b10: 0x0000000100006138 0x0000000000000002
0x100697b20: 0x0000000000000000 0x0002000000000000
0x100697b30: 0x0000000000000000
    1. 我們大膽猜測一下,這段堆空間究竟存放這什么東西,由于是plus函數(shù)中捕獲了num變量,之后匯編中才增加了開辟堆空間的指令,所以堆空間的東西一定和num相關(guān),從輸出結(jié)果是1、3、6、10可以看出來,訪問的num是同一個num,所以很有可能是開辟了一段堆空間來存放num變量的值,也就是把num的值復(fù)制了一份放到了堆空間,方便以后的訪問,num是局部變量,在函數(shù)調(diào)用之后局部變量num就會被銷毀掉。
    1. 我們在plus函數(shù)內(nèi)部再打一個斷點,觀察一下每次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. 從上面可以看出來我們的1、3、6、10,確實在這段堆空間里,也就證實了我們的想法,形成閉包之后getFn()內(nèi)部會開辟一段堆空間,用來存放捕獲的變量。
    1. 那么這段堆空間究竟有多大呢,首先我們知道堆空間分配的內(nèi)存是以16字節(jié)為單位的,也就是說是16的倍數(shù),然后我們觀察callq 0x10000543a分配堆空間的前兩句匯編:movl $0x18, %esi,$0x7, %edx,我們以前說過rsi寄存器、rdx寄存器都是用來存放參數(shù)的,而esi寄存器不就是rsi寄存器的的其中4個字節(jié)的空間嘛,所以esi寄存器中存放的0x18就是要傳給swift_allocObject函數(shù)的參數(shù),同理,edx寄存器中存放的0x7也是swift_allocObject函數(shù)的參數(shù),轉(zhuǎn)化成十進制,也就是說把24和7作為參數(shù)給swift_allocObject函數(shù),可以直接告訴大家,這里的就是堆空間實際占用的字節(jié)數(shù),由于堆空間的內(nèi)存必須是16的倍數(shù),所以這塊堆空間一共分配了32個字節(jié)。
    1. 其實閉包產(chǎn)生的這段堆空間初始化類對象產(chǎn)生的堆空間,非常相似,前8個字節(jié)存儲的都是類型信息,再往后8個字節(jié)存儲的是引用計數(shù)相關(guān),剩下的才是我們要存儲的數(shù)據(jù),所以上面的閉包代碼,你可以認(rèn)為與下面的代碼是等價的。
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. 我們要分清閉包閉包表達式區(qū)別
    - 1>. 閉包:一個函數(shù)和它所捕獲的變量\常量環(huán)境組合起來,稱為閉包,本文章中,plus函數(shù)和它為了存儲num的值而分配的堆空間組合起來稱之為閉包。
    - 2>. 閉包表達式:用簡潔語法構(gòu)建內(nèi)聯(lián)閉包的方式,可以用閉包表達式來定義一個函數(shù),閉包表達式的格式是這樣的:{ (參數(shù)列表) -> 返回值類型 in 函數(shù)體代碼}
15. 總結(jié)
    1. 閉包會對用到的局部變量進行捕獲,也就是會把局部變量的值放到開辟的堆空間中,以防止局部變量銷毀了導(dǎo)致值無法使用
    1. 閉包會對用到的對象引用計數(shù)+1,防止對象被提前釋放掉,不會再分配堆空間了,。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容