從零學(xué)習(xí)Swift 06:匯編分析閉包本質(zhì)

總結(jié)

上一篇我們已經(jīng)了解了閉包表達(dá)式和閉包.今天我們就通過匯編分析一下閉包的本質(zhì).

我們通過普通的函數(shù)類型的變量和閉包對(duì)比,看看他們的變量內(nèi)存地址中的數(shù)據(jù)有何不同:

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

普通函數(shù)類型變量內(nèi)存數(shù)據(jù)

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

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)存:

向堆空間申請(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ù):

sum 函數(shù)的內(nèi)部運(yùn)算

現(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ì)向堆空間申請(qǐng)內(nèi)存

看閉包的匯編語言會(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ù),在來看看它的匯編:

沒有向堆空間申請(qǐng)內(nèi)存捕獲變量

通過上面的對(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即可:

自動(dòng)閉包
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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