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

閉包表達式(Closure Expression)

在Swift中,能夠經過func定義一個函數,也能夠經過閉包表達式定義一個函數swift

//經過func關鍵字定義函數
func sum(_ v1: Int, _ v2: Int) -> Int{v1 + v2}
複製代碼
//經過閉包表達式定義函數
var fn = {
    (v1: Int, v2: Int) -> Int in
    return v1 + v2
}

fn(10, 20)
複製代碼
//閉包表達式結構
{
    (參數列表) -> 返回值類型 in  
    函數代碼
}
複製代碼

這裏的in 是用來區分 函數類型信息函數體代碼的,上面的書寫方法不是必須的,也能夠不換行,都寫在一行裏面,可是這樣不便於代碼的閱讀。數組

閉包表達式能夠在定義的時候直接使用markdown

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

另外須要注意一點就是閉包表達式在使用的時候,不須要寫上那些在定義裏面的出現的參數標籤。網絡

1.閉包表達式的簡寫

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
})
省略參數類型
exec(v1: 10, v2: 20, fn: {
    v1, v2 in return v1 + v2
})
若是函數題代碼就是一個單一的表達式,能夠省略return關鍵字
exec(v1: 10, v2: 20, fn: {
    v1, v2 in v1 + v2
})
省略參數名,經過$0,$1,$2...來獲取按順序獲取參數
exec(v1: 10, v2: 20, fn: {
    $0 + $1
})
若是函數體的表達式比較簡單,好比這裏的+運算,那麼能夠直接寫一個+號便可,編譯器也能夠推斷來
exec(v1: 10, v2: 20, fn: +)
複製代碼

閉包表達式的簡寫須要咱們根據實際狀況,肯定一個合理的書寫簡化成都,不能爲了減小代碼量而一味使用最簡便寫法,這樣會下降代碼的可讀性,提升維護成本,凡事過猶不及。閉包

2.尾隨閉包

  • 若是將一個很長的閉包表達式做爲函數的最後一個實參,使用尾隨閉包能夠加強函數的可讀性

尾隨閉包是一個被書寫在函數調用括號外面(後面)的閉包表達式app

func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
    print(fn(v1, v2))
}
exec(v1: 10, v2: 20) {
    $0 + $1
}
複製代碼
  • 若是閉包表達式是函數的惟一實參,並且使用了尾隨閉包的語法,那就不須要在函數名後邊寫圓括號
func exec(fn: (Int, Int) -> Int) {
    print(fn(1, 2))
}

exec(fn: { $0 + $1})
exec(){ $0 + $1}
exec { $0 + $1}
複製代碼

3.示例 - 數組的排序

---> 數組排序方法的定義
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 }

複製代碼

4.忽略參數

func exec(fn: (Int, Int) -> Int) {
    print(fn(1,2))
}
//下面這種調用方法,表示忽略參數,無論傳什麼參數進來都無論
exec { _,_ in 10}
複製代碼

image 除非你給出類型 imageide

閉包(Closure)

1.關於閉包的嚴謹定義

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

複製代碼

2.閉包本質的推斷

上面的代碼演示了閉包產生的必要條件。那麼閉包的本質是什麼呢?爲了探討這個問題,首先將上面的代碼擴充以下函數

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))
複製代碼

首先說明,上述的代碼編譯正確,不會報錯。那麼咱們按照之前函數相關的知識先來分析一下這段代碼。佈局

  • 因爲num是函數getFn()內部的局部變量,在var fn = getFn()執行完成,就應該被系統回收掉了。
  • print(fn(x))的成功編譯,說明fn(x)這個用法是沒有語法錯誤的。因爲var fn = getFn()說明fn這個變量接受了getFn()的返回結果。
  • getFn()內部最後返回的是函數plus,咱們暫且簡單認爲fn就是函數plus,那麼fn(x)執行確定會調用plus()函數,也就會用到num變量。
  • 因爲剛纔分析咱們知道,getFn()裏的那個num確定是在該函數調用結束時被回收掉,而plus()執行的時候,確定是在getFn()函數結束以後,那麼它內部用到的這個num變量從哪裏來呢?

一個程序內存分佈就這麼幾個地方:代碼段、數據段、堆區、棧區。其中代碼段、數據段時編譯以後就固定好的,棧區在num被聲明的getFn()函數結束後就被回收了,咱們知道只有堆區的空間時在程序運行階段能夠用來動態分配使用的,能夠猜想,應該是在某個時刻,程序把棧上的那個num變量內容轉移到了堆上面。這樣在var fn = getFn()執行完以後,咱們還可經過print(fn(x))來繼續使用num就能夠獲得解釋了。而且還能夠進一步猜想一下,print(fn(1))~print(fn(4))所使用的num應該是堆上的同一個num,由於getFn()函數只執行了一次。那麼按照程序,打印的結果應該是一、三、六、10運行程序結果也確實如此。 另外,這裏引伸出了另一個問題,post

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

咱們有理由懷疑getFn()應該不只僅只是簡單返回了一個plus函數這麼簡單,那麼它返回的究竟是個什麼東東呢?至少咱們能猜獲得,這個返回值,能讓咱們關聯到plus函數,而且可以找獲得對上的那個num變量。

然而,光靠猜確定是不靠譜的,檢驗語法糖的惟一標準是彙編,老規矩彙編代碼走一波。

3.經過彙編來窺探閉包的本質

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

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的局部變量出現,並無被plus函數所使用,在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函數的棧空間劃定一段8個字節做爲局部變量num,而且賦值10,也就是當即數$0x0a,因此對應的是這句代碼var num = 10

根據彙編常識,函數的返回值通常存放在寄存器rax裏面,當前這條彙編指令leaq 0x15(%rip), %rax執行完以後,rax就存上了返回值,由於是leaq指令,說明往rax裏面存入的是一個內存地址,很明顯,這應該就是getFn所返回的plus函數地址。咱們能夠在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函數,究竟是不是呢,此時咱們能夠在plus函數內部加一個斷點 image

這樣繼續運行程序,就會來到plus函數的內部斷點,彙編內容以下 image 很明顯,plus函數的地址只就是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) 
複製代碼

結果很明顯,說明當前的簡單場景下(plus內部沒有使用外部函數的局部變量),getFn()只是簡單返回了plus函數的內存地址,而且被fn接收

-發生了變量捕獲行爲的情景

接下來,針對 捕獲外層函數的局部變量狀況 代碼調整以下

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)
複製代碼

咱們如今在函數plus內部使用了num,在 return plus 加斷點看下此時getFn()函數的彙編

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   

複製代碼

能夠看到,有一個最關鍵的變化就是,getFn()函數內增長了一段swift_allocObject函數的調用,看到alloc相關的函數一般就說明進行了堆空間的動態分配。這裏,應該就是咱們以前猜想的用來轉移num內容的那塊堆空間。看到這裏,咱們暫時用一個猜想回答了上一個猜想的問題,下面再深刻的進行分析。 image 此時咱們在LLDB輸出一下寄存器rax(也就是swift_allocObject函數的返回值)

(lldb) register read rax
     rax = 0x0000000100532130
複製代碼

這個地址就是swift_allocObject函數所動態分配的那一段堆空間的起始地址。按照當前的理解,被plus函數捕獲的num變量的值就存在這個段空間上,具體怎麼存的還不清楚,可是fn(1)~fn(4)的調用結果咱們看出,他們用的都是這個堆空間上的同一個num變量,所以咱們能夠在plus函數每次調用的時候,追蹤一下num的值。首先由於代碼中getFn函數的那個局部變量num值是10,所以被轉移到當前堆空間上的值也應該是10。咱們能夠在LLDB裏面看一下這段堆空間的內容,因爲不肯定這個堆空間的實際長度,因此我先看看前40個字節的內容

在這裏插入圖片描述

這裏咱們發現這段空間的17-24 這8個字節上面正好存了一個10(也就是0x0a),若是它就是那個傳說中的num,那麼fn(1)~fn(4)每次執行完以後,新的num值都放到這段空間上來。咱們跑一遍看看 fn(1)執行完以後

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

17-24字節上的內容:0x0b -> 11

fn(2)執行完以後

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

17-24字節上的內容:0x0d -> 13

fn(3)執行完以後

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

17-24字節上的內容:0x10 -> 16

fn(4)執行完以後

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

17-24字節上的內容:0x14 -> 20

這樣,就證實了這8個字節確實是咱們猜想的那段用來存儲num變量的堆空間。

-那麼每次調用getFn都確實會從新申請一段堆空間給num用嗎?

代碼再調整一下

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

複製代碼

首先,執行var fn = getFn()getFn被第一次調用

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所分配的空間地址,以及內部的num變量的值

(lldb) register read rax
     rax = 0x0000000100707c60
  
//此時堆上的num對應的地址爲0x100707c70,而且num=10 
(lldb) x/4xg 0x0000000100707c60
0x100707c60: 0x0000000100006138 0x0000000000000002
0x100707c70: 0x000000000000000a 0x0000000000000000
複製代碼

而後繼續運行至斷點2斷點3,查看此時的對內存

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

//停留在斷點2,fn(2)執行完成,num=14
(lldb) x/4xg 0x0000000100707c60
0x100707c60: 0x0000000100006138 0x0000000000000002
0x100707c70: 0x000000000000000d 0x0000000000000000
複製代碼

繼續運行程序,執行var fn1 = getFn()會再次調用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對應的地址爲0x1005108f0,而且num=10
(lldb) x/4xg 0x00000001005108e0
0x1005108e0: 0x0000000100006138 0x0000000000000002
0x1005108f0: 0x000000000000000a 0x0002000000000000
複製代碼

而後繼續運行至斷點4斷點5,查看此時的對內存

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

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

這樣咱們就看出了規律,每調用一次getFn函數,就會分配一段新的堆空間來處理num的值,相互之間互不交叉,獨立了開來。

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

由於堆空間的分配,是在函數swift_allocObject內完成的,因此到底分配了多少,應該跟該函數所傳入的參數有關聯。在彙編裏面,函數的參數通常是在調用該函數指令以前,存放在rdi、rsi、rdx、rcx、r八、r9等幾個寄存器裏面(若是還有更多的參數,編譯器會使用棧上的空間來協助存儲),那麼咱們來看一下swift_allocObject的參數 image 能夠看到這裏有個入參是0x18 (也就是24),而咱們上面的分析結果告訴咱們,num就存放在swift_allocObject函數所分配堆空間的17-24個字節上面,而前面16個字節裏面,前8個字節存放類型信息,後8個字節存放引用計數,堆空間裏面的東西其實本質上就是實例對象,那麼就必然會有類型信息和引用計數,相信不難理解image

所以這個參數0x18應該就是函數swift_allocObject用來告訴系統所須要申請的堆空間的大小。由於在iOS/OS X系統裏面,分配堆空間至少是16的倍數,因此實際上swift_allocObject結束後獲得的堆空間應該是32字節,只不過實際上只用到了其中的24個字節。實際上追蹤swift_allocObject的調用堆棧結果以下

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函數來完成動態內存分配的,至於爲何分配的空間是16的倍數請看這裏

4.閉包的內存結構

ok,到此爲止,基於演示代碼,咱們弄清楚了這麼幾個問題:

  • plus函數內部所捕獲的那個外層函數局部變量num,其實是把它的值存儲到了堆上動態申請的一段內存空間上。
  • 動態申請的內存大小是24字節,實際得到的內存大小是32字節,前16個字節存放類型描述信息和引用計數信息,接下來的8個字節用來存放num的值,剩下的暫時空閒。
  • getFn函數每調用一次,就會動態申請一段新的堆內存來存放全新的num值。

那麼var fn = getFn()中的這個fn究竟是什麼?由於fn(x)的運行結果說明,plus函數被調用,而且可以使用堆內存上的num變量。下面咱們就探索一下,它們之間是如何關聯的。

- 沒有變量捕獲時的fn結構

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

如上的案例裏面,咱們閹割掉對外層函數局部變量捕獲,只簡單返回函數sum。運行程序來到斷點1出,現輸出一下此時變量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
複製代碼

能夠看出就是函數sum的地址,咱們還能夠在斷點2出查看sum彙編代碼來進一步確認,

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函數的地址的確就是0x1000011e0,而fn變量所佔內存的大小能夠藉助MemoryLayout 肯定

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

能夠獲得變量fn被分配了16字節內存,並使用了其中16字節。 image 根據斷點位置,咱們能夠判斷var fn = getFn()已經執行完成,fn所處的位置說明它是一個全局變量,這樣咱們能夠很容易定位這句代碼所對應的彙編 image 圖中綠框的兩個地址對應的是連續的兩端內存空間,一共16字節,實際上根據Swift註釋也能夠看出,這段空間就是fn變量的內存空間。由於movq一次只能操做8個字節,因此須要對這段16字節內存空間連續兩次操做才能完成賦值。並且寄存器rax此時存的只就是sum函數的地址,下面的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的空間裏面,存儲的就是函數sum的地址,並且暫時空閒了8個字節。這就是fn的所有。 image

彙編閱讀小技巧:

  • 0xXXXX(%rip) 尋址的結果一般是全局變量(數據段)的內存地址。
  • 0xXX(%rbp) 尋址的結果一般是函數局部變量(棧空間)的內存地址。
  • 0xXX(%rax) 尋址的結果一般是堆空間(一般經過alloc系列函數動態申請)的內存地址。
  • 函數的返回值通常放在rax、rdx寄存器裏面

- 進行變量捕獲狀況下的fn結構

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函數內部使用(捕獲)了外部的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   
複製代碼

根據以前的分析,咱們已經知道var fn這個全局變量佔用16個字節,賦值須要兩次movq指令進行操做,也就是這裏 image

說明getFn()被調用以後,將返回值,放在了raxrdx這兩個寄存器裏面,而後經過上圖裏面的這兩句彙編指令,完成了var fn = getFn()這一句裏面的賦值操做。那麼raxrdx裏面到底放了什麼內容,就須要進入到getFn函數取觀察。咱們繼續運行代碼,便可來到斷點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的賦值軌跡以下圖 在這裏插入圖片描述 從圖中的軌跡路線,能夠判斷:

  • getFn返回的時候,rax裏面存放了一個叫partial apply forwarder for plus的函數的地址,不知道它是否是plus的地址,但至少是跟plus有關的一個函數。
  • rdx裏面存放的是swift_allocObject函數動態申請的堆空間地址,這段堆空間的做用以前已經證實過了,裏面的一段空間是用來存放從棧空間捕獲過來的num變量的值的

爲了驗證咱們的判斷,咱們能夠在上圖第9句彙編碼處加一個斷點,將程序從新運行到該斷點,輸出一下swift_allocObject申請到的堆空間地址

(lldb) register read rax
     rax = 0x0000000103204310
複製代碼

而後運行到第23句彙編碼處,查看此時rdx的值(這個是函數返回錢,rdx最後一次完成賦值)

(lldb) register read rdx
     rdx = 0x0000000103204310
複製代碼

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

(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>
複製代碼

因此當getFn返回以後,fn所收到的值究竟是什麼也就一清二楚了。

- plus函數在哪裏?它是如如何被調用到的?

咱們來分析一下fn(1)這句代碼,咱們知道fn裏面頭8個字節放了一個partial apply forwarder for plus函數地址,後8個字節放的是捕獲變量的那段堆地址,那麼這句代碼要作的必然(而且也只能)是去調用partial apply forwarder for plus這個函數,可是跟直接經過函數名調用一個函數不一樣(經過函數名方式,是直接對函數地址調用,例如 call 0x10000476b),但這裏的fn是一個全局變量,是一個變量哦,能夠把它理解成一個盒子,裏面能夠裝不一樣的東西,你要使用裏面的內容,就必須打開盒子,因此這是一種間接調用,在彙編裏面,間接函數調用是用 callq *[內存地址]這種格式來表示,例如callq *rax,表示根據寄存器rax裏面存儲的指針,找到指定內存,從裏面讀取8個字節的內容,做爲目標函數地址,而後進行調用。

咱們繼續運行程序,來帶斷點3,根據間接調用的語法特徵,咱們能夠看到partial apply forwarder for plus函數的調用在以下位置 image 咱們運行程序至此,而且進入該函數 image partial apply forwarder for plus函數的彙編代碼能夠清晰地看出,最後那句就是跳轉到真正的plus函數,也就是說,plus函數的地址其實是被包裹在了partial apply forwarder for plus函數函數內部,而且直接進行跳轉的。 至此,咱們就弄清楚了,最簡單的閉包產生的條件,以及閉包的內存結構,經過下圖總結一下 image 以上,咱們弄清楚了一個閉包產生的條件,以及閉包內部的具體內容,那麼不一樣閉包之間,內部的內容是否有重疊或者是共用的部分呢?咱們把代碼調整以下

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內存地址等於 0x100000eeb + 0x6325 = 0x100007210,咱們即可以在控制檯輸入打印出fn內存的內容

(lldb) x/2xg 0x100007210
0x100007210: 0x0000000100001350 0x0000000101170a30
複製代碼

繼續運行程序到斷點2,並經過一樣的方法計算獲得fn2的內存地址爲0x100007220,而且打印出它所存儲的內容

(lldb) x/2xg 0x100007220
0x100007220: 0x0000000100001350 0x00000001006598f0
複製代碼

經過對比,咱們看得出,fnfn2的前八個字節存放的內容相同,也就是partial apply forwarder for plus這個函數的地址,而他們後8個字節的內容不同,說明他們各自動態申請了本身的堆空間,用來存放所捕獲的外部變量,其實你應該能感受到,這跟類的實例對象很類似,共享方法,各自管理本身的成員變量。也就是下圖所示 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   
複製代碼

根據咱們以前對fn(1)調用特色的分析,首先會經過0x100001ad2 <+274>: callq *%rax這句彙編調用partial apply forwarder for plus函數,因此除了實參參數1plus到時會用到的num確定會也是從這裏傳進去的,咱們來看一下callq *%rax以前的參數狀況 image image

到目前爲止

  • edi = 1
  • r13 = num地址 - 0x10 (根據上圖,能夠看出num所在的位置,是r13所存放的地址開始跳過16個字節以後的那8個字節,這點藥牢記)

咱們繼續往下走,進入partial apply forwarder for plus函數內部

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的函數,後面的註釋告訴咱們,這個函數就是plus函數,那麼更新一下當前即將被plus函數使用的參數的狀況

  • edi = 1
  • rsi = num地址 - 0x10

咱們跟隨0x100001f78 <+8>: jmp 0x100001e70指令來到plus函數,其完整彙編以下

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    
複製代碼

乍一看不太好分析,咱們從關鍵點入手,咱們知道,plus函數內部的操做以下

  • num += i
    1. 101相加獲得11
    2. 11這個數存回到num的內存裏
  • return num :返回此時num內存裏存放的那個值,也就是11

有相加的操做,就有addq指令,咱們能在plus函數裏找到以下幾句addq指令

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

由於numi都不是當即數,因此第一句第三句就能夠排除掉,由於他們的操做數裏面包含了當即數,那麼結果就落到了第二句上面,咱們在這一句加上斷點,並運行至此處,首先看一下寄存器rcx的內容

(lldb) register read rcx
     rcx = 0x0000000000000001
複製代碼

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

(lldb) register read rdx
     rdx = 0x0000000100604400
複製代碼

是一個堆空間地址,那麼應該就是num相關的那個堆空間,這段堆空間裏面的內容以下

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

那麼能夠看到0x10(%rdx)所尋址到的那段內存地址的值(也就是第三段那8個字節)是0x0a,也就是10,說明他表明的就是num ,咱們還能夠逆向追逐一下rcxrdx是從哪裏取到的值

image

咱們看到rcx的值來自於edi這個寄存器,咱們好像沒有在前面的參數傳遞流程裏看到edi,其實你看下圖就明白了

image

處於系統兼容,rdi寄存器裏面的低32位是edi寄存器,低16位是di寄存器,前面咱們知道rdi存放的是i的值1,也就是 0x0000000000000001,所以它的低32位讀出來就是0x00000001,也就是說edi此時表達的是i的值1

接着咱們看一下rdx image

因此rdx = rsi,而進入plus以前,咱們已經肯定rsi = num地址 - 0x10 ,所以0x10(%rdx) 表明尋址到 rdx + 0x10 = num地址 - 0x10 + 0x10 = num地址,因此0x10(%rdx) = num = 10,這樣 0x100001ef6 <+134>: addq 0x10(%rdx), %rcx就完成了 num += i的操做。也就是說此時num的內存裏面存放的是11

最後咱們來看一下返回的值究竟是不是num內存裏的值,咱們直接從裏ret指令最近的那個rax開始看 image

這樣,關於num的捕獲,存放,以及如何被plus函數使用的全部細節就基本上看清楚了

- 請問閉包對於外界變量的捕獲,到底發生在何時?

咱們繼續把代碼變一下

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)
複製代碼

你認爲plus捕獲的10仍是14呢?運行以後結果顯示,實際捕獲的是14。咱們將程序運行到斷點1處,查看彙編狀況以下 image

根據彙編狀況分析,咱們發現,編譯器其實是將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}這個閉包表達式,也就是說返回的東西跟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>: 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 
複製代碼

好傢伙,編譯器直接不玩閉包這一套了,什麼動態申請堆空間也沒有了,也就是說,編譯器認爲既然返回的東西跟plus沒有任何關係了,那就沒有必要再去費勁把力的分配各類堆空間來給你準備一個閉包了,直接伸略,呵呵,好暴力,我喜歡~~

多變量捕獲的狀況

對於單一的變量捕獲,咱們在上文已經分析完畢,那麼若是捕獲了多個變量呢?首先調整一下代碼

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)
複製代碼

案例中,咱們的閉包包含兩個函數 plusminus, 捕獲了兩個變量num1num2,按照咱們已掌握的堆閉包的認識,先來推斷一下

每次調用getFns函數, num1num2會被捕獲到堆空間中一次,而且只有一份,共本次getFns所返回的plus和minus函數使用,那麼咱們能夠推測最後的運算結果

  • p(5) --> num1 += i 等價於 num1 = 0 + 5 = 5num2 += 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 = 4num2 += i << 1 等價於 num2 = 2 + 3 * 2 = 8
  • m(2) --> num1 -= i 等價於 num1 = 4 - 2 = 2num2 += i << 1 等價於 num2 = 8 - 2 * 2 = 4

運行程序而且打印一下,能夠獲得結果是符合咱們上述的推斷的。如今咱們經過彙編來看一下,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
    ...
    ...
    ...
    ...
複製代碼

咱們能夠發現,getFns返回的內容分別存放在rax、rdx、rcx、r8這四個寄存器中,而且根據註釋信息,能夠看出來,他們將分別給pm這兩個閉包賦值。咱們已經知道,閉包的前8個字節,存放的是一個跟閉包函數相關聯的一個函數地址,後8個字節的內容是一段管理閉包捕獲變量的堆空間地址,也就是rdxr8這兩個寄存器,咱們現將0x100001614 <+20>: callq 0x100001770指令走完,而後查看一下此時的rdxr8,根據上面的推斷,這兩個寄存器放的應該是同一段堆空間地址,裏面管理了被捕獲的num1num2,可是它們實際內容倒是

(lldb) register read rdx
     rdx = 0x0000000100606530
(lldb) register read r8
      r8 = 0x0000000100606320
複製代碼

竟然不是同一段堆空間,在看一下這兩個堆空間裏面的內容

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

竟然是相同的,此時感受有點凌亂了,不要緊,若是你累了建議吃點零食,休息十分鐘,而後咱們繼續。看來隨着多變量的捕獲,以及多閉包的產生,編譯器的處理狀況不是咱們想的那麼簡單了,所以咱們仍是要盡到getFns裏面去一探究竟,咱們重新運行程序,過掉斷點1,來到斷點2處,此時getFns函數的彙編以下

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函數的返回值放在raxrcxrdxr8這四個寄存器裏面,而且返回以後,會被複制到pm這兩個閉包的內存裏面 image

image

看彙編碼的最後幾句咱們能夠發現raxrcx最後存放的是partial apply forwarder for pluspartial apply forwarder for minus這兩個函數的地址,而後被返回以後,它們會分別被存入p和m的前8個字節,因此raxrcx這兩個返回值的內容搞清楚了

咱們根據如今的經驗,知道了堆空間的動態申請,能夠看有沒有走swift_allocObject這個函數,而且在以前分析閉包對單變量捕捉的狀況下,每次生成一個新的閉包,也就是沒調用一次getFn函數,都會調用一次swift_allocObject函數來存放被捕捉的變量的值,在getFns函數的彙編代碼,咱們卻發現,當閉包捕捉兩個變量的時候,swift_allocObject函數被調用了4次,所以這4段堆空間應該都跟 rdxr8 這兩個寄存器裏面的值有關,咱們仍是從函數返回的地方進行逆向追溯,下圖分析了rdx的賦值過程 image

上圖中有個細節,就是申請堆內存的大小問題,對於捕獲變量num1或者num2所申請的內存大小(也就是步驟1和步驟2),和之前咱們分析單變量捕獲狀況同樣,也是向系統申請0x18(24)字節,系統實際分配了32字節(16倍數),而這裏,咱們知道對於閉包mp來講,編譯器還爲它們分別申請了一段堆內存,用於管理剛纔爲num1num2所申請的那兩塊內存,這個時候編譯器申請的大小是0x20(32字節),爲何呢?

分析到這裏,咱們看出動態申請堆內存的特色,

  • 首先前8字節釋放類型描述信息,
  • 接下來用8個字節存放引用計數,用於內存管理,
  • 而後就是存放內容的空間,而且實際得到大小確定是16的倍數。

對於步驟3所申請的內存,由於接下來將要用它來存放步驟一、步驟2所申請的內存地址,所以他就須要至少32字節(16 + 8 * 2),若是咱們須要捕獲3個變量,那麼便須要申請 16 + 8 * 3 = 40個字節的堆內存,若是須要捕獲n個變量,那麼就須要申請 16 + 8 * n 個字節大小的堆內存。

對於r8,整個賦值過程和rdx是相同的,因此函數getFns的所返回的閉包pm的內容佈局總結以下圖

image

自動閉包

看下面一個函數

//若是第一個數大於0,返回第一個數,不然返回第二個數
func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {
    return v1 > 0 ? v1 : v2
}
getFirstPositive(10, 20) // --> 10
getFirstPositive(-2, 20) // --> 20
getFirstPositive(0, -4)  // --> -4
複製代碼

這個函數比較簡單,可是在有些場景下,會有些浪費:

//若是第一個數大於0,返回第一個數,不然返回第二個數
func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {
    return v1 > 0 ? v1 : v2
}

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

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

************************控制檯結果
調用了getNumber()~~~~
調用了getNumber()~~~~
調用了getNumber()~~~~
Program ended with exit code: 0
複製代碼

從結果看,咱們每次調用getFirstPositive的時候,都觸發了getNumber函數,可是getFirstPositive(10, getNumber()) 這一次調用,咱們判斷第一個參數大於0以後,直接返回第一個參數就能夠了,因此此時getNumber()的調用是多餘的,由於函數的每次調用都是要佔用系統資源開銷的,因此這樣的現狀顯然不「經濟環保

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

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

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

getFirstPositive(10, getNumber) 
getFirstPositive(-2, getNumber)
getFirstPositive(0, getNumber)
************************控制檯結果
調用了getNumber()~~~~
調用了getNumber()~~~~
Program ended with exit code: 0
複製代碼

上面,咱們將getFirstPositive的第二個參數v2類型改爲了函數類型 () -> Int,所以咱們傳參的時候,傳入的是函數getNumber自己,此時它並無沒有被觸發調用,只有當v1<=0的時候,纔會進行調用v2(),從運行結果能夠看出,倒是節省了一次函數調用。

其實這種模式有很多的應用場景,好比只有當知足某種狀況的時候,才須要進一步的執行一些複雜的操做,例如文件讀取、網絡請求等

在上面的代碼裏面,getNumber函數裏面就能夠理解成所謂的複雜操做,在實際應用中,文件讀取、網絡請求等複雜操做就能夠用一個函數來進行封裝,可是若是咱們須要的操做很是簡單,那麼就可能致使代碼可讀性下降,好比下面的代碼中,咱們直接用一個閉包表達式來代替getNumber函數

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

這種代碼閱讀起來,就會至關費勁。針對這種狀況,Swift給咱們提供一種語法糖----自動閉包

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

咱們在參數v2的類型前面加上@autoclosure這個編譯器指令,這樣咱們在傳參數v2的時候,就能夠很是精簡getFirstPositive(10, 20),符合咱們正常的閱讀習慣,編譯器根據@autoclosure,會將參數20自動編譯成閉包表達式{20},這樣它本質上仍是和剛纔getNumber同樣,是一個函數,在不須要的狀況下,不會進行調用,因此整個代碼既保證了可讀性,又保證經濟環保性。香不香?

自動閉包注意點

  • @autoclosure會自動將20封裝成{20}
  • @autoclosure只支持 () -> T格式的參數
  • @autoclosure並不是要求必須是最後一個參數
  • @autoclosure能夠構成函數重載
  • 空合併運算符 ?? 就是使用@autoclosure技術實現的

空合併運算符 ??@autoclosure

咱們以前已經學習過空合併運算符的使用方法,Swift對於它的實現以下

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

throws 和 rethrows這兩個關鍵字先不考慮,簡化以下

public func ?? <T>(optional: T?, defaultValue: @autoclosure () -> T?) -> T?
複製代碼

而後將範型T用Int取代,再次簡化以下

public func ?? (optional: Int?, defaultValue: @autoclosure () -> Int?) -> Int?
複製代碼

再將optional類型Int?替換成基本類型Int,而且將函數名??換成一個正常的一個函數名funXxx

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

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

func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int
複製代碼

相信看到這裏就應該明白 ??的本質了吧,點到爲止,本身體會。

相關文章
相關標籤/搜索