從一道 iOS 面試題到 Swift 對象模型和運行時細節——「iOS 面試之道」勘誤

面試工做基本結束,若是不出什麼意外(好比資方最後撤回錄用邀約之類)的話我將會去一家我認爲比較有做爲空間的公司工做。在準備面試過程當中,我買了一本「iOS 面試之道」看,然而發現裏面在技術這一部分仍是有一些紕漏的。發現這些紕漏後我發了電子郵件給本書技術部分的共同做者,可是後來又發現其實那封郵件中也有一些錯誤。如今有時間校對了一下而且擴充了一些內容後選擇在中文 web 上刊登出來。由於其中一道題目涉及冗長的 Swift 對象模型和運行時細節,因此單獨寫成一篇文章發佈出來,其他的再組成第二篇文章。這篇文章是第一篇,主要討論「iOS 面試之道」中第 34 頁中有關鏈表的代碼範例。html

自動引用計數 + 超長鏈表釋放 == 爆棧?

我注意到本書 34 頁中有關鏈表的代碼範例使用了自動引用計數的方式管理鏈表節點的後繼節點。然而根據個人經驗,這種構造方法會形成在鏈表過長後在釋放時爆棧而最終致使 app 崩潰的隱患。node

class ListNode {
    var val: Int
    var next: ListNode?
    
    init(_ val: Int) {
        self.val = val
        self.next = nil
    }
}
複製代碼

咱們能夠打開 Xcode 新建一個 macOS 命令行工程,輸入以下代碼:git

import Foundation

print("Main thread stack size: ", Thread.main.stackSize)

class ListNode {
    var val: Int
    var next: ListNode?
    
    init(_ val: Int) {
        self.val = val
        self.next = nil
    }
}

autoreleasepool {
    let root = ListNode(0)
    
    var current = root
    
    for _ in 0..<100000 {
        let next = ListNode(0)
        current.next = next
        current = next
    }
}

print("Foo")
複製代碼

思考題:你在上述代碼中感到了什麼異樣沒有?github

若是不爆棧,那麼這個程序將最後輸出 "Foo"。可是在運行期間,這個程序就崩潰了——Xcode 會將左側的 Navigator 面板切換到 Debug Navigator 而後顯示下列內容:web

爆棧

依靠直覺,咱們能夠猜想,應該是由於編譯器給 ListNode 自動生成的 deinit 中釋放 next 的過程是一個遞歸過程,100000 個節點將致使遞歸過程太深,而後調用棧爆了(俗稱爆棧)。可是問題來了:面試

  • 真的是這樣嗎?
  • Swift 不會作尾遞歸優化嗎?

尾遞歸優化指的是,一個遞歸函數在函數體尾部調用其自身時,咱們能夠不建立一個新的棧幀(stack frame)而直接利用原有的棧幀來進行計算,這樣就能夠避免遞歸過程當中可能出現的由於調用棧太深而爆棧的危險;這個優化是編譯優化中的一個可選部分,具體有無由編譯器廠商決定。編程

要深刻理解尾遞歸以及討論 iOS 開發中的尾遞歸優化會涉及到編譯原理的運行時環境中的過程抽象這個部分,我以後會專門撰文談談 iOS 開發中的過程抽象,這裏咱們先不作深刻討論。swift

這些問題都須要咱們對 Swift 的對類實例進行釋放時的工做機制有一個基本的瞭解。設計模式

追蹤 ListNode 實例的釋放過程

爲了更加詳細地瞭解 Swift 的工做機制,咱們通常都會想到去直接考察編譯後產生的二進制文件。咱們能夠將 ListNode 部分的代碼保存爲一個名爲 CallStack.swift 的文件,而後在命令行用 swiftc CallStack.swift 編譯後再用 Hopper 反編譯出來:api

反編譯內容

顯然,Swift 編譯器也使用了相似 C++ 編譯器的 name mangling 的技術對譯元(translation unit,既參與編譯的每一份源文件,我不太清楚中文譯法,本身譯的)中的成員名字進行改編,以此讓 Swift 中函數和類型的重名現象降至最低。可是這也形成了在這張圖中,咱們除了能猜想出 valnext 這兩個 properties 的 accessors 對應的過程(既二進制文件中函數對應的機器碼)以外,就再也猜想不出究竟哪一個過程是這個自動生成的 deinit 函數對應的過程了。

「函數」和「過程」的區別在不一樣語境下不一樣。在計算機編程剛剛興起時,函數指的是有返回值的函數,而過程指的是沒有返回值的函數;到了如今,人們幾乎已經再也不區別函數和過程。在這裏,我用函數表示 Swift 中的函數,而過程表示函數在二進制文件中對應的機器碼序列。

若是這時咱們能回溯到更加高一級的編譯產物中,也許能找到答案。可是對於不少人而言,也許只知道源代碼轉換成 AST(抽象語法樹),AST 轉換成 LLVM IR(LLVM 中間表述),而後 LLVM IR 生成機器碼,再又連接器合成目標文件(可執行文件或者庫)的這個流程;而且對於這些人而言,查看和分析 AST 或者 LLVM IR 是很是陌生的。可是 Swift 的編譯過程有點不同:Swift 的編譯過程不等同於傳統 Clang + LLVM 的編譯過程,在 AST和 LLVM IR之間還會生成一個叫 SIL(Swift Intermediate Language,Swift 中間語言)的產物。SIL 是 Swift AST 和 LLVM IR 之間的一層抽象,是一種具有高層(相對於機器語言這種底層)語義的 SSA 形式(靜態單次賦值形式)的編譯器中間表述。若是你看不懂前面這句,那麼能夠認爲 SIL 就是一種具有高級語言語義(既「指令集」抽象自 Swift 運行時,而不單純是目標平臺)以及機器底層抽象(好比說使用 %0 %1 %2 ... %n 來表示虛擬寄存器,同時能夠部分操縱內存佈局的)這兩種知識的綜合體。由於其具有高層語義又兼顧底層,也許咱們能在這裏面找到 ListNodedeinit 函數的相關信息(若是你能夠上 YouTube 能夠看看這個視頻瞭解一下 SIL 是什麼,不過須要一點編譯器相關的知識)。

因而這裏咱們須要在命令行使用 swiftc -emit-sil CallStack.swift 來得到這份源代碼的 SIL。

使用 -emit-sil-emit-silgen 均可以生成 Swift SIL,可是前者會附帶語法診斷信息,然後者是生(raw)SIL。

deinit 過程分析:SIL 視角

經過搜索 ListNode.deinit 咱們能夠找到以下內容:

// ListNode.deinit
sil hidden @$s9CallStack8ListNodeCfd : $@convention(method) (@guaranteed ListNode) -> @owned Builtin.NativeObject {
// %0                                             // users: %4, %2, %1
bb0(%0 : $ListNode):
  debug_value %0 : $ListNode, let, name "self", argno 1 // id: %1
  %2 = ref_element_addr %0 : $ListNode, #ListNode.next // user: %3
  destroy_addr %2 : $*Optional<ListNode>          // id: %3
  %4 = unchecked_ref_cast %0 : $ListNode to $Builtin.NativeObject // user: %5
  return %4 : $Builtin.NativeObject               // id: %5
} // end sil function '$s9CallStack8ListNodeCfd'
複製代碼

有趣的是,即便你在 ListNode 中自定義一個空白的 deinit 函數,Swift 編譯器仍是會生成一樣的 SIL,可見 Swift 編譯器是會在自定義的 deinit 函數中自動補全該摧毀的全部實例成員的摧毀(destroy)代碼的。

因而咱們能夠知道,$s9CallStack8ListNodeCfd 這個過程對應的是 ListNode.deinit 這個函數,其 SIL 的主要內容以下:

bb0(%0 : $ListNode):
  ...
  %2 = ref_element_addr %0 : $ListNode, #ListNode.next // user: %3
  destroy_addr %2 : $*Optional<ListNode>          // id: %3
  ...
}
複製代碼

首先咱們要注意到 bb0 這個東西:

bb 意指 basic block,這是來自 LLVM 中的一個概念:既一段最基本的代碼塊。

一段改寫成 SIL 的 Swift 函數可能只有一個 basic block,也可能由於有各類控制流的加入(if-else 或者 switch-case 之類的)致使有不少個 basic blocks。bb0 就是指的函數體內的第 0 個 basic block。

而後,咱們還能夠注意到 bb0 背後還有一個 (%0 : $ListNode)

在這裏,這個 %0 本質上是 bb0 這個 basic block 內的本地變量,指的是第 0 號虛擬寄存器,而其類型就是 ListNode。特別的,這個第 0 號虛擬寄存器充當這個 basic block 的第一個「形式參數」——固然,bb0 不是函數,這裏我只是借用「形式參數」這個概念來幫助你們理解這個 %0 究竟是個什麼玩意兒。以後你還會在 SIL 中看到 %1 %2 %3 ... %n 這種表記,這些都是「第 n 號虛擬寄存器」的意思——同時他們也被都是「本地變量」。一樣,這種表記方法也來自 LLVM IR,而 SIL 借鑑之。

最後,就是 ref_element_addrdestroy_addr 這些東西是什麼:

這些東西被稱爲 SIL 指令。這些東西也是 SIL 之因此被稱爲 SIL (Swift Intermediate Language) 的緣由——由於這些 SIL 指令並非有關目標平臺指令的抽象,而是 Swift 運行時的抽象。咱們能夠將這些 SIL 指令理解爲一個函數,實際上這些指令在最後生成 LLVM IR 後也確實會去調用那些由 C++ 編寫的 Swift 運行時中對應的函數。

接下來咱們討論一下這段 SIL 幹了啥:

這部分的內容主要就是將 bb0%0 傳入 ref_element_addr 指令。而後用 %2 接住返回值,再將這個返回值傳入 destroy_addr 指令。

咱們能夠在這裏找到 ref_element_addr 指令的說明:

sil-instruction ::= 'ref_element_addr' sil-operand ',' sil-decl-ref

%1 = ref_element_addr %0 : $C, #C.field
// %0 must be a value of class type $C
// #C.field must be a non-static physical field of $C
// %1 will be of type $*U where U is the type of the selected field
//   of C
複製代碼

Given an instance of a class, derives the address of a physical instance variable inside the instance. It is undefined behavior if the class value is null.

因此咱們知道,這個指令是用來得到實例變量的地址的。

你可能看不懂 sil-instruction ::= 'ref_element_addr' sil-operand ',' sil-decl-ref 是什麼意思,不要急,我下面會講。

%2 = ref_element_addr %0 : $ListNode, #ListNode.next
複製代碼

的意思就是得到關於 %0 上這個 ListNode 實例的 ListNode.next 實例變量的地址。

接下來一句:

destroy_addr %2 : $*Optional<ListNode>
複製代碼

咱們能夠在這裏找到 destroy_addr 的文檔:

sil-instruction ::= 'destroy_addr' sil-operand

destroy_addr %0 : $*T
// %0 must be of an address $*T type
複製代碼

Destroys the value in memory at address %0. If T is a non-trivial type, This is equivalent to:

%1 = load %0
strong_release %1
複製代碼

except that destroy_addr may be used even if %0 is of an address-only type. This does not deallocate memory; it only destroys the pointed-to value, leaving the memory uninitialized.

If T is a trivial type, then destroy_addr is a no-op.

咱們能夠從中得知:這句 SIL 其實至關於執行:

strong_release %2 : $*Optional<ListNode>
複製代碼

而文檔對 strong_release 的解釋以下:

strong_release %0 : $T
// $T must be a reference type.
複製代碼

Decrements the strong reference count of the heap object referenced by %0. If the release operation brings the strong reference count of the object to zero, the object is destroyed and @weak references are cleared. When both its strong and unowned reference counts reach zero, the object's memory is deallocated.

因此咱們知道,strong_release 會減小該對象的強引用計數。在強引用計數到 0 的時候,對象被摧毀,且弱引用被置 nil。當強引用計數和 unowned 引用計數都到 0 的時候,對象內存被釋放(deallocated)。

結合咱們構造出來的 ListNode 這個類分析一下:由於除了 ListNode 體內有一個指向 next 節點的強引用以外就沒有任何 unowned 引用了,咱們不可貴出:這句 SIL 意圖是摧毀(destroy)實例變量 next,可是隨後將引起實例變量 next 背後所指向的內存空間被釋放(deallocated)。

但是咱們能夠發現,這個 deinit 函數對應的 SIL 中並無釋放(deallocate)ListNode 實例的相關內容,咱們不由要問:是否是有一個函數會「包裹」住 deinit 而後專門用來釋放 ListNode?又或者釋放一個對象實例實際上是由 Swift 運行時包辦的?

一個「驚喜」……

咱們首先驗證第一個猜想:在咱們生成的 SIL 內容中搜索 $s9CallStack8ListNodeCfd 也就是 deinit 對應的過程,以後咱們會發現 Swift 編譯器還生成了一個叫 $s9CallStack8ListNodeCfD 的過程,對應的 Swift 函數名根據 SIL 中的註釋應該是 ListNode.__deallocating_deinit。同時這個過程「包裹」了咱們的 deinit 函數:

// ListNode.__deallocating_deinit
sil hidden @$s9CallStack8ListNodeCfD : $@convention(method) (@owned ListNode) -> () {
// %0 // users: %3, %1
bb0(%0 : $ListNode):
  debug_value %0 : $ListNode, let, name "self", argno 1 // id: %1
  // function_ref ListNode.deinit
  %2 = function_ref @$s9CallStack8ListNodeCfd : $@convention(method) (@guaranteed ListNode) -> @owned Builtin.NativeObject // user: %3
  %3 = apply %2(%0) : $@convention(method) (@guaranteed ListNode) -> @owned Builtin.NativeObject // user: %4
  %4 = unchecked_ref_cast %3 : $Builtin.NativeObject to $ListNode // user: %5
  dealloc_ref %4 : $ListNode                      // id: %5
  %6 = tuple ()                                   // user: %7
  return %6 : $()                                 // id: %7
} // end sil function '$s9CallStack8ListNodeCfD'
複製代碼

因此咱們第一個猜想是真的。

__deallocating_deinit 過程分析:SIL 視角

ListNode.__deallocating_deinit 函數的 SIL 中的主要內容以下:

bb0(%0 : $ListNode):
  %2 = function_ref @$s9CallStack8ListNodeCfd : $@convention(method) (@guaranteed ListNode) -> @owned Builtin.NativeObject // user: %3
  %3 = apply %2(%0) : $@convention(method) (@guaranteed ListNode) -> @owned Builtin.NativeObject // user: %4
  %4 = unchecked_ref_cast %3 : $Builtin.NativeObject to $ListNode // user: %5
  dealloc_ref %4 : $ListNode                      // id: %5
  ...
}
複製代碼

這段 SIL 中的內容比較繁複,主要是將 bb0%0 傳遞給了過程 $s9CallStack8ListNodeCfd(也就是 ListNode.deinit 函數對應的 SIL 函數),再用 %3 接住上面這個函數的返回值後將返回值扮演(cast,我不知道這個字怎麼譯,本身想的譯法)成 ListNode 儲存到 %4 中,而後將 %4 做爲實際參數傳遞給 SIL 指令 dealloc_ref

咱們能夠在這裏找到 dealloc_ref 的文檔:

sil-instruction ::= 'dealloc_ref' ('[' 'stack' ']')? sil-operand

dealloc_ref [stack] %0 : $T
// $T must be a class type
複製代碼

Deallocates an uninitialized class type instance, bypassing the reference counting mechanism.

The type of the operand must match the allocated type exactly, or else undefined behavior results.

The instance must have a retain count of one.

This does not destroy stored properties of the instance. The contents of stored properties must be fully uninitialized at the time dealloc_ref is applied.

The stack attribute indicates that the instruction is the balanced deallocation of its operand which must be a alloc_ref [stack]. In this case the instruction marks the end of the object's lifetime but has no other effect.

因而咱們能夠知道,dealloc_ref 這個 SIL 指令是用來釋放而且反初始化類實例的,而後這個過程會忽略引用計數,而且要求實例的引用計數爲 1。同時,這個 SIL 指令不會摧毀(destroy)實例內部的 stored properties,stored properties 中的內容必須在執行 dealloc_ref 指令前就被清除掉(就是 ListNode.deinit 所幹的活了)。

可能不少人看不懂 sil-instruction ::= 'dealloc_ref' ('[' 'stack' ']')? sil-operand 之類的是什麼。我曾經在一本 90 年代在美國發行的有關編譯器構造的書上看到過這種表述,叫 EBNF,既 Extended Backus–Naur Form,是一種上下文無關語法(CFG)的表記方法。看不懂「上下文無關語法」的人能夠理解爲這是一種將編程語言全部語義上的有效性(好比函數傳參時類型必須匹配,變量賦值時類型必須匹配等)剝去以後剩下的語言結構。固然,這玩意兒可不止用在編譯器裏面,天然語言也能夠應用。在編譯器相關課程中咱們通常都會學習 BNF 表記法,而這個就是 BNF 的擴展版。其擴展出來的一些寫法可使得原來用 BNF 表記的上下文無關語法(CFG)能夠撰寫得更加簡潔。

這句 EBNF 表記用中文讀出來應該讀做:

SIL 指令 (sil-instruction) 能夠推導出 "dealloc_ref" "[" "stack" "]" SIL算子 (sil-operand),其中 "[" "stack" "]" 是可選的部分。

若是咱們對這句 EBNF 所描述的「上下文無關語法」規則能夠接受的輸入進行舉例,那麼將有:

dealloc_ref [ stack ] %foo : $Bar

dealloc_ref %foo : $Bar
複製代碼

若是使用 BNF 改寫,那麼咱們就必須寫成這樣

sil-instruction ::= 'dealloc_ref' stack-operand-or-epsilon sil-operand
stack-operand-or-epsilon ::= 'stack'
                          |  ε
複製代碼

是否是 EBNF 簡潔了不少呢?

你可能還會注意到 dealloc_ref 這個指令的文檔提到了分配一個類實例的 SIL 指令 alloc_ref 以及一個叫 [stack] 的參數。根據上面我對 EBNF 的解釋,你不可貴出其實 Swift 是支持在 stack 內存上分配類實例這點事實。可是我還沒空去研究如何在代碼中實現這點,因此這裏不展開說明。

ListNode 實例釋放過程 SIL 總結

咱們能夠從上述節選的 SIL 內容推測出來 ListNode.__deallocating_deinit 這個函數是用來釋放(deallocate)heap 內存上的爲對象分配的內存的,而這個過程當中則調用了 ListNode.deinit。這個 ListNode.deinit 函數則負責對實例內部的成員進行摧毀(destroy)。固然,在這裏 ListNodenext 成員並無其餘 unowned 引用或者強引用,因而摧毀(destroy)這個 next 實例成員的同時也會引起 next 節點指向的內存區域被釋放(deallocated)。顯然,上述過程將引起一個間接遞歸調用。

如今咱們能夠再次打開 Hopper 找到 deinit 對應的過程。

deinit 對應過程

其中咱們發現 swiftc 爲 deinit 生成機器碼後這個過程實際上會調用一個叫 _$s9CallStack8ListNodeCSgWOh 的過程,因而咱們找到它:

swift_release

是的,你會看到最終這個過程調用了 swift_release 這個函數,根據咱們以前對 deinit 的 SIL 的研究,這個函數應該就是對應着 dealloc_ref 這個 SIL 指令,也應該就是這個過程最終行使了對 next 節點的釋放。

思考題:請猜想 Swift 爲 ListNode 的成員摧毀過程單獨生成一個函數,並從 deinit 外聯(outline)到該函數的設計意圖。

咱們能夠從名字上猜想出這個函數的做用可能和 [NSObject -release] 或者 CFRelease 差很少,根據咱們對引用計數的常識,其行爲也應該就是進行引用計數到 0 以後將內存釋放這麼一個動做。可是這只是猜想,咱們還要從源代碼角度對其進行驗證。

在這裏咱們要說明的是,swift_release 是 Swift 運行時的一部分,由 Swift 標準庫提供。 Swift 標準庫在 Swift 程序被編譯的過程當中將會被連接到目標文件上。如下分析過程當中咱們都不能忘記咱們是在考證 Swift 運行時的構成,而不是咱們寫的關於 ListNode 這個程序的構成。

追蹤 swift_release

使用 git 克隆 Swift 源代碼,並將分支切換到 swift-5.0-branch

在 Swift 源代碼中搜索 swift_release 咱們能夠在 include/swift/Runtime/HeapObject.h 找到下列代碼:

namespace swift {

...

SWIFT_RUNTIME_EXPORT void swift_release(HeapObject *object);

...

}
複製代碼

在這裏咱們能夠看見 swift_release 是一個 swift 這個命名空間下一個擁有 HeapObject * 類型形式參數的函數,可是咱們還不能肯定這就是咱們要找的——由於這部分代碼是由 C++ 實現的 Swift 的運行時代碼,在 C++ 中開發者能夠對函數名字改編(name mangling)規則進行選擇——既,是用 C 的規則改編仍是用 C++ 的規則改編。

根據 C 的規則進行改編後,swift_release 應該叫 _swift_release;而根據 C++ 的規則改編後,抱歉,C++ 的改編規則太複雜,我也記不得……可是會和 _swift_release 差很遠。

而若是要讓一個 C++ 頭/源文件中的函數或者變量名字使用 C 的名字改編規則進行名字改編,那麼就必須加上 extern "C" 這個前綴。

爲了求證這個函數就是咱們要找的 swift_release 函數,咱們須要找到 SWIFT_RUNTIME_EXPORT 這個宏。咱們能夠在 stdlib/public/SwiftShims/Visibility.h 找到這個宏的定義:

#if defined(__cplusplus)
#define SWIFT_RUNTIME_EXPORT extern "C" SWIFT_EXPORT_ATTRIBUTE
#else
#define SWIFT_RUNTIME_EXPORT SWIFT_EXPORT_ATTRIBUTE
#endif
複製代碼

咱們發現這是一套能夠根據語言來作切換的宏,其中若是是按照 C++ 來編譯(既被 C++ 源文件 include)的話,SWIFT_RUNTIME_EXPORT 的內容就是extern "C" SWIFT_EXPORT_ATTRIBUTE,而若是按照 C 來編譯(既被 C 源文件 include)的話就是 SWIFT_EXPORT_ATTRIBUTE。因而咱們能夠知道,這個在 swift 命名空間下的 swift_release 函數在完成編譯後其二進制符號並不會以 C++ 的方式進行改編,以 C 的方式進行改編。因此咱們能夠肯定這就是咱們要找的函數。

小議 swift_release 函數簽名

在這裏咱們還有一個疑問:爲何這個函數的形式參數是 HeapObject * 類型的?

不少人以爲,多是由於 Swift 和 Objective-C 同樣有一個公共根類——只是說 Swift 沒有把這個公共根類暴露出來。

這樣說能夠說對了一半,可是在這裏屬於答非所問了:確實,在後面咱們能夠看到:一旦 Swift 編譯過程當中選擇了支持 Objective-C 運行時,那麼在 Swift 中除了 @objc 這種消息轉發級別的保證 Swift 對象和 Objective-C 對象間互操做性(interoperability)的語言特性以外,每個 Swift 類都會有一個對應的 Objective-C 類以保證 Swift 和 Objective-C 之間最基本的生命週期控制方面的互操做性,而全部 Swift 類的 Objective-C 對應類都繼承自一個公共根類就是 SwiftObject

至於「全部 Swift 類都有一個公共根類叫 HeapObject」,這就差得遠了。爲何?

首先,C++ 中爲了保證和 C 語言在「值語義」上的統一,其 structclass 並無什麼實質上的區別,在內存佈局上也是如出一轍。C++ 的創造者之因此保留了 struct 僅僅是爲了和 C 兼容。因此即便上述「全部 Swift 類都有一個公共根類HeapObject」的描述爲真,那麼也應該改爲「全部 Swift 類都有一個公共根類型HeapObject」.

「值語義」是什麼?

「值語義」的對義語是「引用語義」,用 Swift 的代碼表示,他們的區別以下:

var a: Int = 4
b = a
複製代碼

上述代碼中 a 複製到 b 後再無關聯,這就是「值語義」的。

class Foo {
    var int: Int = 0
    init(int: Int) { self.int = int }
}

var a = Foo(int: 0)
b = a
b.int = 10 // a.int == 10; b.int == 10;
複製代碼

上述代碼中 a 複製到 b 後依然指向同一個對象,修改 b 的內容會致使 a 的內容同時產生變化,這就是「引用語義」的。

可是若是在 C++ 中咱們書寫相似的代碼:

class Foo {
    int value;
    Foo(int val): value(val) {}
}

auto a = Foo(0);
auto b = a;
b.value = 10; // a.int == 0; b.int == 10;
複製代碼

那麼 avalue 將不會隨 bvalue 被賦值而一塊兒被改變。

而通常,咱們在 C++ 中都會這樣:

class Foo {
    int value;
    Foo(int val): value(val) {}
}

auto a = new Foo(0);
auto b = a;
b -> value = 10; // a -> int == 10; b -> int == 10;
複製代碼

咱們能夠看到,咱們將 Foo(0) 改爲了 new Foo(0),而後也將 b.value = 10; 改爲了 b -> value = 10;,實際上這是將 Foo 的實例分配到了 heap 內存上,最後返回了一個指針。這樣以來,咱們就能夠達到和上述 Swift 代碼同樣的效果了。可是這仍然不是「引用語義」的——由於 new Foo(0) 返回的是一個指向 Foo 實例的指針而不是 Foo 自己,另外操做符 -> 是一個對指針起做用的操做符,上述代碼是圍繞指針展開的而非類型自己。因此這種用指針達到「引用語義」效果的作法並不表明擁有指針的編程語言其自己是包含「引用語義」的。(固然,在最新的 C++ 實踐中咱們應當使用智能指針,特別的,在這裏咱們應當使用 std::shared_ptr,可是這並不妨礙咱們解釋 C++ 和 C 在「值語義」上的統一性。)

思考題:在明白了「值語義」和「引用語義」的區別後,我再問你在 Swift 中何時使用 struct,何時使用 class,你還會給出網上那些所謂的「標準答案」嗎?

其次,咱們能夠從 C++ 對象模型(既內存佈局)的角度來討論——由於若是一個 Swift 類要以一個 C++ 類型爲根類型的話,那麼 Swift 對象和 C++ 的對象至少在內存佈局上要是一致的。

咱們在 include/swift/SwiftShims/HeapObject.h 中找到 HeapObject 類型的定義:

#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS       \
  InlineRefCounts refCounts

/// The Swift heap-object header.
/// This must match RefCountedStructTy in IRGen.
struct HeapObject {
  /// This is always a valid pointer to a metadata object.
  HeapMetadata const *metadata;

  SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;

#ifdef __cplusplus
  HeapObject() = default;

  // Initialize a HeapObject header as appropriate for a newly-allocated object.
  constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }

#ifndef NDEBUG
  void dump() const LLVM_ATTRIBUTE_USED;
#endif

#endif // __cplusplus
}
複製代碼

能夠發現這是一個 struct,其中包含一個數據成員 metadata 和一個宏 SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS

咱們將宏 SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS 展開後能夠獲得 InlineRefCounts refCounts,而咱們又能夠在 include/swift/SwiftShims/RefCount.h 中找到 InlineRefCounts 的定義:

// This definition is a placeholder for importing into Swift.
// It provides size and alignment but cannot be manipulated safely there.
typedef struct {
  __swift_uintptr_t refCounts SWIFT_ATTRIBUTE_UNAVAILABLE;
} InlineRefCountsPlaceholder;

#if !defined(__cplusplus)

typedef InlineRefCountsPlaceholder InlineRefCounts;

#else

...

typedef RefCounts<InlineRefCountBits> InlineRefCounts;

...

#endif
複製代碼

咱們發現這個類型在 C 和 C++ 中看起來會不同:

  • 在 C 中這個類型你是沒法對其進行操做的
  • 在 C++ 中這個類型是 RefCounts<InlineRefCountBits>

而最終咱們能夠在 stdlib/public/SwiftShims/RefCount.h 中找到 RefCountsInlineRefCountBits 的定義:

enum RefCountInlinedness { RefCountNotInline = false, RefCountIsInline = true };

...

typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;

...

template <typename RefCountBits>
class RefCounts {
  std::atomic<RefCountBits> refCounts;
  
  ...
}
複製代碼

咱們繼續能夠在同一個文件內找到 RefCountBitsT 的定義:

// Basic encoding of refcount and flag data into the object's header.
template <RefCountInlinedness refcountIsInline>
class RefCountBitsT {
  ...

  BitsType bits;
  
  ...
}
複製代碼

因此咱們能夠知道,HeapObject 最終看起來是這樣子的:

struct HeapObject {
  HeapMetadata const *metadata;

  RefCounts<RefCountBitsT<true>> refCounts;

#ifdef __cplusplus
  HeapObject() = default;

  // Initialize a HeapObject header as appropriate for a newly-allocated object.
  constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }

#ifndef NDEBUG
  void dump() const LLVM_ATTRIBUTE_USED;
#endif

#endif // __cplusplus
}
複製代碼

struct(包括 class)在 C++ 中存在兩種內存佈局:

  • 一種是和 C struct 同樣的內存佈局。

    這種佈局保證了和 C API 的互操做性。

  • 一種是擁有 C++ 運行時特性的內存佈局。

    這種佈局不能保證和 C API 的互操做性,可是擁有 C++ 特有的虛函數、值語義相關的構造函數、析溝函數、拷貝構造函數和賦值函數這些運行時特性。

因此說,若是 Swift 類都以一個 C++ 類型做爲根類型,那麼:

  • 要麼 Swift 類的實例會和 C struct 是同樣的佈局;
  • 要麼 Swift 類的實例會和擁有 C++ 特性的 C++ 類型實例是同樣的內存佈局。

很明顯,一個 Swift 類是擁有繼承能力的,而繼承以後的類若是沒有被標記爲 final 或者其成員函數沒有被標記爲 final 的話,那麼將須要使用相似 C++ 中 vtable 的技術來對繼承後複寫(override,本身譯的)了的函數進行消息轉發,因此 Swift 類的實例不可能和 C struct 是同樣的佈局。

這樣以來,若是咱們要證實或者證否 Swift 類都擁有一個「隱性」的 C++ 根類型——既 HeapObject 的話,僅僅須要查證 HeapObject 這個類型自己是否是一個擁有 C++ 特性的 C++ 類型就能夠了。

咱們能夠看到,上述 HeapObject 的源代碼有以下宏:

#ifdef __cplusplus
  ...
#endif // __cplusplus
複製代碼

這是一段典型的斷定引用該頭文件的究竟是 C 仍是 C++ 源文件的宏,若是是 C++ 的源文件引用了該頭文件,那麼這個宏內包裹的內容將生效,若是是 C,那麼將無效。

同時這段宏內的內容:

HeapObject() = default;

  // Initialize a HeapObject header as appropriate for a newly-allocated object.
  constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }

#ifndef NDEBUG
  void dump() const LLVM_ATTRIBUTE_USED;
#endif
複製代碼

其目的是定義一個默認的默認構造函數(比較拗口)、一個以常量表達式(constexpr)修飾的構造函數以及一個 debug 用的成員函數。在這些 C++ 的「私貨」中:

  • 咱們能夠看到 HeapObject 的構造函數 constexpr HeapObject(HeapMetadata const *newMetadata) 是一個 constexpr 函數,C++ 將在編譯時就對這個函數進行求值,而後將求值以後的結果寫入編譯產物;

    HeapObject 的默認構造函數 HeapObject() = default; 是默認的。

    因此這些構造函數都是 trivial 的。Trivial 是一個借鑑自數學的概念,在 C/C++ 中指能夠經過簡單的賦值完成的動做。這些動做並不會致使 HeapObject 異化成一個僅僅兼容 C++ 的類型——由於這些構造函數並不須要使用到 C++ 的運行時特性;

  • 新加的成員函數 void dump() const 是非虛(non-virtual)函數。

    這至關於新增一個全局函數 void HeapObjectDump(HeapObject * this),且不會致使 HeapObject 異化成一個只能在 C++ 中使用的類型——由於非虛函數也不須要使用到 C++ 運行時特性。

從以上這些看,HeapObject 是考慮了與 C 的兼容的——也就是說 HeapObject 採起的應該是和 C struct 同樣的佈局。如今咱們能夠給出 HeapObject 實例的內存佈局:

而一個使用 C struct 佈局的 C++ 類型怎麼可能會是全部 Swift 類的根類型呢?

因此咱們知道「全部 Swift 類都有一個公共根類型HeapObject」這點應該是不成立的。

那麼到底爲何 swift_release 的形式參數會是 HeapObject * 類型的?

實際上,後面咱們會看到,swift_release 的形式參數爲 HeapObject * 類型的在這裏是一種編程技巧,目的是爲 HeapObject * 指向的內存區域提供一個能夠在 C/C++ 中訪問的途徑。

繼續探索 swift_release

好的。如今咱們繼續探索 swift_release。由於 swift_release 是一個 swift 命名空間下的函數,咱們能夠嘗試搜索 void swift::swift_release( 以找到這個函數的實現文件。在 stdlib/public/Runtime/HeapObject.cpp 中咱們能夠發現以下內容:

...

void swift::swift_release(HeapObject *object) {
  _swift_release(object);
}

static void _swift_release_(HeapObject *object) {
  SWIFT_RT_TRACK_INVOCATION(object, swift_release);
  if (isValidPointerForNativeRetain(object))
    object->refCounts.decrementAndMaybeDeinit(1);
}

auto swift::_swift_release = _swift_release_;

...
複製代碼

咱們能夠看到 swift_release 這個函數最後跳轉到了 _swift_release_ 這個函數內,而 _swift_release_ 在進行了引用計數追蹤(SWIFT_RT_TRACK_INVOCATION)和原生 Swift 對象的檢查(isValidPointerForNativeRetain)後調用了 decrementAndMaybeDeinit 這個函數。

再搜索 decrementAndMaybeDeinit 找到其頭文件 stdlib/public/SwiftShims/Refcount.h,而後咱們能夠發現以下內容:

enum PerformDeinit { DontPerformDeinit = false, DoPerformDeinit = true };

...

template <typename RefCountBits>
class RefCounts {
  std::atomic<RefCountBits> refCounts;

  ...

  LLVM_ATTRIBUTE_ALWAYS_INLINE void decrementAndMaybeDeinit(uint32_t dec) {
    doDecrement<DoPerformDeinit>(dec);
  }

  ...
}
複製代碼

咱們看到 decrementAndMaybeDeinit 調用了 doDecrement 這個模板函數而且將 true 傳入了第一個模板參數。因而咱們能夠在同一個文件找到這個模板函數:

template <typename RefCountBits>
class RefCounts {
  ...
  
  template <PerformDeinit performDeinit>
  bool doDecrement(uint32_t dec) {
    auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
    RefCountBits newbits;
    
    do {
      newbits = oldbits;
      bool fast =
        newbits.decrementStrongExtraRefCount(dec);
      if (!fast)
        // Slow paths include side table; deinit; underflow
        return doDecrementSlow<performDeinit>(oldbits, dec);
    } while (!refCounts.compare_exchange_weak(oldbits, newbits,
                                              std::memory_order_release,
                                              std::memory_order_relaxed));

    return false;  // don't deinit
  }

  ...
}
複製代碼

這個函數中出現了咱們很喜歡討論的 lock free 編程技巧——CAS(compare and swap),可是咱們在這裏並不關心這個。咱們關心的是在 do...while 循環體內 if(!fast) 這個條件分支下的代碼:總體而言,這整個函數負責減小對象的引用計數,而且在適當的時候(沒法套用 fast path,既包含 side table、會致使 deinit 以及引用計數小於 0)會調用模板函數 doDecrementSlow

咱們在上述源代碼中看到了一個概念叫 underflow。根據我對 Swift 源代碼和文檔的研讀,underflow 在 Swift 中至少對應三個意義,而這三個意義其實均可以看做是 overflow 在相應語境下的對義語:

  1. 數值向下越界:你聲明瞭一個值爲 90 的無符號整數,卻減去了 91;
  2. 緩衝區向後(低地址空間)越界:你聲明瞭一個容量爲 10 個元素的 buffer,卻嘗試訪問第 -1 個位置的元素;
  3. 引用計數向下越界,或者說引用計數小於 0;

咱們接着能夠在同一個文件內找到 doDecrementSlow 這個模板函數:

template <typename RefCountBits>
class RefCounts {
  ...

  template <PerformDeinit performDeinit>
  bool doDecrementSlow(RefCountBits oldbits, uint32_t dec) {
    RefCountBits newbits;
    
    bool deinitNow;
    do {
      newbits = oldbits;
      
      bool fast =
        newbits.decrementStrongExtraRefCount(dec);
      if (fast) {
        // Decrement completed normally. New refcount is not zero.
        deinitNow = false;
      }
      else if (oldbits.hasSideTable()) {
        // Decrement failed because we're on some other slow path.
        return doDecrementSideTable<performDeinit>(oldbits, dec);
      }
      else {
        // Decrement underflowed. Begin deinit.
        // LIVE -> DEINITING
        deinitNow = true;
        assert(!oldbits.getIsDeiniting());  // FIXME: make this an error?
        newbits = oldbits;  // Undo failed decrement of newbits.
        newbits.setStrongExtraRefCount(0);
        newbits.setIsDeiniting(true);
      }
    } while (!refCounts.compare_exchange_weak(oldbits, newbits,
                                              std::memory_order_release,
                                              std::memory_order_relaxed));
    if (performDeinit && deinitNow) {
      std::atomic_thread_fence(std::memory_order_acquire);
      _swift_release_dealloc(getHeapObject());
    }

    return deinitNow;
  }
  
  ...
}
複製代碼

這個函數在確認要執行 deinit 以後將執行 _swift_release_dealloc(getHeapObject()); 來對 heap 上爲對象分配的內存進行釋放,而以前咱們已經在 Debug Navigator 的調用棧中看到了 _swift_release_dealloc

可是這個 _swift_release_dealloc 裏面又作了什麼呢?

_swift_release_dealloc 初探

咱們繼續搜索 void _swift_release_dealloc( 找到其實現文件 include/swift/Runtime/HeapObject.h,內容有下:

void _swift_release_dealloc(HeapObject *object) {
  asFullMetadata(object->metadata)->destroy(object);
}
複製代碼

咱們能夠看見,這個函數將 HeapObject * 指針指向的實例中的 metadata 成員傳遞給了 asFullMetadata 函數,而後又調用了返回值上一個叫 destroy 的看起來像是一個「函數」的成員。

如今咱們來考察asFullMetadata 這個模板函數。咱們能夠搜索源代碼,在 include/Swift/ABI/Metadata.h 中找到相關內容:

...

/// Given a canonical metadata pointer, produce the adjusted metadata pointer.
template <class T> static inline FullMetadata<T> *asFullMetadata(T *metadata) {
  return (FullMetadata<T>*) (((typename T::HeaderType*) metadata) - 1);
}

...
複製代碼

咱們能夠看到,asFullMetadata 其實是一個模板函數。在這個模板函數中,其實際上的動做是:

  1. metadata 扮演(cast)成 T::HeaderType * 類型;
  2. 而後再向後(或者說低地址空間)位移了一個 T::HeaderType 的長度;
  3. 最後再扮演(cast)成 FullMetadata<T> * 類型返回。

因此若是咱們要了解 asFullMetadata(object->metadata)->destroy(object); 這句到底作了什麼,咱們就必需要了解:

  1. metadata 的類型下的 HeaderType 是什麼?
  2. metadata 通過位移後,最後扮演(cast)成的類型 FullMetadata<T> * 是什麼?
  3. FullMetadata<T> 的成員 destroy 是什麼?
  4. metadata 向低地址空間位移一個 T::HeaderType 長度的位置存放的是什麼?

1. metadata 的類型下的 HeaderType 是什麼?

咱們已經從前文中 HeapObject 的頭文件中得知 metadata 的類型是 HeapMetadata,而在 include/swift/Runtime/HeapObject.h 中咱們又能夠找到:

struct InProcess;

template <typename Target> struct TargetHeapMetadata;
using HeapMetadata = TargetHeapMetadata<InProcess>;
複製代碼

因此咱們能夠看到 HeapMetadata 的真身實際上是 TargetHeapMetadata<InProcess>

咱們如今要接着查證 TargetHeapMetadata 的內容。咱們能夠搜索 struct TargetHeapMetadatainclude/swift/ABI/Metadata.h 中找到以下內容:

template <typename Runtime>
struct TargetHeapMetadata : TargetMetadata<Runtime> {
  using HeaderType = TargetHeapMetadataHeader<Runtime>;

  TargetHeapMetadata() = default;
  constexpr TargetHeapMetadata(MetadataKind kind)
    : TargetMetadata<Runtime>(kind) {}
#if SWIFT_OBJC_INTEROP
  constexpr TargetHeapMetadata(TargetAnyClassMetadata<Runtime> *isa)
    : TargetMetadata<Runtime>(isa) {}
#endif
};
複製代碼

因而咱們知道了 metadata 類型下的 HeaderType 就是 TargetHeapMetadataHeader<InProcess>。其定義也能夠在 include/swift/ABI/Metadata.h 中被找到:

template <typename Runtime>
struct TargetHeapMetadataHeader
    : TargetHeapMetadataHeaderPrefix<Runtime>,
      TargetTypeMetadataHeader<Runtime> {
  constexpr TargetHeapMetadataHeader(
      const TargetHeapMetadataHeaderPrefix<Runtime> &heapPrefix,
      const TargetTypeMetadataHeader<Runtime> &typePrefix)
    : TargetHeapMetadataHeaderPrefix<Runtime>(heapPrefix),
      TargetTypeMetadataHeader<Runtime>(typePrefix) {}
};
複製代碼

2. metadata 通過位移後,最後扮演(cast)成的類型 FullMetadata * 是什麼?

咱們一樣能夠在 include/swift/ABI/Metadata.h 中找到以下內容:

template <class T> struct FullMetadata : T::HeaderType, T {
  typedef typename T::HeaderType HeaderType;

  FullMetadata() = default;
  constexpr FullMetadata(const HeaderType &header, const T &metadata)
    : HeaderType(header), T(metadata) {}
};
複製代碼

因此咱們能夠知道:metadata 通過位移後,最終將會被扮演(cast)成 FullMetadata<HeapMetadata> *

根據 FullMetadata<T> 的定義,FullMetadata<T> 被特化成 FullMetadata<HeapMetadata> 以後將繼承 TargetHeapMetadataHeader<InProcess>TargetHeapMetadata<InProcess>

又根據咱們以前的考證:

  • 由於 FullMetadata 的第一繼承目標 TargetHeapMetadataHeader 自己沒有數據成員,要考證其數據成員,咱們僅須要展開 TargetHeapMetadataHeader 的繼承目標 TargetHeapMetadataHeaderPrefixTargetTypeMetadataHeader,進而就能夠知道 FullMetadata 的一部分數據成員。

    咱們能夠在 include/swift/ABI/Metadata.h 中找到上述兩個類型的定義:

    template <typename Runtime>
    struct TargetTypeMetadataHeader {
      /// A pointer to the value-witnesses for this type. This is only
      /// present for type metadata.
      TargetPointer<Runtime, const ValueWitnessTable> ValueWitnesses;
    };
    
    ...
    
    template <typename Runtime>
    struct TargetHeapMetadataHeaderPrefix {
      /// Destroy the object, returning the allocated size of the object
      /// or 0 if the object shouldn't be deallocated.
      TargetPointer<Runtime, HeapObjectDestroyer> destroy;
    };
    複製代碼

    咱們不可貴出 FullMetadata<HeapMetadata> 下第一個數據成員是 :

    TargetPointer<Runtime, HeapObjectDestroyer> destroy;
    複製代碼

    第二個數據成員是:

    TargetPointer<Runtime, const ValueWitnessTable> ValueWitnesses;
    複製代碼
  • FullMetadata 的第二繼承目標 TargetHeapMetadata 自己也沒有數據成員,要考證其數據成員,咱們僅須要展開 TargetHeapMetadata 的繼承目標 TargetMetadata,進而就能夠知道 FullMetadata 剩下的數據成員。

    咱們能夠在 include/swift/ABI/Metadata.h 中找到上述這個類型的定義:

    template <typename Runtime>
    struct TargetMetadata {
      using StoredPointer = typename Runtime::StoredPointer;
      
      ...
      
    private:
      /// The kind. Only valid for non-class metadata; getKind() must be used to get
      /// the kind value.
      StoredPointer Kind;
      
      ...
    }
    複製代碼

    咱們不可貴出 FullMetadata<HeapMetadata> 的第三個數據成員是:

    StoredPointer Kind;
    複製代碼

那麼 TargetPointerStoredPointer 又是什麼呢?

include/swift/ABI/Metadata.h 中咱們能夠找到 TargetPointer 的定義:

template <typename Runtime, typename T>
using TargetPointer = typename Runtime::template Pointer<T>;
複製代碼

咱們看到 TargetPointer 最後實際上會使用模板參數 Runtime 內的 Poitner 這個類型。因此「尋找 TargetPointer 的定義」這個任務如今轉化爲了尋找模板參數 Runtime 的特化目標類型內的 Poitner 這個類型的定義。

而咱們在 InProcess——也就是上述類型的模板參數 Runtime 的特化目標中就能夠找到 StoredPointerPointer 的定義:

struct InProcess {
  using StoredPointer = uintptr_t;
  
  ...
  
  template <typename T>
  using Pointer = T*;
  
  ...
}
複製代碼

咱們將上述 PointerStoredPointer 定義代入以後,能夠獲得以下結構:

struct FullMetadata<HeapMetadata> {
  HeapObjectDestroyer * destroy;
  const ValueWitnessTable * ValueWitnesses;
  uintptr_t Kind;
}
複製代碼

最後在 include/swift/ABI/Metadata.h 中找到 HeapObjectDestroyer 的定義:

using HeapObjectDestroyer =
  SWIFT_CC(swift) void(SWIFT_CONTEXT HeapObject *);
複製代碼

咱們能夠看到 destroy 其實就是一個指向 SWIFT_CC(swift) void (*)(SWIFT_CONTEXT HeapObject *) 函數指針的一個成員。其中,SWIFT_CCSWIFT_CONTEXT 這兩個宏的內容在 include/swift/Runtime/Config.h 內,爲編譯器提供 Swift 專屬的調用規制(calling convention,我也不知道這個字怎麼譯,本身想的譯法)方面的標識符。因此最終這個函數指針會指向一個 Swift 函數。

HeapObjectDestroyer 的定義代入 FullMetadata<HeapMetadata> 後咱們不可貴出下圖:

3. FullMetadata 的成員 destroy 是什麼?

如上圖所示:destroy 是一個指向 void (*)(HeapObject *) 函數指針的一個成員,而這個函數是一個 Swift 函數。

4. 由 metadata 向低地址空間位移一個 T::HeaderType 長度的位置存放的是什麼?

咱們能夠看到,metadata 首先被扮演成了 T::HeaderType *。在這裏咱們代入特化後的結果既是 HeapMetadata::HeaderType。而後從上圖得知,HeapMetadata::HeaderType 的長度是兩個指針的長度,那麼 metadata 向低地址空間位移一個 HeapMetadata::HeaderType 長度後實際上會跑到 destroy 這個成員的地址上。最後咱們將指針扮演成 FullMetadata<HeapMetadata> *,那麼咱們將以 FullMetadata<HeapMetadata> 來觀察這個指針背後的內容。

在這裏我能夠畫圖來進行直觀的說明:

同時,咱們也證實了 swift_release 的形式參數是 HeapObject * 在這裏只是一種編程技巧——其爲該指針背後所指向的內存提供一個在 C/C++ 中訪問的途徑。

可是 HeapObject 中的 metadata 又是怎麼來的?metadata 指向的內存空間後兩個指針長度爲何會是這個類的 destroy 函數?從邏輯上說,要回答這個問題,更好的方法是考察 ListNode 實例的分配過程,而不是釋放過程。

追蹤 ListNode 實例的內存分配過程

咱們再次看到咱們以前生成的 SIL 中的一部份內容:

// ListNode.init(_:)
sil hidden @$s9CallStack8ListNodeCyACSicfc : $@convention(method) (Int, @owned ListNode) -> @owned ListNode {
  ...
} // end sil function '$s9CallStack8ListNodeCyACSicfc'
複製代碼

是的,咱們第一時間會想到要在 init(_:) 函數對應的 SIL 函數中去尋找線索。可是很惋惜,這裏面沒有和 metadata 相關的內容。可是當咱們搜索init(_:) 函數在 SIL 中對應的名字:$s9CallStack8ListNodeCyACSicfc 時,咱們會發現有一個叫 $s9CallStack8ListNodeCyACSicfC 的 SIL 函數(對應 Swift 函數名:ListNode.__allocating_init(_:))調用了 init(_:) 函數。

// ListNode.__allocating_init(_:)
sil hidden @$s9CallStack8ListNodeCyACSicfC : $@convention(method) (Int, @thick ListNode.Type) -> @owned ListNode {
// %0                                             // user: %4
bb0(%0 : $Int, %1 : $@thick ListNode.Type):
  %2 = alloc_ref $ListNode                        // user: %4
  // function_ref ListNode.init(_:)
  %3 = function_ref @$s9CallStack8ListNodeCyACSicfc : $@convention(method) (Int, @owned ListNode) -> @owned ListNode // user: %4
  %4 = apply %3(%0, %2) : $@convention(method) (Int, @owned ListNode) -> @owned ListNode // user: %5
  return %4 : $ListNode                           // id: %5
} // end sil function '$s9CallStack8ListNodeCyACSicfC'
複製代碼

注意到上述代碼的第 5 行,咱們不難推斷出: ListNode.__allocating_init(_:) 這個函數應該是使用了 alloc_ref 這條 SIL 指令來完成對 ListNode 實例的內存分配工做的。可是在這裏,整段代碼依然沒有 metadata 的任何線索。

調查 __allocating_init:LLVM IR 視角

這時候,咱們不妨將視野下降一個層級:咱們直接考察 LLVM IR,看看裏面會有什麼內容。首先使用 swiftc -emit-ir CallStack.swift > CallStack.swift.ll 來生成 CallStack.swift 的 LLVM IR。咱們能夠經過搜索 $s9CallStack8ListNodeCyACSicfC 在第 232 行找到 ListNode.__allocating_init(_:) 函數在 LLVM IR 中對應的函數:

define hidden swiftcc %T9CallStack8ListNodeC* @"$s9CallStack8ListNodeCyACSicfC"(i64, %swift.type* swiftself) #0 {
entry:
  %2 = call swiftcc %swift.metadata_response @"$s9CallStack8ListNodeCMa"(i64 0) #7
  %3 = extractvalue %swift.metadata_response %2, 0
  %4 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* %3, i64 32, i64 7) #2
  %5 = bitcast %swift.refcounted* %4 to %T9CallStack8ListNodeC*
  %6 = call swiftcc %T9CallStack8ListNodeC* @"$s9CallStack8ListNodeCyACSicfc"(i64 %0, %T9CallStack8ListNodeC* swiftself %5)
  ret %T9CallStack8ListNodeC* %6
}
複製代碼

咱們能夠在這個 LLVM IR 函數中清晰地看到 metadata 這個字。那麼咱們來分析一下這段 LLVM IR 函數:

在這裏首先要普及一下 LLVM IR 中的一些基本知識:

  • LLVM IR 是一種擁有類型系統的編譯器中間表述
  • % 開頭的是本地變量
  • @ 開頭的是全局變量

首先第一句:

%2 = call swiftcc %swift.metadata_response @"$s9CallStack8ListNodeCMa"(i64 0)
複製代碼

經過閱讀 call 這條 LLVM IR 指令的文檔,咱們不可貴知這句的意思是經過使用 Swift 的調用規制(calling convention)來調用 LLVM 函數 $s9CallStack8ListNodeCMa,而且傳入一個值爲 0i64 類型(不難猜想就是 64 位整型),在得到了一個類型爲 %swift.metadata_response 的返回值後再賦值給 %2

那麼問題來了:$s9CallStack8ListNodeCMa 又是什麼?實際上,若是你在 profiling 的時候足夠仔細,你可能會發現分配 Swift 對象實例的調用棧裏面會出現 type metadata accessor 這種記錄(抱歉我做弊了)。在後面的探索中,咱們也將不難知道:$s9CallStack8ListNodeCMa 就是 type metadata accessor。

咱們能夠在咱們導出的這份 LLVM IR 的頭部找到 %swift.metadata_response 的定義:

%swift.type = type { i64 }
%swift.metadata_response = type { %swift.type*, i64 }
複製代碼

其中,type 關鍵字是 LLVM IR 中自定義 struct 類型的關鍵字。%swift.type 就是一個成員只有一個 64 位整型值的 struct,咱們能夠從後面的探索得知,這就是一個 Swift 類型對應的 Objective-C meta-class。%swift.type* 則表明一個指向 %swift.type 的指針。因而類型 %swift.metadata_response 改寫成 C 語言代碼其實是:

struct {
    struct {
        int64_t metaclass;
    } SwiftType;
    struct SwiftType * swiftType;
    int64_t foo;
} MetadataResponse;
複製代碼

而後第二句:

%3 = extractvalue %swift.metadata_response %2, 0
複製代碼

經過閱讀 extractvalue 這條 LLVM IR 指令的文檔,咱們不可貴知這句的意思就是以 %swift.metadata_response 類型的結構觀察本地變量 %2,將其中的第 0 個元素取出。因而咱們會實際獲得一個 %swift.type * 類型的值,而後賦值給 %3

接着第三句:

%4 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* %3, i64 32, i64 7) #2
複製代碼

這句的意思是調用 @swift_allocObject 函數,將本地變量 %3 的內容以 %swift.type* 的類型傳入第一個參數,將 32i64 的類型傳入第二個參數,將 7i64 的類型傳入第三個參數,最後將返回值以 %swift.refcounted* 類型賦值給本地變量 %4

你可能不知道 noalias 是什麼意思。其實理不理解 noalias 在這裏沒有什麼影響,noalias 在修飾返回值時表示:@swift_allocObject 函數的返回值不會在該函數的執行過程當中經過該函數的參數這個途徑被訪問。

接着第四句:

%5 = bitcast %swift.refcounted* %4 to %T9CallStack8ListNodeC*
複製代碼

這表示,將本地變量 %4 中的內容從 %swift.refcounted* 類型扮演(cast)成 %T9CallStack8ListNodeC* 類型(用 Swift 來表述就是:CallStack 模塊內的 ListNode 這個類的引用),而後賦值給本地變量 %5

接着第五句:

%6 = call swiftcc %T9CallStack8ListNodeC* @"$s9CallStack8ListNodeCyACSicfc"(i64 %0, %T9CallStack8ListNodeC* swiftself %5)
複製代碼

這句的意思是以 Swift 的調用規制調用 @"$s9CallStack8ListNodeCyACSicfc" 這個函數(也就是 ListNode.init(_:)),將 0i64 類型傳入第一個參數,將本地變量 %5 中的內容以 %T9CallStack8ListNodeC* 的類型(也就是 ListNode 的引用)傳入第二個參數,而且給出第二個參數是 self 參數(swiftself)的提示,而後將返回值以 %T9CallStack8ListNodeC*ListNode 的引用)類型賦值給本地變量 %6

最後第六句:

ret %T9CallStack8ListNodeC* %6
複製代碼

將本地變量 %6 中的內容以 %T9CallStack8ListNodeC*ListNode 的引用)類型返回。

綜上,咱們能夠將以上 LLVM IR 改寫成如下 C-like 的語言:

typealias %swift.type * SwiftTypeRef;
typealias %swift.refcounted * SwiftRefCountedRef;
typealias %swift.metadata_response MetadataResponse;
#define ListNodeTypeMetadataAccessor $s9CallStack8ListNodeCMa

ListNode * ListNode__allocating_init(int64_t arg1, SwiftTypeRef self) {
    MetadataResponse %2 = ListNodeTypeMetadataAccessor(0);
    SwiftTypeRef %3 = %2.swiftType;
    SwiftRefCountedRef %4 = swift_allocObject(%3, 32, 7);
    ListNode * %5 = (ListNode *) %4;
    ListNode * %6 = ListNodeInit(0, %5);
    return %6;
}
複製代碼

因而咱們能夠看到,ListNode.__allocating_init(_:) 確實經過調用 type metadata accessor(既$s9CallStack8ListNodeCMa 這個函數)訪問了 ListNode 的 metadata,而且以此分配了 ListNode 實例的內存空間。

調查 Type Metadata Accessor:LLVM IR 視角

接下來咱們再來考察 ListNode 的 type metadata accessor(也就是 $s9CallStack8ListNodeCMa 這個函數),看看 Swift 究竟是如何在運行時獲取一個 class 類型的 metadata 的。搜索 $s9CallStack8ListNodeCMa 咱們能夠在第 242 行找到這個函數的定義:

; Function Attrs: nounwind readnone
define hidden swiftcc %swift.metadata_response @"$s9CallStack8ListNodeCMa"(i64) #4 {
entry:
  %1 = load %swift.type*, %swift.type** @"$s9CallStack8ListNodeCML", align 8
  %2 = icmp eq %swift.type* %1, null
  br i1 %2, label %cacheIsNull, label %cont

cacheIsNull:                                      ; preds = %entry
  %3 = call %objc_class* @swift_getInitializedObjCClass(%objc_class* bitcast (i64* getelementptr inbounds (<{ void (%T9CallStack8ListNodeC*)*, i8**, i64, %objc_class*, %swift.opaque*, %swift.opaque*, i64, i32, i32, i32, i16, i16, i32, i32, <{ i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor }>*, i8*, i64, i64, i64 (%T9CallStack8ListNodeC*)*, void (i64, %T9CallStack8ListNodeC*)*, { i8*, %TSi* } (i8*, %T9CallStack8ListNodeC*)*, i64 (%T9CallStack8ListNodeC*)*, void (i64, %T9CallStack8ListNodeC*)*, { i8*, %T9CallStack8ListNodeCSg* } (i8*, %T9CallStack8ListNodeC*)*, %T9CallStack8ListNodeC* (i64, %swift.type*)* }>, <{ void (%T9CallStack8ListNodeC*)*, i8**, i64, %objc_class*, %swift.opaque*, %swift.opaque*, i64, i32, i32, i32, i16, i16, i32, i32, <{ i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor }>*, i8*, i64, i64, i64 (%T9CallStack8ListNodeC*)*, void (i64, %T9CallStack8ListNodeC*)*, { i8*, %TSi* } (i8*, %T9CallStack8ListNodeC*)*, i64 (%T9CallStack8ListNodeC*)*, void (i64, %T9CallStack8ListNodeC*)*, { i8*, %T9CallStack8ListNodeCSg* } (i8*, %T9CallStack8ListNodeC*)*, %T9CallStack8ListNodeC* (i64, %swift.type*)* }>* @"$s9CallStack8ListNodeCMf", i32 0, i32 2) to %objc_class*))
  %4 = bitcast %objc_class* %3 to %swift.type*
  store atomic %swift.type* %4, %swift.type** @"$s9CallStack8ListNodeCML" release, align 8
  br label %cont

cont:                                             ; preds = %cacheIsNull, %entry
  %5 = phi %swift.type* [ %1, %entry ], [ %4, %cacheIsNull ]
  %6 = insertvalue %swift.metadata_response undef, %swift.type* %5, 0
  %7 = insertvalue %swift.metadata_response %6, i64 0, 1
  ret %swift.metadata_response %7
}
複製代碼

這個函數牽扯到 SSA Form phi nodes 的還原,很差作句讀,有興趣能夠直接在這裏獲取免費正版的 SSA Book 學習一下 SSA Form 相關的知識。這裏我直接展現其改寫成 C-like 僞代碼後的樣子:

typealias %swift.type * SwiftTypeRef;
typealias %swift.metadata_response MetadataResponse;
#define ListNodeTypeMetadataAccessor $s9CallStack8ListNodeCMa
#define listNodeMetadataRecord $s9CallStack8ListNodeCMf
static SwiftTypeRef * ListNodeSwiftType = $s9CallStack8ListNodeCML;

MetadataResponse ListNodeTypeMetadataAccessor(i64 arg1) {
    SwiftTypeRef swiftType;
    
    if (*ListNodeSwiftType == NULL) {
        swiftType = (SwiftTypeRef)swift_getInitializedObjCClass(&(listNodeMetadataRecord -> objc_class));
        * ListNodeSwiftType = swiftType
    } else {
        swiftType = * ListNodeSwiftType
    }
    
    MetadataResponse metadataResponse = {
        swiftType,
        0,
    };
    
    return metadataResponse;
}
複製代碼

咱們能夠看到:

  • 首先這個函數將檢查全局變量 ListNodeSwiftType(也就是 $s9CallStack8ListNodeCML 這個符號)的內容:
    • 若是爲 0,那麼就會將 listNodeMetadataRecord -> objc_class 傳入 swift_getInitializedObjCClass 來得到 ListNodeswiftType
    • 若是不是 0,那麼會直接使用 ListNodeSwiftType 做爲 ListNodeswiftType
  • 以後再將 ListNodeswiftType 構形成 metadataResponse 而且返回。

要判斷上述代碼中,咱們會執行哪一個條件分支,咱們必須首先知道 ListNodeSwiftType也就是 $s9CallStack8ListNodeCML 的內容。在咱們導出的 LLVM IR 文件的第 37 行咱們能夠找到以下內容:

@"$s9CallStack8ListNodeCML" = internal global %swift.type* null, align 8
複製代碼

因此咱們知道了,對於咱們編譯的 ListNode 這段代碼,$s9CallStack8ListNodeCML 也就是 ListNodeTypeMetadata 會是 null(也就是 0)。同時咱們也能夠在 Hopper 中搜索 $s9CallStack8ListNodeCML 來進行求證。

$s9CallStack8ListNodeCML

能夠看到 $s9CallStack8ListNodeCML 確實是 0。也就是說在咱們的代碼的運行過程當中,ListNode 的 type metadata accessor 會調用 swift_getInitializedObjCClass 來幫助生成 ListNode 類型的 metadata 記錄。

在上面這段 LLVM IR 中,swift_getInitializedObjCClass(&(listNodeMetadataRecord -> objc_class)) 這句所對應的 LLVM IR 通常被認爲是大部分 LLVM 學習者都容易搞混的點。我這裏將這句的 LLVM IR 抽出來單獨講一下:

%3 = call %objc_class* @swift_getInitializedObjCClass(%objc_class* bitcast (i64* getelementptr inbounds (<{ void (%T9CallStack8ListNodeC*)*, i8**, i64, %objc_class*, %swift.opaque*, %swift.opaque*, i64, i32, i32, i32, i16, i16, i32, i32, <{ i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor }>*, i8*, i64, i64, i64 (%T9CallStack8ListNodeC*)*, void (i64, %T9CallStack8ListNodeC*)*, { i8*, %TSi* } (i8*, %T9CallStack8ListNodeC*)*, i64 (%T9CallStack8ListNodeC*)*, void (i64, %T9CallStack8ListNodeC*)*, { i8*, %T9CallStack8ListNodeCSg* } (i8*, %T9CallStack8ListNodeC*)*, %T9CallStack8ListNodeC* (i64, %swift.type*)* }>, <{ void (%T9CallStack8ListNodeC*)*, i8**, i64, %objc_class*, %swift.opaque*, %swift.opaque*, i64, i32, i32, i32, i16, i16, i32, i32, <{ i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor, %swift.method_descriptor }>*, i8*, i64, i64, i64 (%T9CallStack8ListNodeC*)*, void (i64, %T9CallStack8ListNodeC*)*, { i8*, %TSi* } (i8*, %T9CallStack8ListNodeC*)*, i64 (%T9CallStack8ListNodeC*)*, void (i64, %T9CallStack8ListNodeC*)*, { i8*, %T9CallStack8ListNodeCSg* } (i8*, %T9CallStack8ListNodeC*)*, %T9CallStack8ListNodeC* (i64, %swift.type*)* }>* @"$s9CallStack8ListNodeCMf", i32 0, i32 2) to %objc_class*))
複製代碼

咱們能夠看到,這一行實在太長。咱們能夠對其作一下美化:咱們將內聯在 LLVM IR 指令中的類型結構抽出命名爲 %SwiftTypeMetadata,而且將這句 LLVM IR 分解爲多句,因而可得:

%0 = i64* getelementptr inbounds (%SwiftTypeMetadata, %SwiftTypeMetadata * @"$s9CallStack8ListNodeCMf", i32 0, i32 2)
%1 = %objc_class* bitcast (%objc_class* %0 to %objc_class*)
%3 = call %objc_class* @swift_getInitializedObjCClass(%objc_class* %1)
複製代碼

這下咱們能夠很清晰地進行解釋了:

  • 首先咱們要知道,getelementptr 這個指令是用來得到從某一個基地址開始以後某一個元素的地址的。不少 LLVM 學習者都會把這條指令理解成「獲取某一個基地址開始以後的某一個元素」,這是錯的。

    在這裏,這條指令以 $s9CallStack8ListNodeCMf 爲基礎地址,先以 %SwiftTypeMetadata * 類型審視這個地址,移動到從 0 數起的第 0 個元素(i32 0 的做用)後,再以 %SwiftTypeMetadata 類型審視當前地址,移動到從 0 數起的第 2 個元素(i32 2 的做用),最後將通過兩次移動以後的地址賦值給 %0

    因而咱們這裏實際上得到了一個指向 $s9CallStack8ListNodeCMf 第二個地址的指針;

  • 以後咱們再用 bitcast%0 扮演成 %objc_class* 再賦值給 %1

  • 而後調用 @swift_getInitializedObjCClass,並將 %1%objc_class* 做爲參數傳入。

那麼問題來了,%SwiftTypeMetadata 這個類型究竟是什麼樣子的?由於上述 LLVM IR 中咱們以 %SwiftTypeMetadata 來審視了 $s9CallStack8ListNodeCMf 這個符號,因此咱們不妨看看 $s9CallStack8ListNodeCMf 的內容是什麼。在咱們生成的 LLVM IR 的第 38 行能夠看見 $s9CallStack8ListNodeCMf 的內容。爲了方便你們觀察,這裏我將這個結構體的內容畫了出來:

咱們又知道,咱們將指向上圖所示字段中從 0 開始數第 2 個字段的指針扮演成了 %objc_class*(一個指向 Objective-C class 的指針),那麼咱們不妨用 Objective-C class 的結構體來看看 $s9CallStack8ListNodeCMf 的內容:

能夠看見,這個結構體中確實隱含着一個 Objective-C class 結構體。

實際上,若是你記性好,那麼你還會發現,這個結構體的第一個成員就是咱們的 ListNode.__deallocating_deinit 函數——也就是摧毀(destroy)函數。根據咱們前面對 Swift 運行時中 _swift_dealloc_release 函數的研究,咱們不難產生直覺——莫非這個結構體從 0 開始數第 2 個成員就是 HeapObjectmetadata 指針指向的目標?

目前咱們還不能確定,由於代碼尚未研究完,咱們還不知道後面會發生什麼。

接下來咱們必須知道 swift_getInitializedObjCClass 這個函數作了什麼。

搜索咱們導出的 LLVM IR 文件,咱們能夠發現 swift_getInitializedObjCClass 是一個只有聲明(declare)沒有定義(define)的文件。這說明這個函數將在編譯時被連接到目標文件,也就是說,這是一個存在於 Swift 標準庫中的函數。

調查 swift_getInitializedObjCClass

因而咱們能夠在 Swift 源代碼中搜索 swift_getInitializedObjCClass,並在 stdlib/public/runtime/SwiftObject.mm 文件中發現以下內容:

Class swift::swift_getInitializedObjCClass(Class c) {
  // Used when we have class metadata and we want to ensure a class has been
  // initialized by the Objective-C runtime. We need to do this because the
  // class "c" might be valid metadata, but it hasn't been initialized yet.
  return [c class];
}
複製代碼

是的,這個函數僅僅向傳入的參數 c 發送了 [Class +class] 消息,根據 Objective-C 運行時的知識,這將確保 c 在 Objective-C 運行時中被初始化(既 [Class +initialize] 被調用)。而後這個函數會返回 [c class]——既 c 自己。因而咱們能夠看見,這個函數就如其名字同樣——僅僅只是保證這個 Objective-C class 被初始化而已。

Type Metadata Accessor 小結

咱們能夠看到 swift_getInitializedObjCClass 在這裏並無起到什麼偷天換日的做用,因此咱們以前的直覺是對的:HeapObjectmetadata 指針就是指向 $s9CallStack8ListNodeCMf 開始的結構體上從 0 開始數第 2 個元素的。

咱們能夠畫圖作一下說明:

Type Metadata 生成

能夠 type metadata 又是怎麼生成的呢?咱們能夠看到 lib/IRGen/GenMeta.cpp 文件中的 SingletonClassMetadataBuilder 這個 C++ 類。這個類將經過使用 LLVM 的 API 來爲 ListNode 生成 type metadata 的 LLVM IR。

class SingletonClassMetadataBuilder :
    public ClassMetadataBuilderBase<SingletonClassMetadataBuilder> {
    
    ...
}
複製代碼

你會發現這份文件中還有其餘的 class metadata builder,實際上,根據 class 是否套用 @_fixed_layout 屬性,是不是模塊外定義的,是不是泛型的這三點,Swift 會有不一樣的 type metadata 生成策略。這裏我尚未時間一一研究。

咱們能夠看到其繼承自 ClassMetadataBuilderBase<SingletonClassMetadataBuilder>,而咱們又能夠在同一個文件內找到 ClassMetadataBuilderBase 的定義:

template<class Impl>
class ClassMetadataBuilderBase : public ClassMetadataVisitor<Impl> {
  ...
}
複製代碼

不難看出,ClassMetadataBuilderBase<Impl> 繼承自 ClassMetadataVisitor<Impl>,而 ClassMetadataVisitor 就是很是經典的 visitor 模式的應用了。

咱們看到 ClassMetadataVisitorlayout 函數,這就是具體生成 type metadata 的地方。

template <class Impl> class ClassMetadataVisitor : public NominalMetadataVisitor<Impl>,
      public SILVTableVisitor<Impl> {
public:
  void layout() {
    // HeapMetadata header.
    asImpl().addDestructorFunction();

    // Metadata header.
    super::layout();

    asImpl().addSuperclass();
    asImpl().addClassCacheData();
    asImpl().addClassDataPointer();

    asImpl().addClassFlags();
    asImpl().addInstanceAddressPoint();
    asImpl().addInstanceSize();
    asImpl().addInstanceAlignMask();
    asImpl().addRuntimeReservedBits();
    asImpl().addClassSize();
    asImpl().addClassAddressPoint();
    asImpl().addNominalTypeDescriptor();
    asImpl().addIVarDestroyer();

    // Class members.
    addClassMembers(Target);
  }
}
複製代碼

其中 super::layout() 調用的是 NominalMetadataVisitor<Impl>layout 函數,咱們在這裏也將其貼出來:

template <class Impl> class NominalMetadataVisitor {
public:
  void layout() {
    // Common fields.
    asImpl().addValueWitnessTable();
    asImpl().noteAddressPoint();
    asImpl().addMetadataFlags();
  }
}
複製代碼

layout 函數體內的函數們逐一展開後,咱們就能夠將以前得到的 ListNode 的 type metadata 中全部的字段都對上號了:

通常性非泛型非 @_fixed_layout 的 Swift 對象內存佈局及 type metadata 佈局

同時,在這裏咱們也能夠給出 Swift 中通常非泛型,非 @_fixed_layout 類的內存佈局及相應的 type metadata 內存佈局:

思考題:你能說明一下 Swift 爲何要這樣設計嗎?

超長鏈表爆棧緣由小結

至此,咱們能夠總結出,對於使用 ListNode 這種構型的鏈表,其被釋放的過程當中:

  1. 根節點首先進入釋放過程;
  2. 根節點的 __deallocating_deinit 又調用了根節點的 deinit 函數;
  3. 根節點的 deinit 又調用了一個由編譯器自動生成的外聯的 ListNode 成員摧毀過程;
  4. 編譯器自動生成的外聯的 ListNode 成員摧毀過程又調用了 swift_release 來解除對 next 節點的強引用;
  5. swift_release 調用 _swift_release_ 來解除對 next 節點的強引用;
  6. 由於這裏只有一處對這個 next 節點進行強引用且沒有 unowned 引用,因此 _swift_release_ 最終會經過 _swift_release_dealloc 來解除對 next 節點強引用
  7. _swift_release_dealloc 經過調用 ListNode__deallocating_deinit 函數來摧毀 next 節點;
  8. next 節點引用計數變成 0,進入釋放過程;

咱們能夠經過查看徹底的調用棧來查證上述表述是否是真的。

爲了查看徹底的調用棧而不是 Debug Navigator 中那點調用棧摘要,咱們要點擊 Xcode 編輯區左上角「四個方格」的圖標,而後點擊 Disassembly,再點擊當前棧幀 ListNode.deinit 打開所有調用棧。最後咱們能夠看到下圖:

調用棧

咱們能夠注意到,上圖中沒有 swift_release_swift_release_ 兩個函數的過程活動記錄(procedure activation record,能夠粗淺理解爲調用棧和 CPU 上寄存器中的記錄)。這是由於編譯器對這兩個函數作了尾部調用優化(TCO),將 call 系列指令(考慮到 32 位和 64 位平臺上的完成相同功能的指令名字並不相同,我這裏及如下都將稱之爲「系列指令」)改爲了 jmp 系列指令,這樣後繼的 _swift_release__swift_release_dealloc 函數就能夠複用起始函數 swift_release 的調用棧了,而咱們觀察到的就是複用了以後的調用棧。

咱們能夠在 Xcode 中打下 symbolic breakpoints 來求證。

按下 command + 8 將 Navigator 面板切換到 Breakpoint Navigator。按下面板左下角的 "+" 按鈕新增兩個 symbolic breakpoints:swift_release_swift_release_

要注意,在節點生成過程當中也會觸發引用計數,因此這個時候 swift_release 也會被調用,因此咱們首先要關掉這兩個 breakpoints:

而後在 print("Bar") 這個地方打上 breakpoint:

再次運行程序,待程序運行到 print("Bar") 在 breakpoint 除暫停以後打開 swift_relase_swift_release_ 的斷點再繼續。以後咱們將看到程序將在 swift_relase_swift_release_ 的入口處停止。

咱們能夠看到 swift_relase_swift_release_ 的調用以及 _swift_release__swift_release_dealloc 的調用所有都是由 jmp 系列指令完成的。這就是尾部調用優化(TCO)在指令集這個微觀層面的體現。

libswiftCore.dylib`swift_release:
    ....
    0x7fff65730905 <+5>:  jmpq   *0x3351ac9d(%rip)         ; _swift_release
    ....
複製代碼
libswiftCore.dylib`_swift_release_:
    ...
    0x7fff65730ce1 <+145>: jmp    0x7fff65731a50            ; _swift_release_dealloc
    ...
複製代碼

因而咱們經過觀察函數的過程活動記錄的方法證實了上述咱們對 ListNode 的釋放過程的描述是正確的。因而咱們能夠知道:對於以 ListNode 這種方式實現的鏈表的根節點被徹底銷燬以前,其後繼節點就會被釋放,而後由於再也沒有對其後繼節點進行強引用的地方了,因而這個後繼節點也進入到了一個自動的銷燬過程。這種銷燬就像核裂變同樣,是鏈式反應的,而一旦這個「反應鏈」很長——既鏈表自己很長的話就會引發爆棧。固然,爆棧的結果就是應用崩潰。

另外,咱們能夠觀察到,編譯器並無對這個間接遞歸過程進行尾遞歸優化。

不會爆棧的鏈表的實現方法

那麼用什麼方法能夠實現永遠不會在釋放時爆棧的鏈表呢?

方法 1: 釋放時人工處理

咱們能夠在 ListNode 釋放時考慮對 next 節點進行逆向釋放,這至關於一道比較多見的面試題:反轉鏈表。因而咱們能夠寫出以下代碼:

class ListNode {
    var val: Int
    var next: ListNode?
    
    init(_ val: Int) {
        self.val = val
        self.next = nil
    }
    
    deinit {
        var currentOrNil: ListNode? = self
        var nodes = [ListNode]()
        
        while let current = currentOrNil {
            nodes.append(current)
            currentOrNil = current.next
        }
        
        for each in nodes.reversed() {
            each.next = nil
        }
    }
}
複製代碼

這樣,咱們將一個遞歸轉換成循環就能夠避免 ListNode 釋放時後繼節點連鎖釋放所帶來的爆棧。我本身寫的一個玩具級 LRUCache (以前據說某廠面試必考 LRU cache 因此寫了個練手)中所使用的雙向鏈表的 deinit 就使用了這種方法。

然而,使用這種方法構建的鏈表在每次釋放時都要將全部鏈表節點插入一個動態數組,雖然動態數組插入的複雜度是均攤 O(1) 的,可是這依然要耗費 O(n) 的輔助空間,在時間上也會多 O(n) 的開銷(這也是我爲何說我寫的那個 LRU cache 是玩具級的緣由)。有沒有什麼辦法能夠把這些消除掉?

方法 2: 對鏈表節點進行 Pooling

考慮到鏈表是一種將內部結構特徵(前驅節點或者後繼節點)暴露在使用者面前的數據結構,這使得咱們在使用鏈表的同時也不得不考慮如何去維護這個鏈表的內部結構,從而給咱們帶來了不小的智力負擔。因而咱們能夠將其封裝在一個類型內部,僅僅讓這個類型能夠對鏈表進行操做,而後再對這個類型封裝出一些數據集合類型所使用的功能,這樣就能夠減輕使用鏈表時的智力負擔。

關於以上這點請看星際爭霸 I 的開發者撰寫的在星際爭霸 I 的開發過程當中與鏈表鬥智鬥勇的系列文章:

I. Tough Times on the Road to Starcraft

II. Avoiding Game Crashes Related to Linked Lists

III. Explaining the Implementation Details of The Fix(抱歉,第三篇他鴿了七年了)

這樣以來,咱們就能夠在這個類型內部套用 pooling 模式了。Pooling 是一個在計算密集型領域(如遊戲客戶端、大型軟件客戶端和大型服務器端中)常見的設計模式,其在 iOS 平臺上的研發中並不常見。甚至能夠說,從 Objective-C 2.0 無效化 [NSObject +allocWithZone:] 這個 API 來看,蘋果是並不鼓勵開發者在 Objective-C 中使用 pooling 模式的——你要 pooling 那就只能用 C++ 來進行 pooling。Pooling 模式的具體作法是:預先分配一個內存池而後在要建立對象時直接從這個內存池中劃分出一部分給這個對象便可——這樣咱們就能夠避免系統的內存分配函數須要訪問操做系統中關於資源調度安排的信息這個開銷,進一步提升計算性能。固然,在面對解決 ListNode 構型的鏈表過長而致使釋放時崩潰這個問題時,這些都只是 bonus 而不是咱們使用 pooling 的根本目的。

在這裏咱們使用 pooling 模式的根本目的在於:若是咱們給每個要使用鏈表的封裝類型的實例都建立一個內存池,那麼在釋放鏈表時咱們只須要釋放這個內存池就能夠了。

剛剛說了,蘋果彷佛並不鼓勵開發者在 Objective-C 中使用 pooling 模式,其實在 Swift 中也沒法簡單套用 pooling 模式——由於 class 實例的內存分配過程是由 Swift 運行時掌控的。咱們能夠在 stdlib/public/runtime/Heap.cpp 中找到下列函數:

void *swift::swift_slowAlloc(size_t size, size_t alignMask) {
  void *p;
  // This check also forces "default" alignment to use AlignedAlloc.
  if (alignMask <= MALLOC_ALIGN_MASK) {
    p = malloc(size);
  } else {
    size_t alignment = (alignMask == ~(size_t(0)))
                           ? _swift_MinAllocationAlignment
                           : alignMask + 1;
    p = AlignedAlloc(size, alignment);
  }
  if (!p) swift::crash("Could not allocate memory.");
  return p;
}
複製代碼

這就是 Swift 運行時中進行 heap 內存分配的函數,也是最終爲 class 實例分配內存空間的函數,咱們能夠看見其當前實現是利用 C 標準庫的 malloc 進行內存分配的,另一個 AlignedAlloc 在 Darwin 下也會使用 posix_memalign 來進行內存分配,這個過程很難有任何開發者的參與。爲了使用 pooling 模式,咱們只能使用 Swift 標準庫中的 UnsafeMutablePointer 配合 struct 來實現一個內存池。

固然,你也能夠嘗試更「野」(野蠻、暴力之意)的路子:用 C 建立好內存池,保證好內存池中每個實例和 Objective-C 的內存模型一致後再用 CF 對象 bridge 到 Swift 的方法將內存池中的內容 bridge 過來。這樣有一個好處就是你能夠獲得 class 實例而不用裸操做原始指針了。

若是你對這個方法感興趣,能夠參考這篇日本 Qiita 社區(也是一個開發者社區)上的文章:

Swift の Toll-Free Bridge の実裝を読む

總體設計思路以下圖:

如圖所示,_ListStorage 內的維護了一個內存池 buffer、一個頭部節點指針 headOffset(表明使用節點鏈)和一個重用節點指針 reuseHeadOffset(表明重用節點鏈)。在這個鏈表實現中,由於全部節點都在內存池中,咱們可使用節點在內存池中的偏移量來記錄 next 節點的位置。

  • 初始化時,頭部節點指針爲空(-1),重用節點指針指向內存池的第一個元素,內存池內的每個單元的 next 指針都指向下一個單元,最後一個單元的 next 指針爲空(-1),此時內存池上每個單元都在重用節點鏈上。
  • 加入時,從重用節點鏈上拿下頭部節點(若是沒有就對內存池進行擴容),插入到使用節點鏈的頭部節點。
  • 刪除時,將使用節點鏈的頭部節點拿下,而且插入到重用節點鏈的頭部節點。

由於咱們老是分配相同大小的單元,因此咱們不須要像 malloc 那樣在分配的時候留一個小空間來記錄本次分配的空間的大小,因此這個內存池的實現會變得很是簡單。

我將這個實現的完整代碼放在了 GitHub 上,這裏我挑幾個實現中的重點說說:

  • 我在 buffer 內的單元(代碼中對應 bucket 這個概念)中並無直接使用指針來表示後繼節點,而是使用了節點在內存池中的偏移量來做爲指針值。這麼作有一點好處:咱們在應用寫入時複製(COW)時,整個容器僅僅須要簡單複製就能夠了。否則的話,咱們就要一個指針一個指針來進行釐清了。

  • 內存池的增加因子我設置成了 1.5

    let newCapacity = max(1, _capacity + ((_capacity + 1) >> 1))
    複製代碼

    由於當咱們使用 1.5 的增加因子時,操做系統將有可能有機會重複利用以前分配過的空間。這樣能夠起到減小內存碎片的效果。

    舉例以下:

    第一次分配內存: 1  2
    
    第二次分配內存: .  .  1  2  3
    
    第三次分配內存: .  .  .  .  .  1  2  3  4  5
    
    第四次分配內存:.  .  .  .  .  .  .  .  .  .  1  2  3  4  5  6  7  8
    
    第五次分配內存:.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  . 1 2 3 4 5 6 7 8 9 A B C
    
    第六次分配內存:1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  11 12
    複製代碼

    咱們能夠看到,在第六次分配內存的時候,操做系統存在一個重複利用以前分配過的空間的機會。

    Swift 中 ArrayContiguousArray 的增加因子則是 2。咱們能夠看到 stdlib/public/core/ArrayShared.swift 這個文件中有以下內容:

    @inlinable
    internal func _growArrayCapacity(_ capacity: Int) -> Int {
        return capacity * 2
    }
    複製代碼

    這句就控制了 Swift ArrayContiguousArray 的擴容因子。因此對於 Swift 的 ArrayContiguousArray而言,操做系統將沒有這種重複利用以前分配過的空間的機會。

    在實際工程中,騰訊 IEG 出品的 RapidJSON 和 Facebook 出品的 FBFolly 都是使用的 1.5 爲動態數組類型的容器的增加因子。

    Clang 和 GCC 的 C++ STL 中的 std::vector 增加因子也是 2。

  • 咱們能夠經過讓咱們的鏈表類型聽從於 SequenceCollection 以讓咱們能更加方便的在 Swift 中使用這個類型(我在範例代碼中已經這麼作了)。

    可是要注意的是,由於 Collection 協議自帶 Index,而對鏈表使用索引訪問的時間複雜度是 O(n) 的,加上 Collection 在實現了 subscript 的 getter 以後就會自動實現一個使用這個 subscriptiterator,因此若是這時候咱們經過 Swift 中通常遍歷 Sequence 和 Collection 的方法(如 for-in 循環)來遍歷鏈表,那麼這個性能將會很是差(時間複雜度升至 O(n^2))。

    因此,你應該本身實現一個 iterator 來完成 O(n) 的遍歷時間複雜度(我在範例代碼中也已經這麼作了)。

  • 若是要用於生產環境,你應該還要讓這個鏈表類型聽從於 RangeReplaceableCollection。這個協議有一個叫 removeAll(keepingCapacity:) 的函數能夠跟咱們約定一個釋放內存池中無用空間的接口。

方法 3: 僅對鏈表節點的引用進行 Pooling

上述方法太麻煩了,還要本身造內存池(雖然這是個很簡單的特例)。有沒有什麼更簡單的方法?

仍是有的。

咱們能夠依然回到使用 class 來建立鏈表節點這個基本方法,而後選擇僅僅對鏈表節點的引用進行 pooling。這有點像 UITableView 內部對 reusable cells 進行重用的處理方法;Facebook 的 ComponentKit 也會在 view 層級的每個 view 內部創建一個 reuse pool 來對由 ComponentKit 控制的 subviews 進行重用。這些例子中都僅僅只對對象指針進行了 pooling 而不是對象總體。

總體設計思路以下圖:

如圖所示,_ListStorage 維護了一個有關鏈表節點的數組 nodes、頭節點在數組中的偏移量 headNodeIndex 可重用節點在數組中的索引。同時,在鏈表節點中咱們依然要使用數組內偏移量來記錄 next 節點在數組中的位置。這樣作在這個實現中的好處主要是能夠規避引用計數。

我也將這個實現的完整代碼放在了 GitHub 上。

一樣,對於這個鏈表實現,若是要用於生產環境,你應該還要讓這個鏈表類型聽從於 RangeReplaceableCollection。這個協議有一個叫 removeAll(keepingCapacity:) 的函數能夠跟咱們約定一個釋放數據結構中無用空間的接口。

同時我也在範例項目內作了一個簡易的 benchmark,方法二的性能明顯優於方法三。


本文說起資源索引

SSA Book

星際爭霸 I 開發者鏈表心得 I:I. Tough Times on the Road to Starcraft

星際爭霸 I 開發者鏈表心得 II:II. Avoiding Game Crashes Related to Linked Lists

我寫的不爆棧的鏈表範例代碼:LinkedListExamples

另外,查閱 Swift 源代碼我推薦使用 CLion(沒收推廣費)


本文使用 OpenCC 完成繁簡轉換。

相關文章
相關標籤/搜索