Swift之經過彙編探究閉包本質

本文首發於個人我的博客html

前言

先回顧一下,上一篇 Swift之閉包(Closure)中對閉包的解釋git

  • 一個函數和它所捕獲的變量\常量環境組合起來,稱爲閉包
    • 通常指定義在函數內部的函數
    • 通常它捕獲的是外層函數的局部變量\常量
  • 能夠把閉包想象成是一個類的實例對象
    • 內存在堆空間
    • 捕獲的局部變量\常量就是對象的成員(存儲屬性)
    • 組成閉包的函數就是類內部定義的方法

問題

先看下面一段代碼,猜猜會輸出什麼github

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)) // 1
print(fn(2))	// 3
print(fn(3))	// 6
print(fn(4))	// 10
複製代碼

結果是輸出編程

1
3
6
10
複製代碼

那麼,問題來了,爲何輸出的是10呢?由於按照常識,var num = 0 是局部變量,執行完就銷燬了,怎麼能再後面繼續使用呢?swift

驗證

咱們先從簡單的提及 首先是下面一端代碼bash

typealias Fn = (Int) -> Int
func getFn() -> Fn{
    // 局部變量
    var num = 0
    func plus(_ i: Int) -> Int{
        return i 
    }
    return plus(_:) // 這裏打斷點
}

var fn = getFn()
print(fn(1))
複製代碼

先不適用num ,直接 return i 並在這裏打斷點,結果以下閉包

testSwift`getFn():
    0x100001f70 <+0>:  pushq  %rbp
    0x100001f71 <+1>:  movq   %rsp, %rbp
    0x100001f74 <+4>:  movq   $0x0, -0x8(%rbp)
->  0x100001f7c <+12>: leaq   0xd(%rip), %rax           ; plus #1 (Swift.Int) -> Swift.Int in testSwift.getFn() -> (Swift.Int) -> Swift.Int at main.swift:23
    0x100001f83 <+19>: xorl   %ecx, %ecx
    0x100001f85 <+21>: movl   %ecx, %edx
    0x100001f87 <+23>: popq   %rbp
    0x100001f88 <+24>: retq   
複製代碼

可知,0xd(%rip), %rax 這段代碼,把地址值,也就是getFn() 函數的地址值給了rax, 根本沒有alloc malloc等代碼,也就是說,沒有開闢堆空間。那麼接下來咱們看下面的代碼app

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

斷點以下函數

testSwift`getFn():
    0x100001de0 <+0>:  pushq  %rbp
    0x100001de1 <+1>:  movq   %rsp, %rbp
    0x100001de4 <+4>:  subq   $0x20, %rsp
    0x100001de8 <+8>:  leaq   0x3301(%rip), %rdi
    0x100001def <+15>: movl   $0x18, %esi
    0x100001df4 <+20>: movl   $0x7, %edx
    
    // 這裏swift_allocObject 說明產生了堆空間
    0x100001df9 <+25>: callq  0x1000046f8        ; symbol stub for: swift_allocObject
    0x100001dfe <+30>: movq   %rax, %rdx
    0x100001e01 <+33>: addq   $0x10, %rdx
    0x100001e05 <+37>: movq   %rdx, %rsi
    0x100001e08 <+40>: movq   $0x0, 0x10(%rax)
->  0x100001e10 <+48>: movq   %rax, %rdi
    0x100001e13 <+51>: movq   %rax, -0x8(%rbp)
    0x100001e17 <+55>: movq   %rdx, -0x10(%rbp)
    0x100001e1b <+59>: callq  0x100004758         ; symbol stub for: swift_retain
    0x100001e20 <+64>: movq   -0x8(%rbp), %rdi
    0x100001e24 <+68>: movq   %rax, -0x18(%rbp)
    0x100001e28 <+72>: callq  0x100004752         ; symbol stub for: swift_release
    0x100001e2d <+77>: movq   -0x10(%rbp), %rax
    0x100001e31 <+81>: leaq   0x178(%rip), %rax   ; partial apply forwarder for plus #1 (Swift.Int) -> Swift.Int in testSwift.getFn() -> (Swift.Int) -> Swift.Int at <compiler-generated>
    0x100001e38 <+88>: movq   -0x8(%rbp), %rdx
    0x100001e3c <+92>: addq   $0x20, %rsp
    0x100001e40 <+96>: popq   %rbp
    0x100001e41 <+97>: retq   
複製代碼

進一步驗證,下面的代碼是由於,寫文章的時候,從新跑了一遍,因此函數 getFn() 函數的抵制和截圖不一致,是post

rax = 0x0000000101849fd0

此次咱們在

typealias Fn = (Int) -> Int
func getFn() -> Fn{
    // 局部變量
    var num = 0
    func plus(_ i: Int) -> Int{
		 num += i
		 return num  // 第二次這裏打斷點 查看getFn()地址的內容
    }
    return plus(_:) // 第一次這裏打斷點 獲取getFn()地址
}

var fn = getFn()
print(fn(1)) 
print(fn(2))
print(fn(3))
複製代碼

由於調用了三次 fn分別爲 fn(1) 、 fn(2)、fn(3),因此在 return num 地方,會斷三次 咱們分別查看函數getFn() 函數地址的內容

結果如圖

圖中可知,確實是操做同一塊堆空間,並且以前Swift之類中講過,前面16個字節,分別存放 類的信息,引用技術,而後後面纔是值,可知,

剛開始分配完,堆空間裏面是垃圾數據 執行完 print(fn(1)) 以後,堆空間裏面放的是1 執行完 print(fn(2)) 以後,堆空間裏面放的是3 執行完 print(fn(3)) 以後,堆空間裏面放的是6

結論

這也解釋了,文章開頭的那個疑問,由於閉包捕獲了局部變量,在堆中開闢空間,而後後面調用的時候,操做的是堆空間的內存,因此結果是

1
3
6
10
複製代碼

關於彙編的調試指令能夠參考

彙編總結

Swift官方源碼

從入門到精通Swift編程

彙編總結

Swift之閉包(Closure)

相關文章
相關標籤/搜索