
在上一篇我們已經(jīng)了解了閉包表達(dá)式和閉包.今天我們就通過匯編分析一下閉包的本質(zhì).
我們通過普通的函數(shù)類型的變量和閉包對(duì)比,看看他們的變量內(nèi)存地址中的數(shù)據(jù)有何不同:

先用匯編窺探一下普通函數(shù)類型變量fn中的內(nèi)存:

rax中存儲(chǔ)的sum函數(shù)地址:

結(jié)合上面兩張圖,總結(jié)一下:普通函數(shù)類型的變量,占用16個(gè)字節(jié),它的前8個(gè)字節(jié)直接存放的就是函數(shù)地址,后8個(gè)字節(jié)是0
接下來我們?cè)儆脜R編窺探一下閉包變量的內(nèi)存:
首先調(diào)用testClosure()向堆空間申請(qǐng)內(nèi)存:

然后會(huì)將一個(gè)函數(shù)地址和堆空間的內(nèi)存分別放入closure1的前8個(gè)字節(jié)和后8個(gè)字節(jié):

第三步就是傳參調(diào)用函數(shù):

進(jìn)入rax 中存儲(chǔ)的 0x0000000100000f00 函數(shù)地址:

由上圖可以看到,在rax存放的函數(shù)內(nèi)部,會(huì)直接調(diào)用sum函數(shù),進(jìn)入sum函數(shù):

現(xiàn)在我們就通過匯編語言分析了閉包的底層是如何捕獲變量,以及如何訪問堆空間地址的.總結(jié)如下:
調(diào)用testClosure函數(shù)會(huì)向堆空間申請(qǐng)一段內(nèi)存用來存放捕獲的變量/常量,testClosure函數(shù)的返回值占用16個(gè)字節(jié),其中前8個(gè)字節(jié)存放的是一個(gè)函數(shù)地址,這個(gè)函數(shù)內(nèi)部會(huì)直接調(diào)用sum函數(shù);后8個(gè)字節(jié)存放的是向堆空間申請(qǐng)的內(nèi)存地址;當(dāng)我們通過閉包調(diào)用函數(shù)時(shí),會(huì)傳入兩個(gè)參數(shù).第一個(gè)參數(shù)就是調(diào)用方法傳入的常規(guī)參數(shù);第二個(gè)參數(shù)就是堆空間的內(nèi)存地址.閉包就是通過傳入的堆空間的內(nèi)存地址訪問變量的.
到目前為止我們搞清楚了閉包的本質(zhì)以及閉包是如何捕獲變量,如何訪問堆空間內(nèi)存的.現(xiàn)在我們思考一下,閉包捕獲外層函數(shù)的變量時(shí),是什么時(shí)候開始捕獲的?
看看一下代碼,num的值是什么時(shí)候被捕獲的?
func testClosure() -> (Int) -> Int{
var num = 0
func sum(_ a: Int) -> Int{
num += a
return num
}
return sum
}
var closure1 = testClosure()
closure1(1)
closure1(2)
我們?cè)诤瘮?shù)返回之前,修改一下num的值,看看結(jié)果:

最后的結(jié)果是11 , 13.說明最后捕獲的是num = 10的值.也就是說在函數(shù)返回之前會(huì)捕獲num的值.為什么會(huì)這樣呢?我們先看一下它的匯編:

看閉包的匯編語言會(huì)發(fā)現(xiàn),閉包會(huì)向堆空間申請(qǐng)內(nèi)存,并且捕獲num的值.
現(xiàn)在我們把testClosure()的返回值修改如下:
func testClosure() -> (Int) -> Int{
var num = 0
func sum(_ a: Int) -> Int{
num += a
return num
}
num = 10
return {
(v1: Int) -> Int in
return v1
}
}
讓testClosure ()函數(shù)不返回sum函數(shù),在來看看它的匯編:

通過上面的對(duì)比可以發(fā)現(xiàn),只有當(dāng)返回的函數(shù)訪問了外層函數(shù)的變量時(shí),才會(huì)捕獲變量.所以捕獲變量的根本就是看函數(shù)的返回值.所以,現(xiàn)在可以下結(jié)論:閉包什么時(shí)候捕獲變量? 當(dāng)函數(shù)返回之前捕獲.
看看下面代碼運(yùn)行結(jié)果是什么:
typealias Fn = (Int) -> (Int, Int)
func getFn() -> (Fn, Fn){
var num1: Int = 0
var num2: Int = 0
func plus(_ i: Int) -> (Int, Int){
num1 += i
num2 += i << 1
return (num1, num2)
}
func minus(_ i: Int) -> (Int, Int){
num1 -= i
num2 -= i << 1
return (num1, num2)
}
return (plus, minus)
}
var (plus, minus) = getFn()
print(plus(2))//2,4
print(minus(4))//-2, -4
print(plus(6))//4, 8
print(minus(3))//1,2
從結(jié)果上可以看到,plus(), minus()都共用一個(gè)num1, num2.我們看看它的匯編:


從上面兩幅圖可以看到,調(diào)用getFn()會(huì)向堆空間申請(qǐng)兩塊內(nèi)存分別存放num1,num2.并且把num1,num2的地址一起放到另一個(gè)內(nèi)存中,然后再存放到閉包對(duì)象中.之前閉包只有一個(gè)返回值的時(shí)候,閉包對(duì)象的內(nèi)存中直接存放的就是用來捕獲的堆空間地址.
自動(dòng)閉包
//自動(dòng)閉包
func getNum() -> Int{
var num = 10
num -= 1
print("getNum")
return num
}
//獲取第一個(gè)大于0的數(shù)
func getPositiveNum(_ num1: Int, _ num2: Int) -> Int{
return num1 > 0 ? num1 : num2
}
print(getPositiveNum(10, getNum()))
//打印結(jié)果
getNum
10
像上面的代買,獲取第一個(gè)大于0的數(shù),如果第一個(gè)參數(shù)符合條件,那么第二個(gè)參數(shù)的函數(shù)其實(shí)就無需執(zhí)行了,但是按照上面的寫法,第二個(gè)參數(shù)的函數(shù)每次都會(huì)執(zhí)行.
那怎么才能讓第二個(gè)參數(shù)在有需要的時(shí)候才去執(zhí)行呢?只需要把第二個(gè)參數(shù)定義成函數(shù)類型的參數(shù)即可:

當(dāng)然也可使用閉包表達(dá)式的寫法來實(shí)現(xiàn),別忘了閉包表達(dá)式也是定義函數(shù)的一種方式:

但是像這種閉包表達(dá)式,每次都要我們手動(dòng)寫大括號(hào){},過于繁瑣.編譯器考慮到程序員的煩惱,就有了自動(dòng)閉包這種語法糖:
使用自動(dòng)閉包實(shí)現(xiàn)只需要在參數(shù)標(biāo)簽后面加上autoclosure即可:
