Swift底層原理探索5----閉包

閉包表達(dá)式(Closure Expression)

在Swift中咏瑟,可以通過func定義一個函數(shù),也可以通過閉包表達(dá)式定義一個函數(shù)

//通過func關(guān)鍵字定義函數(shù)
func sum(_ v1: Int, _ v2: Int) -> Int{v1 + v2}
//通過閉包表達(dá)式定義函數(shù)
var fn = {
    (v1: Int, v2: Int) -> Int in
    return v1 + v2
}

fn(10, 20)
//閉包表達(dá)式結(jié)構(gòu)
{
    (參數(shù)列表) -> 返回值類型 in  
    函數(shù)代碼
}

這里的in 是用來區(qū)分 函數(shù)類型信息函數(shù)體代碼的仇让,上面的書寫方法不是必須的那槽,也可以不換行蜈膨,都寫在一行里面屿笼,但是這樣不便于代碼的閱讀荒给。

閉包表達(dá)式可以在定義的時候直接使用

{
    (v1: Int, v2: Int) -> Int in
    return v1 + v2
}(10, 20)

另外需要注意一點就是閉包表達(dá)式在使用的時候,不需要寫上那些在定義里面的出現(xiàn)的參數(shù)標(biāo)簽刁卜。

閉包表達(dá)式的簡寫

func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
    print(fn(v1, v2))
}

正常寫法
exec(v1: 10, v2: 20, fn: {
    (v1: Int, v2: Int) -> Int in
    return v1 + v2
})
省略參數(shù)類型
exec(v1: 10, v2: 20, fn: {
    v1, v2 in return v1 + v2
})
如果函數(shù)題代碼就是一個單一的表達(dá)式志电,可以省略return關(guān)鍵字
exec(v1: 10, v2: 20, fn: {
    v1, v2 in v1 + v2
})
省略參數(shù)名,通過$0,$1,$2...來獲取按順序獲取參數(shù)
exec(v1: 10, v2: 20, fn: {
    $0 + $1
})
如果函數(shù)體的表達(dá)式比較簡單蛔趴,比如這里的+運算挑辆,那么可以直接寫一個+號即可,編譯器也可以推斷來
exec(v1: 10, v2: 20, fn: +)

閉包表達(dá)式的簡寫需要我們根據(jù)實際情況孝情,確定一個合理的書寫簡化成都鱼蝉,不能為了減少代碼量而一味使用最簡便寫法,這樣會降低代碼的可讀性箫荡,提高維護成本魁亦,凡事過猶不及。

尾隨閉包

  • 如果將一個很長的閉包表達(dá)式作為函數(shù)的最后一個實參羔挡,使用尾隨閉包可以增強函數(shù)的可讀性
    尾隨閉包是一個被書寫在函數(shù)調(diào)用括號外面(后面)的閉包表達(dá)式
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
    print(fn(v1, v2))
}
exec(v1: 10, v2: 20) {
    $0 + $1
}
  • 如果閉包表達(dá)式是函數(shù)的唯一實參洁奈,而且使用了尾隨閉包的語法,那就不需要在函數(shù)名后邊寫圓括號
func exec(fn: (Int, Int) -> Int) {
    print(fn(1, 2))
}

exec(fn: { $0 + $1})
exec(){ $0 + $1}
exec { $0 + $1}

示例 - 數(shù)組的排序

---> 數(shù)組排序方法的定義
func sort(by areInIncreaseingOrder: (Element, Element) -> Bool)

func cmp(i1: Int, i2: Int) -> Bool {
    return i1 > i2
}
var nums = [11,4,7,23,13,90]
nums.sort(by: cmp)


--->  還可以用以下的簡單寫法
nums.sorted(by: {
    (i1: Int, i2: Int) -> Bool in
    return i1 < i2
})

nums.sorted(by: {
    i1, i2 in return i1 < i2
})

nums.sorted(by: {
    i1, i2 in i1 < i2
})

nums.sorted(by: {
    $0 < $1
})

nums.sorted(by: <)

nums.sorted() { $0 < $1 }

nums.sorted { $0 < $1 }

忽略參數(shù)

func exec(fn: (Int, Int) -> Int) {
    print(fn(1,2))
}
//下面這種調(diào)用方法绞灼,表示忽略參數(shù)利术,不管傳什么參數(shù)進來都不管
exec { _,_ in 10}
image

除非你給出類型


image

閉包(Closure)

1.關(guān)于閉包的嚴(yán)謹(jǐn)定義

  • 一個函數(shù)它所捕獲的變量/常量環(huán)境組合起來,稱為閉包
    1. 一般指定義在函數(shù)內(nèi)部的函數(shù)
    2. 一般它捕獲的是外層函數(shù)的局部變量/常量
typealias Fn = (Int) -> Int
func getFn() -> Fn {
    var num = 0 // 這里num是一個局部變量
    // 函數(shù)plus是函數(shù)getFn內(nèi)部的一個函數(shù)
    func plus(_ i: Int) -> Int {
        //這里plus捕獲(使用)了它外層的函數(shù)的局部變量 num
        num += i
        return num
    }
    
    return plus
}// 因此這個被返回的plus 和 被其捕獲的num 就形成了一個閉包

2.閉包本質(zhì)的推斷

上面的代碼演示了閉包產(chǎn)生的必要條件低矮。那么閉包的本質(zhì)是什么呢印叁?為了探討這個問題,首先將上面的代碼擴充如下

typealias Fn = (Int) -> Int
func getFn() -> Fn {
    var num = 0
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }
    return plus
}
var fn = getFn()
print(fn(1))
print(fn(2))
print(fn(3))
print(fn(4))

首先說明军掂,上述的代碼編譯正確轮蜕,不會報錯。那么我們按照以前函數(shù)相關(guān)的知識先來分析一下這段代碼蝗锥。

  • 由于num是函數(shù)getFn()內(nèi)部的局部變量跃洛,在var fn = getFn()執(zhí)行完成,就應(yīng)該被系統(tǒng)回收掉了玛追。
  • print(fn(x))的成功編譯税课,說明fn(x)這個用法是沒有語法錯誤的官还。由于var fn = getFn()說明fn這個變量接受了getFn()的返回結(jié)果琐鲁。
  • getFn()內(nèi)部最后返回的是函數(shù)plus晃洒,我們暫且簡單認(rèn)為fn就是函數(shù)plus,那么fn(x)執(zhí)行肯定會調(diào)用plus()函數(shù)陆馁,也就會用到num變量。
  • 由于剛才分析我們知道合愈,getFn()里的那個num肯定是在該函數(shù)調(diào)用結(jié)束時被回收掉叮贩,而plus()執(zhí)行的時候击狮,肯定是在getFn()函數(shù)結(jié)束之后,那么它內(nèi)部用到的這個num變量從哪里來呢益老?

一個程序內(nèi)存分布就這么幾個地方:代碼段彪蓬、數(shù)據(jù)段、堆區(qū)捺萌、棧區(qū)档冬。其中代碼段、數(shù)據(jù)段時編譯之后就固定好的桃纯,棧區(qū)在num被聲明的getFn()函數(shù)結(jié)束后就被回收了酷誓,我們知道只有堆區(qū)的空間時在程序運行階段可以用來動態(tài)分配使用的,可以猜測态坦,應(yīng)該是在某個時刻盐数,程序把棧上的那個num變量內(nèi)容轉(zhuǎn)移到了堆上面。這樣在var fn = getFn()執(zhí)行完之后伞梯,我們還可通過print(fn(x))來繼續(xù)使用num就可以得到解釋了玫氢。并且還可以進一步猜測一下,print(fn(1))~print(fn(4))所使用的num應(yīng)該是堆上的同一個num谜诫,因為getFn()函數(shù)只執(zhí)行了一次琐旁。那么按照程序,打印的結(jié)果應(yīng)該是1猜绣、3灰殴、6、10掰邢,運行程序結(jié)果也確實如此牺陶。
另外,這里引申出了另外一個問題辣之,

假設(shè)fn只是簡單接收了getFn()所返回的plus函數(shù)掰伸,那么plus函數(shù)是如何找到堆上的這個num變量呢?

我們有理由懷疑getFn()應(yīng)該不僅僅只是簡單返回了一個plus函數(shù)這么簡單怀估,那么它返回的到底是個什么東東呢狮鸭?至少我們能猜得到,這個返回值多搀,能讓我們關(guān)聯(lián)到plus函數(shù)歧蕉,并且能夠找得到對上的那個num變量。

然而康铭,光靠猜肯定是不靠譜的惯退,檢驗語法糖的唯一標(biāo)準(zhǔn)是匯編,老規(guī)矩匯編代碼走一波从藤。

3.通過匯編來窺探閉包的本質(zhì)

- 不存在變量捕獲行為的簡單情景

func getFn() -> Fn {
    var num = 10
    func plus(_ i: Int) -> Int {
        return i
    }
    return plus //斷點處
}
var fn = getFn()

fn(1)
fn(2)
fn(3)
fn(4)

上面num制作為getFn的局部變量出現(xiàn)催跪,并沒有被plus函數(shù)所使用锁蠕,在return plus處斷點,運行并輸出匯編代碼如下

SwiftTest`getFn():
    0x100001390 <+0>:  pushq  %rbp
    0x100001391 <+1>:  movq   %rsp, %rbp
    0x100001394 <+4>:  movq   $0x0, -0x8(%rbp)
    0x10000139c <+12>: movq   $0xa, -0x8(%rbp)
->  0x1000013a4 <+20>: leaq   0x15(%rip), %rax          ; plus #1 (Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at main.swift:388
    0x1000013ab <+27>: xorl   %ecx, %ecx
    0x1000013ad <+29>: movl   %ecx, %edx
    0x1000013af <+31>: popq   %rbp
    0x1000013b0 <+32>: retq

首先懊蒸,第四條指令0x10000139c <+12>: movq $0xa, -0x8(%rbp)很明顯是在getFn函數(shù)的椚偾悖空間劃定一段8個字節(jié)作為局部變量num,并且賦值10骑丸,也就是立即數(shù)$0x0a舌仍,所以對應(yīng)的是這句代碼var num = 10

根據(jù)匯編常識,函數(shù)的返回值一般存放在寄存器rax里面者娱,當(dāng)前這條匯編指令leaq 0x15(%rip), %rax執(zhí)行完之后抡笼,rax就存上了返回值,因為是leaq指令黄鳍,說明往rax里面存入的是一個內(nèi)存地址推姻,很明顯,這應(yīng)該就是getFn所返回的plus函數(shù)地址框沟。我們可以在LLDB里面打印一下rax

(lldb) si
(lldb) register read rax
     rax = 0x00000001000013c0  SwiftTest`plus #1 (Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at main.swift:388
(lldb) 

看注釋藏古,提到了plus函數(shù),到底是不是呢忍燥,此時我們可以在plus函數(shù)內(nèi)部加一個斷點

image

這樣繼續(xù)運行程序拧晕,就會來到plus函數(shù)的內(nèi)部斷點,匯編內(nèi)容如下

image

很明顯梅垄,plus函數(shù)的地址只就是0x1000013c0厂捞,也就寄存器rax里的那個值0x00000001000013c0。我們打印一下此時的fn

p fn
() -> () $R0 = 0x00000001000013c0 SwiftTest`plus #1 (Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at main.swift:388
(lldb) 

結(jié)果很明顯队丝,說明當(dāng)前的簡單場景下(plus內(nèi)部沒有使用外部函數(shù)的局部變量)靡馁,getFn()只是簡單返回了plus函數(shù)的內(nèi)存地址,并且被fn接收

-發(fā)生了變量捕獲行為的情景

接下來机久,針對 捕獲外層函數(shù)的局部變量情況 代碼調(diào)整如下

typealias Fn = (Int) -> Int

func getFn() -> Fn {
    var num = 10
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }
    return plus //斷點處
}

var fn = getFn()

fn(1)
fn(2)
fn(3)
fn(4)

我們現(xiàn)在在函數(shù)plus內(nèi)部使用了num臭墨,在 return plus 加斷點看下此時getFn()函數(shù)的匯編

SwiftTest`getFn():
    0x100001210 <+0>:  pushq  %rbp
    0x100001211 <+1>:  movq   %rsp, %rbp
    0x100001214 <+4>:  subq   $0x20, %rsp
    0x100001218 <+8>:  leaq   0x4f19(%rip), %rdi
    0x10000121f <+15>: movl   $0x18, %esi
    0x100001224 <+20>: movl   $0x7, %edx
    0x100001229 <+25>: callq  0x100005474               ; symbol stub for: swift_allocObject
    0x10000122e <+30>: movq   %rax, %rdx
    0x100001231 <+33>: addq   $0x10, %rdx
    0x100001235 <+37>: movq   %rdx, %rsi
    0x100001238 <+40>: movq   $0xa, 0x10(%rax)
->  0x100001240 <+48>: movq   %rax, %rdi
    0x100001243 <+51>: movq   %rax, -0x8(%rbp)
    0x100001247 <+55>: movq   %rdx, -0x10(%rbp)
    0x10000124b <+59>: callq  0x1000054ec               ; symbol stub for: swift_retain
    0x100001250 <+64>: movq   -0x8(%rbp), %rdi
    0x100001254 <+68>: movq   %rax, -0x18(%rbp)
    0x100001258 <+72>: callq  0x1000054e6               ; symbol stub for: swift_release
    0x10000125d <+77>: movq   -0x10(%rbp), %rax
    0x100001261 <+81>: leaq   0x138(%rip), %rax         ; partial apply forwarder for plus #1 (Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at <compiler-generated>
    0x100001268 <+88>: movq   -0x8(%rbp), %rdx
    0x10000126c <+92>: addq   $0x20, %rsp
    0x100001270 <+96>: popq   %rbp
    0x100001271 <+97>: retq   

可以看到,有一個最關(guān)鍵的變化就是膘盖,getFn()函數(shù)內(nèi)增加了一段swift_allocObject函數(shù)的調(diào)用胧弛,看到alloc相關(guān)的函數(shù)通常就說明進行了堆空間的動態(tài)分配。這里侠畔,應(yīng)該就是我們之前猜測的用來轉(zhuǎn)移num內(nèi)容的那塊堆空間结缚。看到這里践图,我們暫時用一個猜測回答了上一個猜測的問題掺冠,下面再深入的進行分析。

image

此時我們在LLDB輸出一下寄存器rax(也就是swift_allocObject函數(shù)的返回值)

(lldb) register read rax
     rax = 0x0000000100532130

這個地址就是swift_allocObject函數(shù)所動態(tài)分配的那一段堆空間的起始地址码党。按照當(dāng)前的理解德崭,被plus函數(shù)捕獲的num變量的值就存在這個段空間上,具體怎么存的還不清楚揖盘,但是fn(1)~fn(4)的調(diào)用結(jié)果我們看出眉厨,他們用的都是這個堆空間上的同一個num變量,因此我們可以在plus函數(shù)每次調(diào)用的時候兽狭,追蹤一下num的值憾股。首先因為代碼中getFn函數(shù)的那個局部變量num值是10,因此被轉(zhuǎn)移到當(dāng)前堆空間上的值也應(yīng)該是10箕慧。我們可以在LLDB里面看一下這段堆空間的內(nèi)容服球,由于不確定這個堆空間的實際長度,所以我先看看前40個字節(jié)的內(nèi)容

在這里插入圖片描述

這里我們發(fā)現(xiàn)這段空間的17-24 這8個字節(jié)上面正好存了一個10(也就是0x0a)颠焦,如果它就是那個傳說中的num斩熊,那么fn(1)~fn(4)每次執(zhí)行完之后,新的num值都放到這段空間上來伐庭。我們跑一遍看看
fn(1)執(zhí)行完之后

(lldb) x/5xg 0x0000000100532130
0x100532130: 0x0000000100006138 0x0000000000000002
0x100532140: 0x000000000000000b 0x0000000000000004
0x100532150: 0x0000000000000000

17-24字節(jié)上的內(nèi)容:0x0b -> 11

fn(2)執(zhí)行完之后

(lldb) x/5xg 0x0000000100532130
0x100532130: 0x0000000100006138 0x0000000000000002
0x100532140: 0x000000000000000d 0x0000000000000004
0x100532150: 0x0000000000000000

17-24字節(jié)上的內(nèi)容:0x0d -> 13

fn(3)執(zhí)行完之后

(lldb) x/5xg 0x0000000100532130
0x100532130: 0x0000000100006138 0x0000000000000002
0x100532140: 0x0000000000000010 0x0000000000000004
0x100532150: 0x0000000000000000

17-24字節(jié)上的內(nèi)容:0x10 -> 16

fn(4)執(zhí)行完之后

(lldb) x/5xg 0x0000000100532130
0x100532130: 0x0000000100006138 0x0000000000000002
0x100532140: 0x0000000000000014 0x0000000000000004
0x100532150: 0x0000000000000000

17-24字節(jié)上的內(nèi)容:0x14 -> 20

這樣粉渠,就證明了這8個字節(jié)確實是我們猜測的那段用來存儲num變量的堆空間。

-那么每次調(diào)用getFn都確實會重新申請一段堆空間給num用嗎圾另?

代碼再調(diào)整一下

typealias Fn = (Int) -> Int

func getFn() -> Fn {
    var num = 10
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }
    return plus  //加斷點1
}

var fn = getFn()
fn(1)
fn(2)//加斷點2
print("Debug stop")//加斷點3

var fn1 = getFn()
fn1(1)
fn1(2)//加斷點4
print("Debug stop")//加斷點5

首先霸株,執(zhí)行var fn = getFn()getFn被第一次調(diào)用

SwiftTest`getFn():
    0x1000011b0 <+0>:  pushq  %rbp
    0x1000011b1 <+1>:  movq   %rsp, %rbp
    0x1000011b4 <+4>:  subq   $0x20, %rsp
    0x1000011b8 <+8>:  leaq   0x4f79(%rip), %rdi
    0x1000011bf <+15>: movl   $0x18, %esi
    0x1000011c4 <+20>: movl   $0x7, %edx
    0x1000011c9 <+25>: callq  0x10000545a               ; symbol stub for: swift_allocObject
    0x1000011ce <+30>: movq   %rax, %rdx
    0x1000011d1 <+33>: addq   $0x10, %rdx
    0x1000011d5 <+37>: movq   %rdx, %rsi
    0x1000011d8 <+40>: movq   $0xa, 0x10(%rax)
->  0x1000011e0 <+48>: movq   %rax, %rdi
    0x1000011e3 <+51>: movq   %rax, -0x8(%rbp)
    0x1000011e7 <+55>: movq   %rdx, -0x10(%rbp)
    .............

按照上面用過的方法,看一下這次swift_allocObject所分配的空間地址集乔,以及內(nèi)部的num變量的值

(lldb) register read rax
     rax = 0x0000000100707c60
  
//此時堆上的num對應(yīng)的地址為0x100707c70去件,并且num=10   
(lldb) x/4xg 0x0000000100707c60
0x100707c60: 0x0000000100006138 0x0000000000000002
0x100707c70: 0x000000000000000a 0x0000000000000000

然后繼續(xù)運行至斷點2斷點3,查看此時的對內(nèi)存

//停留在斷點2扰路,fn(1)執(zhí)行完成尤溜,num=11
(lldb) x/4xg 0x0000000100707c60
0x100707c60: 0x0000000100006138 0x0000000000000002
0x100707c70: 0x000000000000000b 0x0000000000000000

//停留在斷點2,fn(2)執(zhí)行完成幼衰,num=14
(lldb) x/4xg 0x0000000100707c60
0x100707c60: 0x0000000100006138 0x0000000000000002
0x100707c70: 0x000000000000000d 0x0000000000000000

繼續(xù)運行程序靴跛,執(zhí)行var fn1 = getFn()會再次調(diào)用getFn,并走到斷點1

SwiftTest`getFn():
    0x1000011b0 <+0>:  pushq  %rbp
    0x1000011b1 <+1>:  movq   %rsp, %rbp
    0x1000011b4 <+4>:  subq   $0x20, %rsp
    0x1000011b8 <+8>:  leaq   0x4f79(%rip), %rdi
    0x1000011bf <+15>: movl   $0x18, %esi
    0x1000011c4 <+20>: movl   $0x7, %edx
    0x1000011c9 <+25>: callq  0x10000545a               ; symbol stub for: swift_allocObject
    0x1000011ce <+30>: movq   %rax, %rdx
    0x1000011d1 <+33>: addq   $0x10, %rdx
    0x1000011d5 <+37>: movq   %rdx, %rsi
    0x1000011d8 <+40>: movq   $0xa, 0x10(%rax)
->  0x1000011e0 <+48>: movq   %rax, %rdi
    0x1000011e3 <+51>: movq   %rax, -0x8(%rbp)
    0x1000011e7 <+55>: movq   %rdx, -0x10(%rbp)
    ..............

同樣渡嚣,查看新分配的堆空間地址

(lldb) register read rax
     rax = 0x00000001005108e0

//此時堆上的num對應(yīng)的地址為0x1005108f0梢睛,并且num=10
(lldb) x/4xg 0x00000001005108e0
0x1005108e0: 0x0000000100006138 0x0000000000000002
0x1005108f0: 0x000000000000000a 0x0002000000000000

然后繼續(xù)運行至斷點4斷點5,查看此時的對內(nèi)存

//停留在斷點2识椰,fn1(1)執(zhí)行完成绝葡,num=11
(lldb) x/4xg 0x00000001005108e0
0x1005108e0: 0x0000000100006138 0x0000000000000002
0x1005108f0: 0x000000000000000b 0x0002000000000000

//停留在斷點2,fn1(1)執(zhí)行完成腹鹉,num=14
(lldb) x/4xg 0x00000001005108e0
0x1005108e0: 0x0000000100006138 0x0000000000000002
0x1005108f0: 0x000000000000000d 0x0002000000000000

這樣我們就看出了規(guī)律藏畅,每調(diào)用一次getFn函數(shù),就會分配一段新的堆空間來處理num的值,相互之間互不交叉愉阎,獨立了開來绞蹦。

-每次對變量進行捕獲時到底每次分配了多少堆空間?

因為堆空間的分配榜旦,是在函數(shù)swift_allocObject內(nèi)完成的幽七,所以到底分配了多少,應(yīng)該跟該函數(shù)所傳入的參數(shù)有關(guān)聯(lián)溅呢。在匯編里面澡屡,函數(shù)的參數(shù)一般是在調(diào)用該函數(shù)指令之前,存放在rdi咐旧、rsi驶鹉、rdx、rcx铣墨、r8室埋、r9等幾個寄存器里面(如果還有更多的參數(shù),編譯器會使用棧上的空間來協(xié)助存儲)踏兜,那么我們來看一下swift_allocObject的參數(shù)

image

可以看到這里有個入?yún)⑹?code>0x18 (也就是24)词顾,而我們上面的分析結(jié)果告訴我們,num就存放在swift_allocObject函數(shù)所分配堆空間的17-24個字節(jié)上面碱妆,而前面16個字節(jié)里面肉盹,前8個字節(jié)存放類型信息,后8個字節(jié)存放引用計數(shù)疹尾,堆空間里面的東西其實本質(zhì)上就是實例對象上忍,那么就必然會有類型信息和引用計數(shù),相信不難理解纳本。
image

因此這個參數(shù)0x18應(yīng)該就是函數(shù)swift_allocObject用來告訴系統(tǒng)所需要申請的堆空間的大小窍蓝。因為在iOS/OS X系統(tǒng)里面,分配堆空間至少是16的倍數(shù)繁成,所以實際上swift_allocObject結(jié)束后得到的堆空間應(yīng)該是32字節(jié)吓笙,只不過實際上只用到了其中的24個字節(jié)。實際上追蹤swift_allocObject的調(diào)用堆棧結(jié)果如下

    frame #0: 0x00007fff6975ace0 libsystem_malloc.dylib`malloc
    frame #1: 0x00007fff68ec0ca9 libswiftCore.dylib`swift_slowAlloc + 25
    frame #2: 0x00007fff68ec0d27 libswiftCore.dylib`swift_allocObject + 39
    frame #3: 0x00000001000011ce SwiftTest`getFn() at <compiler-generated>:0
    frame #4: 0x0000000100000e09 SwiftTest`main at main.swift:392:10
    frame #5: 0x00007fff695a4cc9 libdyld.dylib`start + 1
    frame #6: 0x00007fff695a4cc9 libdyld.dylib`start + 1

最終實際上是通過libsystem_malloc.dylib庫下的malloc函數(shù)來完成動態(tài)內(nèi)存分配的巾腕,至于為什么分配的空間是16的倍數(shù)請看這里

4.閉包的內(nèi)存結(jié)構(gòu)

ok面睛,到此為止,基于演示代碼尊搬,我們弄清楚了這么幾個問題:

  • plus函數(shù)內(nèi)部所捕獲的那個外層函數(shù)局部變量num叁鉴,實際上是把它的值存儲到了堆上動態(tài)申請的一段內(nèi)存空間上。
  • 動態(tài)申請的內(nèi)存大小是24字節(jié)佛寿,實際獲得的內(nèi)存大小是32字節(jié)幌墓,前16個字節(jié)存放類型描述信息和引用計數(shù)信息,接下來的8個字節(jié)用來存放num的值,剩下的暫時空閑常侣。
  • getFn函數(shù)每調(diào)用一次蜡饵,就會動態(tài)申請一段新的堆內(nèi)存來存放全新的num值。

那么var fn = getFn()中的這個fn到底是什么袭祟?因為fn(x)的運行結(jié)果說明验残,plus函數(shù)被調(diào)用捞附,并且能夠使用堆內(nèi)存上的num變量巾乳。下面我們就探索一下,它們之間是如何關(guān)聯(lián)的鸟召。

- 沒有變量捕獲時的fn結(jié)構(gòu)

func getFn() -> (Int, Int) -> Int {
    func sum(_ v1: Int, _ v2: Int) -> Int {
        v1 + v2 //加斷點2
    }
    return sum
}
var fn = getFn()
fn(1, 2) //加斷點1

如上的案例里面胆绊,我們閹割掉對外層函數(shù)局部變量捕獲,只簡單返回函數(shù)sum欧募。運行程序來到斷點1出压状,現(xiàn)輸出一下此時變量fn獲得的值

(lldb) p fn 
() -> () $R0 = 0x00000001000011e0 SwiftTest`sum #1 (Swift.Int, Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int, Swift.Int) -> Swift.Int at main.swift:412

可以看出就是函數(shù)sum的地址,我們還可以在斷點2出查看sum匯編代碼來進一步確認(rèn)跟继,

SwiftTest`sum #1 (_:_:) in getFn():
    0x1000011e0 <+0>:   pushq  %rbp
    0x1000011e1 <+1>:   movq   %rsp, %rbp
    0x1000011e4 <+4>:   subq   $0x40, %rsp
    0x1000011e8 <+8>:   xorl   %eax, %eax
    0x1000011ea <+10>:  leaq   -0x8(%rbp), %rcx
    0x1000011ee <+14>:  movq   %rdi, -0x18(%rbp)
    ..........

看得出种冬,sum函數(shù)的地址的確就是0x1000011e0,而fn變量所占內(nèi)存的大小可以借助MemoryLayout
確定

(lldb) p MemoryLayout.size(ofValue: fn)
(Int) $R4 = 16
(lldb) p MemoryLayout.stride(ofValue: fn)
(Int) $R10 = 16

可以得到變量fn被分配了16字節(jié)內(nèi)存舔糖,并使用了其中16字節(jié)娱两。

image

根據(jù)斷點位置,我們可以判斷var fn = getFn()已經(jīng)執(zhí)行完成金吗,fn所處的位置說明它是一個全局變量十兢,這樣我們可以很容易定位這句代碼所對應(yīng)的匯編
image

圖中綠框的兩個地址對應(yīng)的是連續(xù)的兩端內(nèi)存空間,一共16字節(jié)摇庙,實際上根據(jù)Swift注釋也可以看出旱物,這段空間就是fn變量的內(nèi)存空間。因為movq一次只能操作8個字節(jié)卫袒,所以需要對這段16字節(jié)內(nèi)存空間連續(xù)兩次操作才能完成賦值宵呛。而且寄存器rax此時存的只就是sum函數(shù)的地址,下面的rdx暫時為0

(lldb) register read rax
     rax = 0x00000001000011e0  SwiftTest`sum #1 (Swift.Int, Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int, Swift.Int) -> Swift.Int at main.swift:412
(lldb) register read rdx
     rdx = 0x0000000000000000

所以此時fn的空間里面夕凝,存儲的就是函數(shù)sum的地址宝穗,而且暫時空閑了8個字節(jié)。這就是fn的全部迹冤。

image

匯編閱讀小技巧:

  • 0xXXXX(%rip) 尋址的結(jié)果通常是全局變量(數(shù)據(jù)段)的內(nèi)存地址讽营。
  • 0xXX(%rbp) 尋址的結(jié)果通常是函數(shù)局部變量(棧空間)的內(nèi)存地址泡徙。
  • 0xXX(%rax) 尋址的結(jié)果通常是堆空間(通常通過alloc系列函數(shù)動態(tài)申請)的內(nèi)存地址橱鹏。
  • 函數(shù)的返回值一般放在rax、rdx寄存器里面


- 進行變量捕獲情況下的fn結(jié)構(gòu)

typealias Fn = (Int) -> Int

func getFn() -> Fn {
    var num = 10
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }
    return plus //?斷點2?
}

var fn = getFn()//?斷點1?
fn(1)  //?斷點3?
print("Debug stop")

上述代碼,我們在plus函數(shù)內(nèi)部使用(捕獲)了外部的num變量莉兰。運行代碼挑围,來到斷點1處的匯編代碼

SwiftTest`main:
    0x100001160 <+0>:   pushq  %rbp
    0x100001161 <+1>:   movq   %rsp, %rbp
    0x100001164 <+4>:   pushq  %r13
    0x100001166 <+6>:   subq   $0x48, %rsp
    0x10000116a <+10>:  movl   %edi, -0x24(%rbp)
    0x10000116d <+13>:  movq   %rsi, -0x30(%rbp)
->  0x100001171 <+17>:  callq  0x100001200               ; SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at main.swift:388
    0x100001176 <+22>:  leaq   0x6093(%rip), %rsi        ; SwiftTest.fn : (Swift.Int) -> Swift.Int
    0x10000117d <+29>:  xorl   %edi, %edi
    0x10000117f <+31>:  movl   %edi, %ecx
    0x100001181 <+33>:  movq   %rax, 0x6088(%rip)        ; SwiftTest.fn : (Swift.Int) -> Swift.Int
    0x100001188 <+40>:  movq   %rdx, 0x6089(%rip)        ; SwiftTest.fn : (Swift.Int) -> Swift.Int + 8
    0x10000118f <+47>:  movq   %rsi, %rdi
    0x100001192 <+50>:  leaq   -0x20(%rbp), %rsi
    0x100001196 <+54>:  movl   $0x20, %edx
    0x10000119b <+59>:  callq  0x100005470               ; symbol stub for: swift_beginAccess
    0x1000011a0 <+64>:  movq   0x6069(%rip), %rax        ; SwiftTest.fn : (Swift.Int) -> Swift.Int
    0x1000011a7 <+71>:  movq   0x606a(%rip), %rcx        ; SwiftTest.fn : (Swift.Int) -> Swift.Int + 8
    0x1000011ae <+78>:  movq   %rcx, %rdi
    0x1000011b1 <+81>:  movq   %rax, -0x38(%rbp)
    0x1000011b5 <+85>:  movq   %rcx, -0x40(%rbp)
    0x1000011b9 <+89>:  callq  0x1000054dc               ; symbol stub for: swift_retain
    0x1000011be <+94>:  leaq   -0x20(%rbp), %rdi
    0x1000011c2 <+98>:  movq   %rax, -0x48(%rbp)
    0x1000011c6 <+102>: callq  0x100005494               ; symbol stub for: swift_endAccess
    0x1000011cb <+107>: movl   $0x1, %edi
    0x1000011d0 <+112>: movq   -0x40(%rbp), %r13
    0x1000011d4 <+116>: movq   -0x38(%rbp), %rax
    0x1000011d8 <+120>: callq  *%rax
    0x1000011da <+122>: movq   -0x40(%rbp), %rdi
    0x1000011de <+126>: movq   %rax, -0x50(%rbp)
    0x1000011e2 <+130>: callq  0x1000054d6               ; symbol stub for: swift_release
    0x1000011e7 <+135>: xorl   %eax, %eax
    0x1000011e9 <+137>: addq   $0x48, %rsp
    0x1000011ed <+141>: popq   %r13
    0x1000011ef <+143>: popq   %rbp
    0x1000011f0 <+144>: retq   

根據(jù)之前的分析,我們已經(jīng)知道var fn這個全局變量占用16個字節(jié)糖荒,賦值需要兩次movq指令進行操作杉辙,也就是這里

image

說明getFn()被調(diào)用之后,將返回值捶朵,放在了raxrdx這兩個寄存器里面蜘矢,然后通過上圖里面的這兩句匯編指令,完成了var fn = getFn()這一句里面的賦值操作综看。那么raxrdx里面到底放了什么內(nèi)容品腹,就需要進入到getFn函數(shù)取觀察。我們繼續(xù)運行代碼红碑,即可來到斷點2舞吭,匯編如下

SwiftTest`getFn():
    0x100001200 <+0>:  pushq  %rbp
    0x100001201 <+1>:  movq   %rsp, %rbp
    0x100001204 <+4>:  subq   $0x20, %rsp
    0x100001208 <+8>:  leaq   0x4f29(%rip), %rdi
    0x10000120f <+15>: movl   $0x18, %esi
    0x100001214 <+20>: movl   $0x7, %edx
    0x100001219 <+25>: callq  0x100005464               ; symbol stub for: swift_allocObject
    0x10000121e <+30>: movq   %rax, %rdx
    0x100001221 <+33>: addq   $0x10, %rdx
    0x100001225 <+37>: movq   %rdx, %rsi
    0x100001228 <+40>: movq   $0xa, 0x10(%rax)
->  0x100001230 <+48>: movq   %rax, %rdi
    0x100001233 <+51>: movq   %rax, -0x8(%rbp)
    0x100001237 <+55>: movq   %rdx, -0x10(%rbp)
    0x10000123b <+59>: callq  0x1000054dc               ; symbol stub for: swift_retain
    0x100001240 <+64>: movq   -0x8(%rbp), %rdi
    0x100001244 <+68>: movq   %rax, -0x18(%rbp)
    0x100001248 <+72>: callq  0x1000054d6               ; symbol stub for: swift_release
    0x10000124d <+77>: movq   -0x10(%rbp), %rax
    0x100001251 <+81>: leaq   0x138(%rip), %rax         ; partial apply forwarder for plus #1 (Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at <compiler-generated>
    0x100001258 <+88>: movq   -0x8(%rbp), %rdx
    0x10000125c <+92>: addq   $0x20, %rsp
    0x100001260 <+96>: popq   %rbp
    0x100001261 <+97>: retq 

我們從下往上逆向追溯,可以看到raxrdx的賦值軌跡如下圖

在這里插入圖片描述

從圖中的軌跡路線析珊,可以判斷:

  • 當(dāng)getFn返回的時候羡鸥,rax里面存放了一個叫partial apply forwarder for plus的函數(shù)的地址,不知道它是不是plus的地址忠寻,但至少是跟plus有關(guān)的一個函數(shù)惧浴。
  • rdx里面存放的是swift_allocObject函數(shù)動態(tài)申請的堆空間地址,這段堆空間的作用之前已經(jīng)證明過了锡溯,里面的一段空間是用來存放從椄嫌撸空間捕獲過來的num變量的值的

為了驗證我們的判斷祭饭,我們可以在上圖第9句匯編碼處加一個斷點芜茵,將程序重新運行到該斷點,輸出一下swift_allocObject申請到的堆空間地址

(lldb) register read rax
     rax = 0x0000000103204310

然后運行到第23句匯編碼處倡蝙,查看此時rdx的值(這個是函數(shù)返回錢九串,rdx最后一次完成賦值)

(lldb) register read rdx
     rdx = 0x0000000103204310

沒錯,就是堆空間的地址寺鸥。同時看一下此時rax被賦予的partial apply forwarder for plus函數(shù)的地址值是多少

(lldb) register read rax
     rax = 0x0000000100001390  SwiftTest`partial apply forwarder for plus #1 (Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at <compiler-generated>

所以當(dāng)getFn返回之后猪钮,fn所收到的值到底是什么也就一清二楚了。

- plus函數(shù)在哪里胆建?它是如如何被調(diào)用到的烤低?

我們來分析一下fn(1)這句代碼,我們知道fn里面頭8個字節(jié)放了一個partial apply forwarder for plus函數(shù)地址笆载,后8個字節(jié)放的是捕獲變量的那段堆地址扑馁,那么這句代碼要做的必然(并且也只能)是去調(diào)用partial apply forwarder for plus這個函數(shù)涯呻,但是跟直接通過函數(shù)名調(diào)用一個函數(shù)不同(通過函數(shù)名方式,是直接對函數(shù)地址調(diào)用腻要,例如 call 0x10000476b)复罐,但這里的fn是一個全局變量,是一個變量哦雄家,可以把它理解成一個盒子效诅,里面可以裝不同的東西,你要使用里面的內(nèi)容趟济,就必須打開盒子乱投,所以這是一種間接調(diào)用,在匯編里面咙好,間接函數(shù)調(diào)用是用 callq *[內(nèi)存地址]這種格式來表示篡腌,例如callq *rax,表示根據(jù)寄存器rax里面存儲的指針勾效,找到指定內(nèi)存,從里面讀取8個字節(jié)的內(nèi)容叛甫,作為目標(biāo)函數(shù)地址层宫,然后進行調(diào)用。

我們繼續(xù)運行程序其监,來帶斷點3萌腿,根據(jù)間接調(diào)用的語法特征,我們可以看到partial apply forwarder for plus函數(shù)的調(diào)用在如下位置

image

我們運行程序至此抖苦,并且進入該函數(shù)
image

partial apply forwarder for plus函數(shù)的匯編代碼可以清晰地看出毁菱,最后那句就是跳轉(zhuǎn)到真正的plus函數(shù),也就是說锌历,plus函數(shù)的地址實際上是被包裹在了partial apply forwarder for plus函數(shù)函數(shù)內(nèi)部贮庞,并且直接進行跳轉(zhuǎn)的。
至此究西,我們就弄清楚了窗慎,最簡單的閉包產(chǎn)生的條件,以及閉包的內(nèi)存結(jié)構(gòu)卤材,通過下圖總結(jié)一下
image

以上遮斥,我們弄清楚了一個閉包產(chǎn)生的條件,以及閉包內(nèi)部的具體內(nèi)容扇丛,那么不同閉包之間术吗,內(nèi)部的內(nèi)容是否有重疊或者是共用的部分呢?我們把代碼調(diào)整如下

typealias Fn = (Int) -> Int

func getFn() -> Fn {
    var num = 10
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }
    return plus
}

var fn = getFn()
fn(1)  //斷點1

var fn2 = getFn()
fn2(1) //斷點2

運行代碼帆精,先來到斷點1處较屿,匯編如下

image

我們可以計算得到fn內(nèi)存地址等于 0x100000eeb + 0x6325 = 0x100007210材蹬,我們便可以在控制臺輸入打印出fn內(nèi)存的內(nèi)容

(lldb) x/2xg 0x100007210
0x100007210: 0x0000000100001350 0x0000000101170a30

繼續(xù)運行程序到斷點2,并通過同樣的方法計算得到fn2的內(nèi)存地址為0x100007220吝镣,并且打印出它所存儲的內(nèi)容

(lldb) x/2xg 0x100007220
0x100007220: 0x0000000100001350 0x00000001006598f0

通過對比堤器,我們看得出,fnfn2的前八個字節(jié)存放的內(nèi)容相同末贾,也就是partial apply forwarder for plus這個函數(shù)的地址货裹,而他們后8個字節(jié)的內(nèi)容不一樣露泊,說明他們各自動態(tài)申請了自己的堆空間,用來存放所捕獲的外部變量,其實你應(yīng)該能感覺到祭椰,這跟類的實例對象很相似,共享方法垫蛆,各自管理自己的成員變量违孝。也就是下圖所示

image

- num是怎么被找到并且使用的?

再次回顧一下我們的案例代碼

typealias Fn = (Int) -> Int

func getFn() -> Fn {
    var num = 10
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }    
    return plus
}
var fn = getFn()
fn(1)  //斷點1

運行程序至斷點1集索,匯編如下

 SwiftTest`main:
    0x1000019c0 <+0>:   pushq  %rbp
    0x1000019c1 <+1>:   movq   %rsp, %rbp
    0x1000019c4 <+4>:   pushq  %r13
    0x1000019c6 <+6>:   subq   $0x98, %rsp
    0x1000019cd <+13>:  movl   %edi, -0x3c(%rbp)
    0x1000019d0 <+16>:  movq   %rsi, -0x48(%rbp)
    0x1000019d4 <+20>:  callq  0x100001de0               ; SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at main.swift:393
    0x1000019d9 <+25>:  leaq   0x69a0(%rip), %rcx        ; SwiftTest.fn : (Swift.Int) -> Swift.Int
    0x1000019e0 <+32>:  xorl   %edi, %edi
    0x1000019e2 <+34>:  movl   %edi, %esi
    0x1000019e4 <+36>:  movq   %rax, 0x6995(%rip)        ; SwiftTest.fn : (Swift.Int) -> Swift.Int
    0x1000019eb <+43>:  movq   %rdx, 0x6996(%rip)        ; SwiftTest.fn : (Swift.Int) -> Swift.Int + 8
->  0x1000019f2 <+50>:  movq   %rcx, %rdi
    0x1000019f5 <+53>:  leaq   -0x20(%rbp), %rax
    0x1000019f9 <+57>:  movq   %rsi, -0x50(%rbp)
    0x1000019fd <+61>:  movq   %rax, %rsi
    0x100001a00 <+64>:  movl   $0x20, %edx
    0x100001a05 <+69>:  movq   -0x50(%rbp), %rcx
    0x100001a09 <+73>:  callq  0x100006322               ; symbol stub for: swift_beginAccess
    0x100001a0e <+78>:  movq   0x696b(%rip), %rax        ; SwiftTest.fn : (Swift.Int) -> Swift.Int
    0x100001a15 <+85>:  movq   0x696c(%rip), %rcx        ; SwiftTest.fn : (Swift.Int) -> Swift.Int + 8
    0x100001a1c <+92>:  movq   %rcx, %rdi
    0x100001a1f <+95>:  movq   %rax, -0x58(%rbp)
    0x100001a23 <+99>:  movq   %rcx, -0x60(%rbp)
    0x100001a27 <+103>: callq  0x10000638e               ; symbol stub for: swift_retain
    0x100001a2c <+108>: leaq   -0x20(%rbp), %rdi
    0x100001a30 <+112>: movq   %rax, -0x68(%rbp)
    0x100001a34 <+116>: callq  0x10000634c               ; symbol stub for: swift_endAccess
    0x100001a39 <+121>: movl   $0x1, %edi
    0x100001a3e <+126>: movq   -0x60(%rbp), %r13
    0x100001a42 <+130>: movq   -0x58(%rbp), %rax
    0x100001a46 <+134>: callq  *%rax
    0x100001a48 <+136>: movq   -0x60(%rbp), %rdi
    0x100001a4c <+140>: movq   %rax, -0x70(%rbp)
    0x100001a50 <+144>: callq  0x100006388               ; symbol stub for: swift_release
    0x100001a55 <+149>: callq  0x100001de0               ; SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at main.swift:393
    0x100001a5a <+154>: leaq   0x692f(%rip), %rcx        ; SwiftTest.fn2 : (Swift.Int) -> Swift.Int
    0x100001a61 <+161>: xorl   %r8d, %r8d
    0x100001a64 <+164>: movl   %r8d, %esi
    0x100001a67 <+167>: movq   %rax, 0x6922(%rip)        ; SwiftTest.fn2 : (Swift.Int) -> Swift.Int
    0x100001a6e <+174>: movq   %rdx, 0x6923(%rip)        ; SwiftTest.fn2 : (Swift.Int) -> Swift.Int + 8
    0x100001a75 <+181>: movq   %rcx, %rdi
    0x100001a78 <+184>: leaq   -0x38(%rbp), %rax
    0x100001a7c <+188>: movq   %rsi, -0x78(%rbp)
    0x100001a80 <+192>: movq   %rax, %rsi
    0x100001a83 <+195>: movl   $0x20, %edx
    0x100001a88 <+200>: movq   -0x78(%rbp), %rcx
    0x100001a8c <+204>: callq  0x100006322               ; symbol stub for: swift_beginAccess
    0x100001a91 <+209>: movq   0x68f8(%rip), %rax        ; SwiftTest.fn2 : (Swift.Int) -> Swift.Int
    0x100001a98 <+216>: movq   0x68f9(%rip), %rcx        ; SwiftTest.fn2 : (Swift.Int) -> Swift.Int + 8
    0x100001a9f <+223>: movq   %rcx, %rdi
    0x100001aa2 <+226>: movq   %rax, -0x80(%rbp)
    0x100001aa6 <+230>: movq   %rcx, -0x88(%rbp)
    0x100001aad <+237>: callq  0x10000638e               ; symbol stub for: swift_retain
    0x100001ab2 <+242>: leaq   -0x38(%rbp), %rdi
    0x100001ab6 <+246>: movq   %rax, -0x90(%rbp)
    0x100001abd <+253>: callq  0x10000634c               ; symbol stub for: swift_endAccess
    0x100001ac2 <+258>: movl   $0x1, %edi
    0x100001ac7 <+263>: movq   -0x88(%rbp), %r13
    0x100001ace <+270>: movq   -0x80(%rbp), %rax
    0x100001ad2 <+274>: callq  *%rax
    0x100001ad4 <+276>: movq   -0x88(%rbp), %rdi
    0x100001adb <+283>: movq   %rax, -0x98(%rbp)
    0x100001ae2 <+290>: callq  0x100006388               ; symbol stub for: swift_release
    0x100001ae7 <+295>: xorl   %eax, %eax
    0x100001ae9 <+297>: addq   $0x98, %rsp
    0x100001af0 <+304>: popq   %r13
    0x100001af2 <+306>: popq   %rbp
    0x100001af3 <+307>: retq   

根據(jù)我們之前對fn(1)調(diào)用特點的分析屿愚,首先會通過0x100001ad2 <+274>: callq *%rax這句匯編調(diào)用partial apply forwarder for plus函數(shù),所以除了實參參數(shù)1务荆,plus到時會用到的num肯定會也是從這里傳進去的妆距,我們來看一下callq *%rax之前的參數(shù)情況

image

image

到目前為止

  • edi = 1
  • r13 = num地址 - 0x10 (根據(jù)上圖,可以看出num所在的位置函匕,是r13所存放的地址開始跳過16個字節(jié)之后的那8個字節(jié)娱据,這點藥牢記)

我們繼續(xù)往下走,進入partial apply forwarder for plus函數(shù)內(nèi)部

SwiftTest`partial apply for plus #1 (_:) in getFn():
->  0x100001f70 <+0>: pushq  %rbp
    0x100001f71 <+1>: movq   %rsp, %rbp
    0x100001f74 <+4>: movq   %r13, %rsi
    0x100001f77 <+7>: popq   %rbp
    0x100001f78 <+8>: jmp    0x100001e70               ; plus #1 (Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int) -> Swift.

這里很簡單盅惜,就是把寄存器r13的值賦值給寄存器rsi中剩,然后在跳入地址為0x100001e70的函數(shù),后面的注釋告訴我們抒寂,這個函數(shù)就是plus函數(shù)结啼,那么更新一下當(dāng)前即將被plus函數(shù)使用的參數(shù)的情況

  • edi = 1
  • rsi = num地址 - 0x10
    我們跟隨0x100001f78 <+8>: jmp 0x100001e70指令來到plus函數(shù),其完整匯編如下
SwiftTest`plus #1 (_:) in getFn():
->  0x100001e70 <+0>:   pushq  %rbp
    0x100001e71 <+1>:   movq   %rsp, %rbp
    0x100001e74 <+4>:   subq   $0x90, %rsp
    0x100001e7b <+11>:  xorl   %eax, %eax
    0x100001e7d <+13>:  movl   %eax, %ecx
    0x100001e7f <+15>:  xorl   %eax, %eax
    0x100001e81 <+17>:  leaq   -0x8(%rbp), %rdx
    0x100001e85 <+21>:  movq   %rdi, -0x48(%rbp)
    0x100001e89 <+25>:  movq   %rdx, %rdi
    0x100001e8c <+28>:  movq   %rsi, -0x50(%rbp)
    0x100001e90 <+32>:  movl   %eax, %esi
    0x100001e92 <+34>:  movl   $0x8, %edx
    0x100001e97 <+39>:  movq   %rdx, -0x58(%rbp)
    0x100001e9b <+43>:  movq   %rcx, -0x60(%rbp)
    0x100001e9f <+47>:  movl   %eax, -0x64(%rbp)
    0x100001ea2 <+50>:  callq  0x100006310               ; symbol stub for: memset
    0x100001ea7 <+55>:  leaq   -0x10(%rbp), %rcx
    0x100001eab <+59>:  movq   %rcx, %rdi
    0x100001eae <+62>:  movl   -0x64(%rbp), %esi
    0x100001eb1 <+65>:  movq   -0x58(%rbp), %rdx
    0x100001eb5 <+69>:  callq  0x100006310               ; symbol stub for: memset
    0x100001eba <+74>:  movq   -0x48(%rbp), %rcx
    0x100001ebe <+78>:  movq   %rcx, -0x8(%rbp)
    0x100001ec2 <+82>:  movq   -0x50(%rbp), %rdx
    0x100001ec6 <+86>:  addq   $0x10, %rdx
    0x100001ecd <+93>:  movq   %rdx, -0x10(%rbp)
    0x100001ed1 <+97>:  movq   %rdx, %rdi
    0x100001ed4 <+100>: leaq   -0x28(%rbp), %rsi
    0x100001ed8 <+104>: movl   $0x21, %r8d
    0x100001ede <+110>: movq   %rdx, -0x70(%rbp)
    0x100001ee2 <+114>: movq   %r8, %rdx
    0x100001ee5 <+117>: movq   -0x60(%rbp), %rcx
    0x100001ee9 <+121>: callq  0x100006322               ; symbol stub for: swift_beginAccess
    0x100001eee <+126>: movq   -0x48(%rbp), %rcx
    0x100001ef2 <+130>: movq   -0x50(%rbp), %rdx
    0x100001ef6 <+134>: addq   0x10(%rdx), %rcx
    0x100001efa <+138>: seto   %r9b
    0x100001efe <+142>: testb  $0x1, %r9b
    0x100001f02 <+146>: movq   %rcx, -0x78(%rbp)
    0x100001f06 <+150>: jne    0x100001f60               ; <+240> at main.swift:398:13
    0x100001f08 <+152>: movq   -0x70(%rbp), %rax
    0x100001f0c <+156>: movq   -0x78(%rbp), %rcx
    0x100001f10 <+160>: movq   %rcx, (%rax)
    0x100001f13 <+163>: leaq   -0x28(%rbp), %rdi
    0x100001f17 <+167>: callq  0x10000634c               ; symbol stub for: swift_endAccess
    0x100001f1c <+172>: xorl   %edx, %edx
    0x100001f1e <+174>: movl   %edx, %ecx
    0x100001f20 <+176>: leaq   -0x40(%rbp), %rax
    0x100001f24 <+180>: movl   $0x20, %edx
    0x100001f29 <+185>: movq   -0x70(%rbp), %rdi
    0x100001f2d <+189>: movq   %rax, %rsi
    0x100001f30 <+192>: movq   %rax, -0x80(%rbp)
    0x100001f34 <+196>: callq  0x100006322               ; symbol stub for: swift_beginAccess
    0x100001f39 <+201>: movq   -0x70(%rbp), %rax
    0x100001f3d <+205>: movq   (%rax), %rax
    0x100001f40 <+208>: movq   -0x80(%rbp), %rdi
    0x100001f44 <+212>: movq   %rax, -0x88(%rbp)
    0x100001f4b <+219>: callq  0x10000634c               ; symbol stub for: swift_endAccess
    0x100001f50 <+224>: movq   -0x88(%rbp), %rax
    0x100001f57 <+231>: addq   $0x90, %rsp
    0x100001f5e <+238>: popq   %rbp
    0x100001f5f <+239>: retq   
    0x100001f60 <+240>: ud2    

乍一看不太好分析蓬推,我們從關(guān)鍵點入手妆棒,我們知道,plus函數(shù)內(nèi)部的操作如下

  • num += i
    1. 101相加得到11
    2. 11這個數(shù)存回到num的內(nèi)存里
  • return num :返回此時num內(nèi)存里存放的那個值沸伏,也就是11

有相加的操作糕珊,就有addq指令,我們能在plus函數(shù)里找到如下幾句addq指令

0x100001ec6 <+86>:  addq   $0x10, %rdx
.
.
0x100001ef6 <+134>: addq   0x10(%rdx), %rcx
.
.
0x100001f57 <+231>: addq   $0x90, %rsp

因為numi都不是立即數(shù)毅糟,所以第一句第三句就可以排除掉红选,因為他們的操作數(shù)里面包含了立即數(shù),那么結(jié)果就落到了第二句上面姆另,我們在這一句加上斷點喇肋,并運行至此處坟乾,首先看一下寄存器rcx的內(nèi)容

(lldb) register read rcx
     rcx = 0x0000000000000001

說明寄存器rcx里面存放的就是i的值1,在看一下rdx的值

(lldb) register read rdx
     rdx = 0x0000000100604400

是一個堆空間地址蝶防,那么應(yīng)該就是num相關(guān)的那個堆空間甚侣,這段堆空間里面的內(nèi)容如下

(lldb) x/4gx 0x0000000100604400
0x100604400: 0x0000000100007150 0x0000000200000002
0x100604410: 0x000000000000000a 0x00000001003292a8

那么可以看到0x10(%rdx)所尋址到的那段內(nèi)存地址的值(也就是第三段那8個字節(jié))是0x0a,也就是10间学,說明他代表的就是num 殷费,我們還可以逆向追逐一下rcxrdx是從哪里取到的值

image

我們看到rcx的值來自于edi這個寄存器低葫,我們好像沒有在前面的參數(shù)傳遞流程里看到edi详羡,其實你看下圖就明白了

image

處于系統(tǒng)兼容,rdi寄存器里面的低32位是edi寄存器嘿悬,低16位是di寄存器实柠,前面我們知道rdi存放的是i的值1,也就是 0x0000000000000001善涨,因此它的低32位讀出來就是0x00000001窒盐,也就是說edi此時表達(dá)的是i的值1

接著我們看一下rdx

image

所以rdx = rsi,而進入plus之前躯概,我們已經(jīng)確定rsi = num地址 - 0x10 登钥,因此0x10(%rdx) 代表尋址到 rdx + 0x10 = num地址 - 0x10 + 0x10 = num地址,所以0x10(%rdx) = num = 10娶靡,這樣 0x100001ef6 <+134>: addq 0x10(%rdx), %rcx就完成了 num += i的操作。也就是說此時num的內(nèi)存里面存放的是11

最后我們來看一下返回的值到底是不是num內(nèi)存里的值看锉,我們直接從里ret指令最近的那個rax開始看

image

這樣姿锭,關(guān)于num的捕獲,存放伯铣,以及如何被plus函數(shù)使用的所有細(xì)節(jié)就基本上看清楚了

- 請問閉包對于外界變量的捕獲呻此,到底發(fā)生在什么時候?

我們繼續(xù)把代碼變一下

typealias Fn = (Int) -> Int

func getFn() -> Fn {
    var num = 10
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }
    
    num = 14
    
    return plus  //斷點1
}

var fn = getFn()
fn(1)

你認(rèn)為plus捕獲的10還是14呢腔寡?運行之后結(jié)果顯示焚鲜,實際捕獲的是14。我們將程序運行到斷點1處放前,查看匯編情況如下

image

根據(jù)匯編情況分析忿磅,我們發(fā)現(xiàn),編譯器實際上是將return之前凭语,將num的所有賦值都捕獲一次葱她,所以最終生成的閉包捕獲到的有效值是numreturn之前的最后一次賦值,蠻有趣似扔!接著再變一下

typealias Fn = (Int) -> Int

func getFn() -> Fn {
    var num = 10
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }
    
    num = 14
    
    return {$0} //斷點1
}

var fn = getFn()

我們將getFn的返回值替換成{$0}這個閉包表達(dá)式吨些,也就是說返回的東西跟plus無關(guān)搓谆,來看看匯編情況如何

SwiftTest`getFn():
    0x100001390 <+0>:  pushq  %rbp
    0x100001391 <+1>:  movq   %rsp, %rbp
    0x100001394 <+4>:  movq   $0x0, -0x8(%rbp)
    0x10000139c <+12>: movq   $0xa, -0x8(%rbp)
    0x1000013a4 <+20>: movq   $0xe, -0x8(%rbp)
->  0x1000013ac <+28>: leaq   0xd(%rip), %rax           ; closure #1 (Swift.Int) -> Swift.Int in SwiftTest.getFn() -> (Swift.Int) -> Swift.Int at main.swift:397
    0x1000013b3 <+35>: xorl   %ecx, %ecx
    0x1000013b5 <+37>: movl   %ecx, %edx
    0x1000013b7 <+39>: popq   %rbp
    0x1000013b8 <+40>: retq 

好家伙,編譯器直接不玩閉包這一套了豪墅,什么動態(tài)申請堆空間也沒有了泉手,也就是說,編譯器認(rèn)為既然返回的東西跟plus沒有任何關(guān)系了偶器,那就沒有必要再去費勁把力的分配各種堆空間來給你準(zhǔn)備一個閉包了斩萌,直接伸略,呵呵状囱,好暴力术裸,我喜歡~~

多變量捕獲的情況

對于單一的變量捕獲,我們在上文已經(jīng)分析完畢亭枷,那么如果捕獲了多個變量呢袭艺?首先調(diào)整一下代碼

typealias Fn = (Int) -> (Int, Int)
func getFns() -> (Fn, Fn) {
    var num1 = 0
    var num2 = 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) // 斷點2
}

let (p, m) = getFns() //斷點1

p(5)
m(4)
p(3)
m(2)

案例中,我們的閉包包含兩個函數(shù) plusminus叨粘, 捕獲了兩個變量num1num2猾编,按照我們已掌握的堆閉包的認(rèn)識,先來推斷一下

每次調(diào)用getFns函數(shù)升敲, num1num2會被捕獲到堆空間中一次答倡,并且只有一份,共本次getFns所返回的plus和minus函數(shù)使用驴党,那么我們可以推測最后的運算結(jié)果

  • p(5) --> num1 += i 等價于 num1 = 0 + 5 = 5瘪撇;num2 += i << 1 等價于 num2 = 0 + 5 * 2 = 10
  • m(4) --> num1 -= i 等價于 num1 = 5 - 4 = 1num2 += i << 1 等價于 num2 = 10 - 4 * 2 = 2
  • p(3) --> num1 += i 等價于 num1 = 1 + 3 = 4港庄;num2 += i << 1 等價于 num2 = 2 + 3 * 2 = 8
  • m(2) --> num1 -= i 等價于 num1 = 4 - 2 = 2倔既;num2 += i << 1 等價于 num2 = 8 - 2 * 2 = 4

運行程序并且打印一下,可以得到結(jié)果是符合我們上述的推斷的∨粞酰現(xiàn)在我們通過匯編來看一下渤涌,num1num2被捕獲之后,是如何在堆空間被管理的把还,我們將程序重新運行至斷點1處并查看匯編

SwiftTest`main:
    0x100001600 <+0>:   pushq  %rbp
    0x100001601 <+1>:   movq   %rsp, %rbp
    0x100001604 <+4>:   pushq  %r13
    0x100001606 <+6>:   subq   $0xb8, %rsp
    0x10000160d <+13>:  movl   %edi, -0xc(%rbp)
    0x100001610 <+16>:  movq   %rsi, -0x18(%rbp)
->  0x100001614 <+20>:  callq  0x100001770               ; SwiftTest.getFns() -> ((Swift.Int) -> (Swift.Int, Swift.Int), (Swift.Int) -> (Swift.Int, Swift.Int)) at main.swift:437
    0x100001619 <+25>:  movq   %rax, 0x7be8(%rip)        ; SwiftTest.p : (Swift.Int) -> (Swift.Int, Swift.Int)
    0x100001620 <+32>:  movq   %rdx, 0x7be9(%rip)        ; SwiftTest.p : (Swift.Int) -> (Swift.Int, Swift.Int) + 8
    0x100001627 <+39>:  movq   %rcx, 0x7bea(%rip)        ; SwiftTest.m : (Swift.Int) -> (Swift.Int, Swift.Int)
    0x10000162e <+46>:  movq   %r8, 0x7beb(%rip)         ; SwiftTest.m : (Swift.Int) -> (Swift.Int, Swift.Int) + 8
    ...
    ...
    ...
    ...

我們可以發(fā)現(xiàn)实蓬,getFns返回的內(nèi)容分別存放在rax、rdx吊履、rcx安皱、r8這四個寄存器中,并且根據(jù)注釋信息率翅,可以看出來练俐,他們將分別給pm這兩個閉包賦值。我們已經(jīng)知道冕臭,閉包的前8個字節(jié)腺晾,存放的是一個跟閉包函數(shù)相關(guān)聯(lián)的一個函數(shù)地址燕锥,后8個字節(jié)的內(nèi)容是一段管理閉包捕獲變量的堆空間地址,也就是rdxr8這兩個寄存器悯蝉,我們現(xiàn)將0x100001614 <+20>: callq 0x100001770指令走完归形,然后查看一下此時的rdxr8,根據(jù)上面的推斷鼻由,這兩個寄存器放的應(yīng)該是同一段堆空間地址暇榴,里面管理了被捕獲的num1num2,但是它們實際內(nèi)容卻是

(lldb) register read rdx
     rdx = 0x0000000100606530
(lldb) register read r8
      r8 = 0x0000000100606320

居然不是同一段堆空間蕉世,在看一下這兩個堆空間里面的內(nèi)容

(lldb) x/4xg 0x0000000100606530
0x100606530: 0x0000000100008178 0x0000000000000002
0x100606540: 0x0000000100604cb0 0x0000000100606510
(lldb) x/4xg 0x0000000100606530
0x100606530: 0x0000000100008178 0x0000000000000002
0x100606540: 0x0000000100604cb0 0x0000000100606510

居然是相同的蔼紧,此時感覺有點凌亂了,沒關(guān)系狠轻,如果你累了建議吃點零食奸例,休息十分鐘,然后我們繼續(xù)向楼〔榈酰看來隨著多變量的捕獲,以及多閉包的產(chǎn)生湖蜕,編譯器的處理情況不是我們想的那么簡單了逻卖,因此我們還是要盡到getFns里面去一探究竟,我們從新運行程序昭抒,過掉斷點1评也,來到斷點2處,此時getFns函數(shù)的匯編如下

SwiftTest`getFns():
->  0x100001770 <+0>:   pushq  %rbp
    0x100001771 <+1>:   movq   %rsp, %rbp
    0x100001774 <+4>:   subq   $0x70, %rsp
    0x100001778 <+8>:   leaq   0x69d1(%rip), %rax
    0x10000177f <+15>:  movl   $0x18, %ecx
    0x100001784 <+20>:  movl   $0x7, %edx
    0x100001789 <+25>:  movq   %rax, %rdi
    0x10000178c <+28>:  movq   %rcx, %rsi
    0x10000178f <+31>:  movq   %rdx, -0x8(%rbp)
    0x100001793 <+35>:  movq   %rax, -0x10(%rbp)
    0x100001797 <+39>:  movq   %rcx, -0x18(%rbp)
    0x10000179b <+43>:  callq  0x1000073e6               ; symbol stub for: swift_allocObject
    0x1000017a0 <+48>:  movq   %rax, %rcx
    0x1000017a3 <+51>:  addq   $0x10, %rcx
    0x1000017a7 <+55>:  movq   %rcx, %rdx
    0x1000017aa <+58>:  movq   $0x0, 0x10(%rax)
    0x1000017b2 <+66>:  movq   -0x10(%rbp), %rdi
    0x1000017b6 <+70>:  movq   -0x18(%rbp), %rsi
    0x1000017ba <+74>:  movq   -0x8(%rbp), %rdx
    0x1000017be <+78>:  movq   %rax, -0x20(%rbp)
    0x1000017c2 <+82>:  movq   %rcx, -0x28(%rbp)
    0x1000017c6 <+86>:  callq  0x1000073e6               ; symbol stub for: swift_allocObject
    0x1000017cb <+91>:  movq   %rax, %rcx
    0x1000017ce <+94>:  addq   $0x10, %rcx
    0x1000017d2 <+98>:  movq   %rcx, %rdx
    0x1000017d5 <+101>: movq   $0x0, 0x10(%rax)
    0x1000017dd <+109>: movq   -0x20(%rbp), %rdi
    0x1000017e1 <+113>: movq   %rax, -0x30(%rbp)
    0x1000017e5 <+117>: movq   %rcx, -0x38(%rbp)
    0x1000017e9 <+121>: callq  0x100007452               ; symbol stub for: swift_retain
    0x1000017ee <+126>: movq   -0x30(%rbp), %rdi
    0x1000017f2 <+130>: movq   %rax, -0x40(%rbp)
    0x1000017f6 <+134>: callq  0x100007452               ; symbol stub for: swift_retain
    0x1000017fb <+139>: leaq   0x6976(%rip), %rdi
    0x100001802 <+146>: movl   $0x20, %ecx
    0x100001807 <+151>: movq   %rcx, %rsi
    0x10000180a <+154>: movq   -0x8(%rbp), %rdx
    0x10000180e <+158>: movq   %rax, -0x48(%rbp)
    0x100001812 <+162>: movq   %rcx, -0x50(%rbp)
    0x100001816 <+166>: callq  0x1000073e6               ; symbol stub for: swift_allocObject
    0x10000181b <+171>: movq   -0x20(%rbp), %rcx
    0x10000181f <+175>: movq   %rcx, 0x10(%rax)
    0x100001823 <+179>: movq   -0x30(%rbp), %rdx
    0x100001827 <+183>: movq   %rdx, 0x18(%rax)
    0x10000182b <+187>: movq   %rcx, %rdi
    0x10000182e <+190>: movq   %rax, -0x58(%rbp)
    0x100001832 <+194>: callq  0x100007452               ; symbol stub for: swift_retain
    0x100001837 <+199>: movq   -0x30(%rbp), %rdi
    0x10000183b <+203>: movq   %rax, -0x60(%rbp)
    0x10000183f <+207>: callq  0x100007452               ; symbol stub for: swift_retain
    0x100001844 <+212>: leaq   0x6955(%rip), %rdi
    0x10000184b <+219>: movq   -0x50(%rbp), %rsi
    0x10000184f <+223>: movq   -0x8(%rbp), %rdx
    0x100001853 <+227>: movq   %rax, -0x68(%rbp)
    0x100001857 <+231>: callq  0x1000073e6               ; symbol stub for: swift_allocObject
    0x10000185c <+236>: movq   -0x20(%rbp), %rcx
    0x100001860 <+240>: movq   %rcx, 0x10(%rax)
    0x100001864 <+244>: movq   -0x30(%rbp), %rdx
    0x100001868 <+248>: movq   %rdx, 0x18(%rax)
    0x10000186c <+252>: movq   %rdx, %rdi
    0x10000186f <+255>: movq   %rax, -0x70(%rbp)
    0x100001873 <+259>: callq  0x10000744c               ; symbol stub for: swift_release
    0x100001878 <+264>: movq   -0x20(%rbp), %rdi
    0x10000187c <+268>: callq  0x10000744c               ; symbol stub for: swift_release
    0x100001881 <+273>: movq   -0x28(%rbp), %rax
    0x100001885 <+277>: movq   -0x38(%rbp), %rcx
    0x100001889 <+281>: leaq   0xc00(%rip), %rax         ; partial apply forwarder for plus #1 (Swift.Int) -> (Swift.Int, Swift.Int) in SwiftTest.getFns() -> ((Swift.Int) -> (Swift.Int, Swift.Int), (Swift.Int) -> (Swift.Int, Swift.Int)) at <compiler-generated>
    0x100001890 <+288>: leaq   0x17e9(%rip), %rcx        ; partial apply forwarder for minus #1 (Swift.Int) -> (Swift.Int, Swift.Int) in SwiftTest.getFns() -> ((Swift.Int) -> (Swift.Int, Swift.Int), (Swift.Int) -> (Swift.Int, Swift.Int)) at <compiler-generated>
    0x100001897 <+295>: movq   -0x58(%rbp), %rdx
    0x10000189b <+299>: movq   -0x70(%rbp), %r8
    0x10000189f <+303>: addq   $0x70, %rsp
    0x1000018a3 <+307>: popq   %rbp
    0x1000018a4 <+308>: retq 

首先我們知道灭返,getFns函數(shù)的返回值放在rax仇参,rcxrdx婆殿,r8這四個寄存器里面,并且返回之后罩扇,會被復(fù)制到pm這兩個閉包的內(nèi)存里面

image

image

看匯編碼的最后幾句我們可以發(fā)現(xiàn)raxrcx最后存放的是partial apply forwarder for pluspartial apply forwarder for minus這兩個函數(shù)的地址婆芦,然后被返回之后,它們會分別被存入p和m的前8個字節(jié)喂饥,所以rax消约,rcx這兩個返回值的內(nèi)容搞清楚了

我們根據(jù)現(xiàn)在的經(jīng)驗,知道了堆空間的動態(tài)申請员帮,可以看有沒有走swift_allocObject這個函數(shù)或粮,并且在之前分析閉包對單變量捕捉的情況下,每次生成一個新的閉包捞高,也就是沒調(diào)用一次getFn函數(shù)氯材,都會調(diào)用一次swift_allocObject函數(shù)來存放被捕捉的變量的值渣锦,在getFns函數(shù)的匯編代碼,我們卻發(fā)現(xiàn)氢哮,當(dāng)閉包捕捉兩個變量的時候袋毙,swift_allocObject函數(shù)被調(diào)用了4次,因此這4段堆空間應(yīng)該都跟 rdx冗尤、r8這兩個寄存器里面的值有關(guān)听盖,我們還是從函數(shù)返回的地方進行逆向追溯,下圖分析了rdx的賦值過程

image

上圖中有個細(xì)節(jié)裂七,就是申請堆內(nèi)存的大小問題皆看,對于捕獲變量num1或者num2所申請的內(nèi)存大小(也就是步驟1和步驟2)背零,和以前我們分析單變量捕獲情況一樣腰吟,也是向系統(tǒng)申請0x18(24)字節(jié),系統(tǒng)實際分配了32字節(jié)(16倍數(shù))捉兴,而這里蝎困,我們知道對于閉包mp來說,編譯器還為它們分別申請了一段堆內(nèi)存倍啥,用于管理剛才為num1num2所申請的那兩塊內(nèi)存禾乘,這個時候編譯器申請的大小是0x20(32字節(jié)),為什么呢虽缕?

分析到這里始藕,我們看出動態(tài)申請堆內(nèi)存的特點,

  • 首先前8字節(jié)釋放類型描述信息氮趋,
  • 接下來用8個字節(jié)存放引用計數(shù)伍派,用于內(nèi)存管理,
  • 然后就是存放內(nèi)容的空間剩胁,并且實際獲得大小肯定是16的倍數(shù)诉植。

對于步驟3所申請的內(nèi)存,因為接下來將要用它來存放步驟1昵观、步驟2所申請的內(nèi)存地址晾腔,因此他就需要至少32字節(jié)(16 + 8 * 2),如果我們需要捕獲3個變量啊犬,那么便需要申請 16 + 8 * 3 = 40個字節(jié)的堆內(nèi)存灼擂,如果需要捕獲n個變量,那么就需要申請 16 + 8 * n 個字節(jié)大小的堆內(nèi)存觉至。

對于r8剔应,整個賦值過程和rdx是相同的,所以函數(shù)getFns的所返回的閉包pm的內(nèi)容布局總結(jié)如下圖

image

自動閉包

看下面一個函數(shù)

//如果第一個數(shù)大于0峻贮,返回第一個數(shù)席怪,否則返回第二個數(shù)
func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {
    return v1 > 0 ? v1 : v2
}
getFirstPositive(10, 20) // --> 10
getFirstPositive(-2, 20) // --> 20
getFirstPositive(0, -4)  // --> -4

這個函數(shù)比較簡單,但是在有些場景下月洛,會有些浪費:

//如果第一個數(shù)大于0何恶,返回第一個數(shù),否則返回第二個數(shù)
func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {
    return v1 > 0 ? v1 : v2
}

func getNumber() -> Int {
    print("調(diào)用了getNumber()~~~~")
    let a = 10
    let b = 11
    return a + b
}

getFirstPositive(10, getNumber()) 
getFirstPositive(-2, getNumber()) 
getFirstPositive(0, getNumber()) 

************************控制臺結(jié)果
調(diào)用了getNumber()~~~~
調(diào)用了getNumber()~~~~
調(diào)用了getNumber()~~~~
Program ended with exit code: 0

從結(jié)果看嚼黔,我們每次調(diào)用getFirstPositive的時候细层,都觸發(fā)了getNumber函數(shù),但是getFirstPositive(10, getNumber()) 這一次調(diào)用唬涧,我們判斷第一個參數(shù)大于0之后疫赎,直接返回第一個參數(shù)就可以了,所以此時getNumber()的調(diào)用是多余的碎节,因為函數(shù)的每次調(diào)用都是要占用系統(tǒng)資源開銷的捧搞,所以這樣的現(xiàn)狀顯然不“經(jīng)濟環(huán)保

又一個辦法可以將上面的問題解決,請看代碼

func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int {
    return v1 > 0 ? v1 : v2()
}

func getNumber() -> Int {
    print("調(diào)用了getNumber()~~~~")
    let a = 10
    let b = 11
    return a + b
}

getFirstPositive(10, getNumber) 
getFirstPositive(-2, getNumber)
getFirstPositive(0, getNumber)
************************控制臺結(jié)果
調(diào)用了getNumber()~~~~
調(diào)用了getNumber()~~~~
Program ended with exit code: 0

上面狮荔,我們將getFirstPositive的第二個參數(shù)v2類型改成了函數(shù)類型 () -> Int胎撇,因此我們傳參的時候,傳入的是函數(shù)getNumber本身殖氏,此時它并沒有沒有被觸發(fā)調(diào)用晚树,只有當(dāng)v1<=0的時候,才會進行調(diào)用v2()雅采,從運行結(jié)果可以看出爵憎,卻是節(jié)省了一次函數(shù)調(diào)用。

其實這種模式有不少的應(yīng)用場景婚瓜,比如只有當(dāng)滿足某種情況的時候宝鼓,才需要進一步的執(zhí)行一些復(fù)雜的操作,例如文件讀取巴刻、網(wǎng)絡(luò)請求等愚铡。

在上面的代碼里面,getNumber函數(shù)里面就可以理解成所謂的復(fù)雜操作胡陪,在實際應(yīng)用中茂附,文件讀取、網(wǎng)絡(luò)請求等復(fù)雜操作就可以用一個函數(shù)來進行封裝督弓,但是如果我們需要的操作非常簡單,那么就可能導(dǎo)致代碼可讀性降低乒验,比如下面的代碼中愚隧,我們直接用一個閉包表達(dá)式來代替getNumber函數(shù)

func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int {
    return v1 > 0 ? v1 : v2()
}
getFirstPositive(10, {20})
getFirstPositive(10){20}

這種代碼閱讀起來,就會相當(dāng)費勁。針對這種情況狂塘,Swift給我們提供一種語法糖----自動閉包

func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
    return v1 > 0 ? v1 : v2()
}
getFirstPositive(10, 20)

我們在參數(shù)v2的類型前面加上@autoclosure這個編譯器指令录煤,這樣我們在傳參數(shù)v2的時候,就可以非常精簡getFirstPositive(10, 20)荞胡,符合我們正常的閱讀習(xí)慣妈踊,編譯器根據(jù)@autoclosure,會將參數(shù)20自動編譯成閉包表達(dá)式{20}泪漂,這樣它本質(zhì)上還是和剛才getNumber一樣廊营,是一個函數(shù),在不需要的情況下萝勤,不會進行調(diào)用露筒,所以整個代碼既保證了可讀性,又保證經(jīng)濟環(huán)保性敌卓。香不香慎式?

自動閉包注意點

  • @autoclosure會自動將20封裝成{20}
  • @autoclosure只支持 () -> T格式的參數(shù)
  • @autoclosure并非要求必須是最后一個參數(shù)
  • @autoclosure可以構(gòu)成函數(shù)重載
  • 空合并運算符 ?? 就是使用@autoclosure技術(shù)實現(xiàn)的

空合并運算符 ??@autoclosure

我們之前已經(jīng)學(xué)習(xí)過空合并運算符的使用方法,Swift對于它的實現(xiàn)如下

public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T?

throws 和 rethrows這兩個關(guān)鍵字先不考慮趟径,簡化如下

public func ?? <T>(optional: T?, defaultValue: @autoclosure ()  -> T?) -> T?

然后將范型T用Int取代瘪吏,再次簡化如下

public func ?? (optional: Int?, defaultValue: @autoclosure ()  -> Int?) -> Int?

再將optional類型Int?替換成基本類型Int,并且將函數(shù)名??換成一個正常的一個函數(shù)名funXxx

func funXxx (optional: Int, defaultValue: @autoclosure ()  -> Int) -> Int

---->對比剛才的getFirstPositive<----

func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int

相信看到這里就應(yīng)該明白 ??的本質(zhì)了吧蜗巧,點到為止掌眠,自己體會。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末惧蛹,一起剝皮案震驚了整個濱河市扇救,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌香嗓,老刑警劉巖迅腔,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異靠娱,居然都是意外死亡沧烈,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門像云,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锌雀,“玉大人,你說我怎么就攤上這事迅诬∫改妫” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵侈贷,是天一觀的道長惩歉。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么撑蚌? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任上遥,我火速辦了婚禮,結(jié)果婚禮上争涌,老公的妹妹穿的比我還像新娘粉楚。我一直安慰自己,他們只是感情好亮垫,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布模软。 她就那樣靜靜地躺著,像睡著了一般包警。 火紅的嫁衣襯著肌膚如雪撵摆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天害晦,我揣著相機與錄音特铝,去河邊找鬼。 笑死壹瘟,一個胖子當(dāng)著我的面吹牛鲫剿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播稻轨,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼灵莲,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了殴俱?” 一聲冷哼從身側(cè)響起政冻,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎线欲,沒想到半個月后明场,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡李丰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年苦锨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片趴泌。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡舟舒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嗜憔,到底是詐尸還是另有隱情秃励,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布吉捶,位于F島的核電站莺治,受9級特大地震影響廓鞠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜谣旁,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望滋早。 院中可真熱鬧榄审,春花似錦、人聲如沸杆麸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽昔头。三九已至饼问,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間揭斧,已是汗流浹背莱革。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留讹开,地道東北人盅视。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像旦万,于是被迫代替她去往敵國和親闹击。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容