Unowned 仍是 Weak?生命週期和性能對比

做者:Umberto Raimondi,原文連接,原文日期:2016-10-27
譯者:shanks;校對:Crystal Sun;定稿:CMBhtml

每當處理循環引用(retain cycles)時,須要考量對象生命週期來選擇unowned或者weak標識符,這已經成爲了一個共識。可是有時仍然會心存疑問,在具體的使用中應該選擇哪個,或者退一步講,保守的只使用 weak 是否是一個好的選擇呢?前端

本文首先對循環引用的基礎知識作一個簡要介紹,而後會分析 Swift 源代碼的一些片斷,講解 unownedweak 在生命週期和性能上的差別點,但願看完本文之後,在的使用場景中,能使用正確的弱引用類型。git

目錄:github

GitHub 或者 zipped 獲取本文相關的 Playground 代碼。而後從這裏獲取閉包案例和 SIL,SILGen 以及 LLVM IR 的輸出。

基礎知識

衆所周知,Swift 利用古老而且有效的自動引用計數(ARC, Automatic Reference Counting)來管理內存,帶來的後果和在 Objective-C 中使用的狀況相似,須要手動使用弱引用來解決循環引用問題。

若是對 ARC 不瞭解,只須要知道的是,每個引用類型實例都有一個引用計數與之關聯,這個引用計數用來記錄這個對象實例正在被變量或常量引用的總次數。當引用計數變爲 0 時,實例將會被析構,實例佔有的內存和資源都將變得從新可用。

當有兩個實例經過某種形式互相引用時,就會造成循環引用(好比:兩個類實例都有一個屬性指向對方的類實例;雙向鏈表中兩個相鄰的節點實例等...), 因爲兩個實例的引用計數都一直大於 0, 循環引用將會阻止這些實例的析構。

爲了解決這個問題,和其餘一些有相似問題的語言同樣, 在 Swift 中,弱引用 的概念被提了出來,弱引用不會被 ARC 計算,也就是說,當一個弱引用指向一個引用類型實例時,引用計數不會增長。

弱引用不會阻止實例的析構, 只須要記住的是,在任何狀況下,弱引用都不會擁有它指向的對象。在正式的場景中不是什麼大問題,可是在咱們處理這類引用的時候,須要意識到這一點。

在 Swift 中有 2 種 引用形式,unownedweak

雖然它們的做用相似,但與它們相關實例生命週期的假設會略有不一樣,而且具備不一樣的性能特徵。

爲了舉例說明循環引用,這裏不使用你們指望看到的類之間的循環引用,而使用閉包的上下文案例,這在 Objective-C 平常開發中處理循環引用時常常會遇到的狀況。和類的循環引用相似,經過建立一個強引用指向外部實例,或捕獲它,阻止它析構。

Objective-C ,按照標準的作法,定義一個弱引用指向閉包外部的實例,而後在閉包內部定義強引用指向這個實例,在閉包執行期間使用它。固然,有必要在使用前檢查引用的有效性。

爲了更方便的處理循環引用,Swift 引入了一個新的概念,用於簡化和更加明顯地表達在閉包內部外部變量的捕獲:捕獲列表(capture list)。使用捕獲列表,能夠在函數的頭部定義和指定那些須要用在內部的外部變量,而且指定引用類型(譯者注:這裏是指 unownedweak)。

接下來舉一些例子,在各類狀況下捕獲變量的表現。

當不使用捕獲列表時,閉包將會建立一個外部變量的強引用:

var i1 = 1, i2 = 1

var fStrong = {
    i1 += 1
    i2 += 2
}

fStrong()
print(i1,i2) //Prints 2 and 3

閉包內部對變量的修改將會改變外部原始變量的值,這與預期是一致的。

使用捕獲列表,閉包內部會建立一個新的可用常量。若是沒有指定常量修飾符,閉包將會簡單地拷貝原始值到新的變量中,對於值類型和引用類型都是同樣的。

var fCopy = { [i1] in
    print(i1,i2)
}

fStrong()
print(i1,i2) //打印結果是 2 和 3  

fCopy()  //打印結果是 1 和 3

在上面的例子中,在調用 fStrong 以前定義函數 fCopy ,在該函數定義的時候,私有常量已經被建立了。正如你所看到的,當調用第二個函數時候,仍然打印 i1 的原始值。

對於外部引用類型的變量,在捕獲列表中指定 weakunowned,這個常量將會被初始化爲一個弱引用,指向原始值,這種指定的捕獲方式就是用來處理循環引用的方式。

class aClass{
    var value = 1
}

var c1 = aClass()
var c2 = aClass()

var fSpec = { [unowned c1, weak c2] in
    c1.value += 1
    if let c2 = c2 {
        c2.value += 1
    }
}

fSpec()
print(c1.value,c2.value) //Prints 2 and 2

兩個 aClass 捕獲實例的不一樣的定義方式,決定了它們在閉包中不一樣的使用方式。

unowned 引用使用的場景是,原始實例永遠不會爲 nil,閉包能夠直接使用它,而且直接定義爲顯式解包可選值。當原始實例被析構後,在閉包中使用這個捕獲值將致使崩潰。

若是捕獲原始實例在使用過程當中可能爲 nil ,必須將引用聲明爲 weak, 而且在使用以前驗證這個引用的有效性。

問題來了: unowened 仍是 weak?

在實際使用中如何選擇這兩種弱引用類型呢?

這個問題的答案能夠簡單由原始對象和引用它的閉包的生命週期來解釋

有兩個可能出現的場景:

  • 閉包和捕獲對象的生命週期相同,因此對象能夠被訪問,也就意味着閉包也能夠被訪問。外部對象和閉包有相同的生命週期(好比:對象和它的父對象的簡單返回引用)。在這種狀況下,你應該把引用定義爲 unowned

一個經典的案例是: [unowned self], 主要用在閉包中,這種閉包主要在他們的父節點上下文中作一些事情,沒有在其餘地方被引用或傳遞,不能做用在父節點以外。

  • 閉包的生命週期和捕獲對象的生命週期相互獨立,當對象不能再使用時,閉包依然可以被引用。這種狀況下,你應該把引用定義爲 weak,而且在使用它以前驗證一下它是否爲 nil(請不要對它進行強制解包).

一個經典的案例是: [weak delegate = self.delegate!],能夠在某些使用閉包的場景中看到,閉包使用的是徹底無關(生命週期獨立)的代理對象。

當沒法確認兩個對象之間生命週期的關係時,是否不該該去冒險選擇一個無效 unowned 引用?而是保守選擇 weak 引用是一個更好的選擇?

答案是否認的,不只僅是由於對象生命週期瞭解是一件必要的事情,並且這兩個修飾符在性能特性上也有很大的不一樣。

弱引用最多見的實現是,每次一個新的引用生成時,都會把每一個弱引用和它指向的對象信息存儲到一個附加表中。

當沒有任何強引用指向一個對象時,Swift 運行時會啓動析構過程,可是在這以前,運行時會把全部相關的弱引用置爲 nil 。弱引用的這種實現方式咱們稱之爲"零和弱引用"。

這種實現有實際的開銷,考慮到須要額外實現的數據結構,須要確保在併發訪問狀況下,對這個全局引用結構全部操做的正確性。一旦析構過程開始了,在任何環境中,都不容許訪問弱引用所指向的對象了。

弱引用(包括 unowned 和一些變體的 weak)在 Swift 使用了更簡單和快速的實現機制。

Swift 中的每一個對象保持了兩個引用計數器,一個是強引用計數器,用來決定 ARC 何時能夠安全地析構這個對象,另一個附加的弱引用計數器,用來計算建立了多少個指向這個對象的 unowned 或者 weak 引用,當這個計數器爲零時,這個對象將被 析構

須要重點理解的是,只有等到全部 unowned 引用被釋放後,這個對象纔會被真正地析構,而後對象將會保持未解析可訪問狀態,當析構發生後,對象的內容纔會被回收。

每當 unowned 引用被定義時,對應的 unowned 引用計數會進行原子級別地增長(使用原子gcc/llvm操做,進行一系列快速且線程安全的基本操做,例如:增長,減小,比較,交換等),以保證線程安全。在增長計數以前,會檢查強引用計數以確保對象是有效的。

試圖訪問一個無效的對象,將會致使錯誤的斷言,你的應用在運行時中會報錯(這就是爲何這裏的 unownd 實現方式叫作 unowned(safe) 實現)

爲了更好的優化,應用編譯時帶有 -OFastunowned 引用不會去驗證引用對象的有效性,unowned 引用的行爲就會像 Objective-C 中的 __unsafe_unretained 同樣。若是引用對象無效,unowned 引用將會指向已經釋放垃圾內存(這種實現稱之 unowned(unsafe))。

當一個 unowned 引用被釋放後,若是這時沒有其餘強引用或 unowned 引用指向這個對象,那麼最終這個對象將被析構。這就是爲何一個引用對象不能在強引用計數器等於零的狀況下,被析構的緣由,全部的引用計數器必須可以被訪問用來驗證 unowned 引用和強引用數量。

Swift 的 weak 引用添加了附加層,間接地把 unowned 引用包裹到了一個可選容器裏面,在指向的對象析構以後變成空的狀況下,這樣處理會更加的清晰。可是須要付出的代價是,附加的機制須要正確地處理可選值。

考慮到以上因素,在對象關係生命週期容許的狀況下,優先選擇使用 unowned 引用。可是這不是此故事的結局,接下來比較一下二者性能1上的差異。

性能:深度探索

在查看 Swift 項目源碼驗證以前,須要理解 ARC 如何管理這兩種引用類型,而且還須要解釋 swiftcLLVMSIL 的相關知識。

接下來試着簡要介紹本文所須要的必備知識點,若是想了解更多,將在最後的腳註中找到一些有用的連接。

使用一個圖來解釋 swiftc 整個編譯過程的包含的模塊:

Swiftcclang 同樣構建在 LLVM 上,遵循 clang 編譯器類似的編譯流程。

在編譯過程的第一部分,使用一個特定語言前端進行管理,swift 源代碼被解釋生成一個抽象語法樹(AST)表達2,而後抽象語法樹的結果從語義角度進行分析,找出語義錯誤。

在這個點上,對於其餘的基於 LLVM 的編譯器來說,在經過一個附加步驟對源代碼進行靜態分析後(必要時能夠顯示錯誤和警告),接着 IRGen 模塊 會把 AST 的內容會轉換成一個輕量的和底層的機器無關的表示,咱們稱之爲 LLVM IR(LLVM 中間表示)。

儘管兩個模塊都須要作一些相同檢查,可是這兩個模塊是區分開的,在兩個模塊之間也存在許多重複的代碼。

IR 是一種靜態單賦值形式SSA-form)一致語言,能夠看作注入了 LLVM 的虛擬機下的 RISC 類型彙編語言。基於 SSA 將簡化接下來的編譯過程,從語言前端提供的中間表達會在 IR 進行多重優化。

須要重點注意的是,IR 其中一個特色是,它具備三種不一樣的形式:內存表達(內部使用),序列化位代碼形式(你已經知道的位代碼形式)和可讀形式。

最後一種形式很是有用,用來驗證 IR 代碼的最終結構,這個結構將會傳入到整個過程當中的最後一步,將會從機器獨立的 IR 代碼轉換成平臺相關的表達(好比:x86,ARM 等等)。最後一步將被 LLVM 平臺後端執行。

那麼 swiftc 和其餘基於 LLVM 的編譯器有什麼不一樣呢?

swiftc 和其餘編譯器從結構形式上的差異主要體如今一個附加組件,這個附加組件是 SILGen ,在 IRGen 以前,執行代碼的監測和優化,生成一個高級的中間表達,咱們稱之爲 SIL (Swift Intermediate Language,Swift 中間語言),最後 SIL 將會轉換成 LLVM IR。這一步增強了在單個軟件模塊上全部具體語言的檢查,而且簡化了 IRGen

ASTIR 的轉換分爲兩個步驟。SILGenAST 源代碼轉換爲原始 SIL ,而後編譯器進行 Swift 語法檢查(須要時打印錯誤或者警告信息),優化有效的原始 SIL ,經過一些步驟最後生成標準化 SIL 。如上面的示意圖顯示那樣,標準化 SIL 的最後轉化爲 LLVM IR

再次強調,SIL 是一個 SSA 類型語言,使用附加的結構擴展了 Swift 的語法。它依賴 Swift 的類型系統,而且能理解 Swift 的定義,可是須要重點記住的是,當編譯一個手寫的 SIL 源碼(是的,能夠手動寫 SIL 而後編譯它)時,高階的 Swift 代碼或者函數內容將被編譯器忽略。

在接下來的章節,咱們將分析一個標準化 SIL 的案例,來理解 unownedweak 引用如何被編譯器處理。一個包含捕獲列表的基本閉包的例子,查看這個例子生成的 SIL 代碼,能夠看到被編譯器添加的全部 ARC 相關的函數調用。

GitHub 或者 zipped 獲取本文相關的 Playground 代碼。而後從這裏獲取閉包案例和 SILSILGen 以及 LLVM IR 的輸出。

捕獲列表處理解析

接下來看看一個簡單的 Swift 的例子,定義兩個類變量,而後在一個閉包中對他們進行弱引用的捕獲:

class aClass{
    var value = 1
}

var c1 = aClass()
var c2 = aClass()

var fSpec = { 
    [unowned c1, weak c2] in
    c1.value = 42
    if let c2o = c2 {
        c2o.value = 42
    }
}

fSpec()

經過 xcrun swiftc -emit-sil sample.swift 編譯 swift 源代碼,生成標準化 SIL 代碼。原始SIL 可使用 -emit-silgen 選項來生成。

運行以上命令之後,會發現 swiftc 產生了許多代碼。經過查看 swiftc 輸出代碼的片斷,學習一下基本的 SIL 指令,理解整個結構。

在下面代碼中須要的地方添加了一些多行註釋(編譯器也生成了一些單行註釋),但願這些註釋已經足夠說清楚發生了什麼:

/*
  此文件包含典型 SIL 代碼
*/
sil_stage canonical             

/* 
  只有在 SIL 內部使用的特殊的導入
*/
import Builtin                  
import Swift
import SwiftShims

/*
    三個全局變量的定義,包括 c1,c2 和 閉包 fSpec。
      @_Tv4clos2c1CS_6aClass是變量的符號,$aClass 是它的類型(類型前綴爲$)。
      變量名在這裏看起來很亂,可是在後面的代碼中將變得更加可讀。
*/
// c1
sil_global hidden @_Tv4sample2c1CS_6aClass : $aClass

// c2
sil_global hidden @_Tv4sample2c2CS_6aClass : $aClass

// fSpec
sil_global hidden @_Tv4sample5fSpecFT_T_ : $@callee_owned () -> ()

...

/*
  層次做用域定義表示原始代碼的位置。
  每一個 SIL 指示將會指向它生成的 `sil_scope`。
*/
sil_scope 1 {  parent @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 }
sil_scope 2 { loc "sample.swift":14:1 parent 1 }

/*
    自動生成的 @main 函數包含了咱們原始所有做用域的代碼。
     這裏沿用了熟悉的 c main() 函數結構,接收參數個數和參數數組兩個輸入,這個函數遵循 c 調用約定。
      這個函數包含了須要調用閉包的指令。
*/
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
/*
  入口定義頭部爲 % 符號,後面跟隨一個數字 id。
  每當一個新的入口定義時(或者函數開頭定義函數參數),編譯器在入口行尾根據它的值(叫作 users)添加一個註釋。
  對於其餘指令,須要提供 id 號。
  在這裏,入口 0 將被用來計算入口 4 的內容,入口 1 將被用來建立入口 10 的值。
*/
// %0                                             // user: %4
// %1                                             // user: %10
/*
  每個函數被分解成一系列的基本指令塊,每個指令塊結束於一個終止指令(一個分支或者一個返回)。
  這一系列的指令塊表示函數全部可能的執行路徑。
*/
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
...
  /*
    每個 SIL 指令都包含一個引用,指向源代碼的位置,包括指令具體從源代碼中哪一個地方來,屬於哪個做用域。
    在後面分析具體的方法會看到這些內容。
  */
  unowned_retain %27 : $@sil_unowned aClass, loc "sample.swift":9:14, scope 2 // id: %28
  store %27 to %2 : $*@sil_unowned aClass, loc "sample.swift":9:14, scope 2 // id: %29
  %30 = alloc_box $@sil_weak Optional<aClass>, var, name "c2", loc "sample.swift":9:23, scope 2 // users: %46, %44, %43, %31
  %31 = project_box %30 : $@box @sil_weak Optional<aClass>, loc "sample.swift":9:23, scope 2 // user: %35
  %32 = load %19 : $*aClass, loc "sample.swift":9:23, scope 2 // users: %34, %33
  ...
}

...

/* 
  下面是一系列自動生成的`aClass`的方法,包括: init/deinit, setter/getter 和其餘一些工具方法。
  每一個方法前的註釋是編譯器添加的,用來講明代碼的具體做用。
*/

/*
  隱藏方法只在它們模塊中可見。
  @convention(方法名)是 Swift 中方法調用默認的約定,在尾部有一個附加的參數指向它本身。
*/
// aClass.__deallocating_deinit
sil hidden @_TFC4clos6aClassD : $@convention(method) (@owned aClass) -> () {
    ...
}

/*
  @guaranteed 參數表示保證在整個週期內調用此方法都有效。
*/
// aClass.deinit
sil hidden @_TFC4clos6aClassd : $@convention(method) (@guaranteed aClass) -> @owned Builtin.NativeObject {
    ...
}

/*
  [transparent] 修飾的方法是內聯的小方法
*/
// aClass.value.getter
sil hidden [transparent] @_TFC4clos6aClassg5valueSi : $@convention(method) (@guaranteed aClass) -> Int {
    ...
}

// aClass.value.setter
sil hidden [transparent] @_TFC4clos6aClasss5valueSi : $@convention(method) (Int, @guaranteed aClass) -> () {
    ...
}
// aClass.value.materializeForSet
sil hidden [transparent] @_TFC4clos6aClassm5valueSi : $@convention(method) (Builtin.RawPointer, @inout Builtin.UnsafeValueBuffer, @guaranteed aClass) -> (Builtin.RawPointer, Optional<Builtin.RawPointer>) {
    ...
}

/*
  @owned 修飾符表示這個對象將被調用者擁有。
*/
// aClass.init() -> aClass
sil hidden @_TFC4clos6aClasscfT_S0_ : $@convention(method) (@owned aClass) -> @owned aClass {
    ...
}

// aClass.__allocating_init() -> aClass
sil hidden @_TFC4clos6aClassCfT_S0_ : $@convention(method) (@thick aClass.Type) -> @owned aClass {
    ...
}

/* 
  接下面是閉包代碼段
*/
// (closure #1)
sil shared @_TF4closU_FT_T_ : $@convention(thin) (@owned @sil_unowned aClass, @owned @box @sil_weak Optional<aClass>) -> () {
    ...
    /* 關於閉包的 SIL 代碼, 見下文 */

    ...
}

...
/* 
  sil_vtable 定義全部關於 aClass 類的虛函數表。
  sil_vtable 包含了指望的全部自動生成的方法。
*/
sil_vtable aClass {
  #aClass.deinit!deallocator: _TFC4clos6aClassD    // aClass.__deallocating_deinit
  #aClass.value!getter.1: _TFC4clos6aClassg5valueSi    // aClass.value.getter
  #aClass.value!setter.1: _TFC4clos6aClasss5valueSi    // aClass.value.setter
  #aClass.value!materializeForSet.1: _TFC4clos6aClassm5valueSi    // aClass.value.materializeForSet
  #aClass.init!initializer.1: _TFC4clos6aClasscfT_S0_    // aClass.init() -> aClass
}

如今回到主函數,看看兩個類實例如何被獲取到,並如何傳遞給調用他們的閉包。

在這裏,全部標識都被從新整理,使得代碼片斷更加可讀。

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
// %0                                             // user: %4
// %1                                             // user: %10
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  ...
  /*
    全局變量的引用使用三個入口來放置。
  */
  %13 = global_addr @clos.c1 : $*aClass, loc "sample.swift":5:5, scope 1 // users: %26, %17
  ...
  %19 = global_addr @clos.c2 : $*aClass, loc "sample.swift":6:5, scope 1 // users: %32, %23
  ...
  %25 = global_addr @clos.fSpec : $*@callee_owned () -> (), loc "sample.swift":8:5, scope 1 // users: %48, %45
  /*
    c1 是 unowned_retained 的。
    下面的指令增長變量的 unowned 引用計數。
  */
  %26 = load %13 : $*aClass, loc "sample.swift":9:14, scope 2 // user: %27
  %27 = ref_to_unowned %26 : $aClass to $@sil_unowned aClass, loc "sample.swift":9:14, scope 2 // users: %47, %38, %39, %29, %28
  unowned_retain %27 : $@sil_unowned aClass, loc "sample.swift":9:14, scope 2 // id: %28
  store %27 to %2 : $*@sil_unowned aClass, loc "sample.swift":9:14, scope 2 // id: %29
  /*
    對 c2 的處理會更加複雜一些。
    alloc_box 建立了一個這個變量的引用數容器,變量將會存在這個容器的堆中。
    容器建立之後,將會建立一個可選變量,指向 c2,而且可選變量會存儲在容器裏。容器會增長所包含值的技術,正以下面看到的同樣,一旦容器被遷移,可選值就會被釋放。
    在這裏,c2 的值將被存儲在這個可選值中,對象將暫時strong_retained 而後釋放。
  */
  %30 = alloc_box $@sil_weak Optional<aClass>, var, name "c2", loc "sample.swift":9:23, scope 2 // users: %46, %44, %43, %31
  %31 = project_box %30 : $@box @sil_weak Optional<aClass>, loc "sample.swift":9:23, scope 2 // user: %35
  %32 = load %19 : $*aClass, loc "sample.swift":9:23, scope 2 // users: %34, %33
  strong_retain %32 : $aClass, loc "sample.swift":9:23, scope 2 // id: %33
  %34 = enum $Optional<aClass>, #Optional.some!enumelt.1, %32 : $aClass, loc "sample.swift":9:23, scope 2 // users: %36, %35
  store_weak %34 to [initialization] %31 : $*@sil_weak Optional<aClass>, loc "sample.swift":9:23, scope 2 // id: %35
  release_value %34 : $Optional<aClass>, loc "sample.swift":9:23, scope 2 // id: %36
  /*
    獲取到閉包的引用。
  */
  // function_ref (closure #1)
  %37 = function_ref @sample.(closure #1) : $@convention(thin) (@owned @sil_unowned aClass, @owned @box @sil_weak Optional<aClass>) -> (), loc "sample.swift":8:13, scope 2 // user: %44
  /*
    c1 將被標記爲 tagged,而且變量變爲 unowned_retained。
  */
  strong_retain_unowned %27 : $@sil_unowned aClass, loc "sample.swift":8:13, scope 2 // id: %38
  %39 = unowned_to_ref %27 : $@sil_unowned aClass to $aClass, loc "sample.swift":8:13, scope 2 // users: %42, %40
  %40 = ref_to_unowned %39 : $aClass to $@sil_unowned aClass, loc "sample.swift":8:13, scope 2 // users: %44, %41
  unowned_retain %40 : $@sil_unowned aClass, loc "sample.swift":8:13, scope 2 // id: %41
  strong_release %39 : $aClass, loc "sample.swift":8:13, scope 2 // id: %42
  /*
    包含 c2 的可選值容器是 strong_retained 的。
  */
  strong_retain %30 : $@box @sil_weak Optional<aClass>, loc "sample.swift":8:13, scope 2 // id: %43
  /*
    建立一個閉包對象,綁定方法到參數中。
  */
  %44 = partial_apply %37(%40, %30) : $@convention(thin) (@owned @sil_unowned aClass, @owned @box @sil_weak Optional<aClass>) -> (), loc "sample.swift":8:13, scope 2 // user: %45
  store %44 to %25 : $*@callee_owned () -> (), loc "sample.swift":8:13, scope 2 // id: %45
  /*
    
    對 c1 和 c2 的容器變量進行釋放(使用 對應匹配的 *_release 方法)。
  */
  strong_release %30 : $@box @sil_weak Optional<aClass>, loc "sample.swift":14:1, scope 2 // id: %46
  unowned_release %27 : $@sil_unowned aClass, loc "sample.swift":9:14, scope 2 // id: %47
  /*
     加載原先存儲的閉包對象,增長強引用而後調用它。
  */
   %48 = load %25 : $*@callee_owned () -> (), loc "sample.swift":17:1, scope 2 // users: %50, %49
  strong_retain %48 : $@callee_owned () -> (), loc "sample.swift":17:1, scope 2 // id: %49
  %50 = apply %48() : $@callee_owned () -> (), loc "sample.swift":17:7, scope 2
  ...
}

閉包有一個更加複雜的結構:

/*
  閉包參數被標記爲 @sil, 指定參數如何被計數,有一個 unowned 的 aClass 類變量 c2, 和另一個包含 c2 的可選值容器。
*/
// (closure #1)
sil shared @clos.fSpec: $@convention(thin) (@owned @sil_unowned aClass, @owned @box @sil_weak Optional<aClass>) -> () {
// %0                                             // users: %24, %6, %5, %2
// %1                                             // users: %23, %3
/*
  下面的函數包含三塊,後面兩塊的執行依賴可選值 c2 具體的值。
*/
bb0(%0 : $@sil_unowned aClass, %1 : $@box @sil_weak Optional<aClass>):
...
  /*
    c1 被強計數。
  */
  strong_retain_unowned %0 : $@sil_unowned aClass, loc "sample.swift":10:5, scope 17 // id: %5
  %6 = unowned_to_ref %0 : $@sil_unowned aClass to $aClass, loc "sample.swift":10:5, scope 17 // users: %11, %10, %9
  /*
    使用內部自帶包,傳入一個整型字面量到整型結構中,初始化了一個值爲 42 的整型值。
    這個值將被設置爲 c1 的新值,完成之後這個變量將會被釋放。
    在這裏,咱們第一次看到 class_method 指令,用於獲取 vtable 中的函數引用。
  */
  %7 = integer_literal $Builtin.Int64, 42, loc "sample.swift":10:16, scope 17 // user: %8
  %8 = struct $Int (%7 : $Builtin.Int64), loc "sample.swift":10:16, scope 17 // user: %10
  %9 = class_method %6 : $aClass, #aClass.value!setter.1 : (aClass) -> (Int) -> () , $@convention(method) (Int, @guaranteed aClass) -> (), loc "sample.swift":10:14, scope 17 // user: %10
  %10 = apply %9(%8, %6) : $@convention(method) (Int, @guaranteed aClass) -> (), loc "sample.swift":10:14, scope 17
  strong_release %6 : $aClass, loc "sample.swift":10:16, scope 17 // id: %11
  /*
    接下來討論 c2。
    獲取可選值,而後根據它的內容執行接下來的分支語句。

    If the optional has a value the bb2 block will be executed before jumping 
    to bb3, if it doesn't after a brief jump to bb1, the function will proceed to bb3 releasing
    the retained parameters.
    
  */
  %12 = load_weak %3 : $*@sil_weak Optional<aClass>, loc "sample.swift":11:18, scope 18 // user: %13
  switch_enum %12 : $Optional<aClass>, case #Optional.some!enumelt.1: bb2, default bb1, loc "sample.swift":11:18, scope 18 // id: %13
  bb1:                                              // Preds: bb0
  /*
    跳轉到閉包的結尾。
  */
  br bb3, loc "sample.swift":11:18, scope 16        // id: %14

// %15                                            // users: %21, %20, %19, %16
bb2(%15 : $aClass):                               // Preds: bb0
  /*
    調用 aClass 的 setter,設置它的值爲 42.
  */
  ...
  %17 = integer_literal $Builtin.Int64, 42, loc "sample.swift":12:21, scope 19 // user: %18
  %18 = struct $Int (%17 : $Builtin.Int64), loc "sample.swift":12:21, scope 19 // user: %20
  %19 = class_method %15 : $aClass, #aClass.value!setter.1 : (aClass) -> (Int) -> () , $@convention(method) (Int, @guaranteed aClass) -> (), loc "sample.swift":12:19, scope 19 // user: %20
  %20 = apply %19(%18, %15) : $@convention(method) (Int, @guaranteed aClass) -> (), loc "sample.swift":12:19, scope 19
  strong_release %15 : $aClass, loc "sample.swift":13:5, scope 18 // id: %21
  br bb3, loc "sample.swift":13:5, scope 18         // id: %22

bb3:                                              // Preds: bb1 bb2
  /*
    釋放全部獲取的變量而後返回。
  */
  strong_release %1 : $@box @sil_weak Optional<aClass>, loc "sample.swift":14:1, scope 17 // id: %23
  unowned_release %0 : $@sil_unowned aClass, loc "sample.swift":14:1, scope 17 // id: %24
  %25 = tuple (), loc "sample.swift":14:1, scope 17 // user: %26
  return %25 : $(), loc "sample.swift":14:1, scope 17 // id: %26
}

在這裏,忽略掉不一樣的 ARC 指令帶來的性能的差別點,對不一樣階段每種類型的捕獲變量作一個快速的對比:

動做 Unowned Weak
預先調用 #1 對象進行 unowned_retain 操做 建立一個容器,而且對象進行 strong_retain 操做。建立一個可選值,存入到容器中,而後釋放可選值
預先調用 #2 strong_retain_unowned,unowned_retain 和 strong_release strong_retain
閉包執行 strong_retain_unowned,unowned_release load_weak, 打開可選值, strong_release
調用以後 unowned_release strong_release

正如上面看到的 SIL 代碼段那樣,處理 weak 引用會涉及到更多的工做,由於須要處理引用須要的可選值。

參照官方文檔的描述,這裏對涉及到的全部 ARC 指令作一個簡要的解釋:

  • unowned_retain增長堆對象中的 unowned 引用計數。

  • strong_retain_unowned斷言對象的強引用計數大於 0,而後增長這個引用計數。

  • strong_retain增長對象的強引用計數。

  • load_weak不是真正的 ARC 調用,可是它將增長可選值指向對象的強引用計數。

  • strong_release減小對象的強引用計數。若是釋放操做把對象強引用計數變爲0,對象將被銷燬,而後弱引用將被清除。當整個強引用計數和 unowned 引用計數都爲0時,對象的內存纔會被釋放。

  • unowned_release減小對象的 unowned 引用計數。當整個強引用計數和 unowned 引用計數都爲 0 時,對象的內存纔會被釋放。

接下來深刻到 Swift 運行時看看,這些指令都是如何被實現的,相關的代碼文件有:HeapObject.cppHeapObject.hRefCount.hHeap.cppSwiftObject.mm 中的少許定義。容器實現能夠在 MetadataImpl.h 找到,可是本文不展開討論。

這些文件中定義大多數的 ARC 方法都有三種變體,一種是對 Swift 對象的基礎實現,另外兩種實現是針對非原生 Swift 對象的:橋接對象和未知對象。後面兩種變體這裏不予討論。

第一個討論指令集和 unowned 引用相關。

HeapObject.cpp 文件中間能夠看到對 unowned_retainunowned_release 的實現方法:

SWIFT_RT_ENTRY_VISIBILITY
void swift::swift_unownedRetain(HeapObject *object)
    SWIFT_CC(RegisterPreservingCC_IMPL) {
  if (!object)
    return;

  object->weakRefCount.increment();
}

SWIFT_RT_ENTRY_VISIBILITY
void swift::swift_unownedRelease(HeapObject *object)
    SWIFT_CC(RegisterPreservingCC_IMPL) {
  if (!object)
    return;

  if (object->weakRefCount.decrementShouldDeallocate()) {
    // Only class objects can be weak-retained and weak-released.
    auto metadata = object->metadata;
    assert(metadata->isClassObject());
    auto classMetadata = static_cast<const ClassMetadata*>(metadata);
    assert(classMetadata->isTypeMetadata());
    SWIFT_RT_ENTRY_CALL(swift_slowDealloc)
        (object, classMetadata->getInstanceSize(),
         classMetadata->getInstanceAlignMask());
  }
}

swift_unownedRetainunowned_retain 的具體實現,簡單地進行 unowned 引用計數的原子增長操做(這裏定義爲weakRefCount),swift_unownedRelease 更加複雜,緣由以前也描述過,當沒有其餘 unowned 引用存在時,它須要執行對象的析構操做。

可是總體來說都不復雜,在這裏能夠看到 doDecrementShouldDeallocate 方法,這個方法在上面代碼中被一個命名相似的方法調用了。這個方法沒有作太多,swift_slowDealloc 只是釋放給定的指針。

到此已經有了一個對象的 unowned 引用,另一個指令,strong_retain_unowned 用來建立一個強引用:

SWIFT_RT_ENTRY_VISIBILITY
void swift::swift_unownedRetainStrong(HeapObject *object)
    SWIFT_CC(RegisterPreservingCC_IMPL) {
  if (!object)
    return;
  assert(object->weakRefCount.getCount() &&
         "object is not currently weakly retained");

  if (! object->refCount.tryIncrement())
    _swift_abortRetainUnowned(object);
}

由於弱引用應該指向了這個對象,要使用斷言來驗證對象是否被弱引用,一旦斷言經過,將嘗試進行增長強引用計數的操做。一旦對象在進程中已經被釋放,嘗試將會失敗。

全部相似於 tryIncrement 經過某種形式修改引用計數的方法都放到 RefCount.h 中,須要使用原子操做進行這些任務。

接下來討論下 weak 引用的的實現,正如以前看到的那樣,swift_weakLoadStrong 用來獲取容器中可選值中強引用的對象。

HeapObject *swift::swift_weakLoadStrong(WeakReference *ref) {
  if (ref->Value == (uintptr_t)nullptr) {
    return nullptr;
  }

  // ref 可能被其餘線程訪問
  auto ptr = __atomic_fetch_or(&ref->Value, WR_READING, __ATOMIC_RELAXED);
  while (ptr & WR_READING) {
    short c = 0;
    while (__atomic_load_n(&ref->Value, __ATOMIC_RELAXED) & WR_READING) {
      if (++c == WR_SPINLIMIT) {
        std::this_thread::yield();
        c -= 1;
      }
    }
    ptr = __atomic_fetch_or(&ref->Value, WR_READING, __ATOMIC_RELAXED);
  }

  auto object = (HeapObject*)(ptr & ~WR_NATIVE);
  if (object == nullptr) {
    __atomic_store_n(&ref->Value, (uintptr_t)nullptr, __ATOMIC_RELAXED);
    return nullptr;
  }
  if (object->refCount.isDeallocating()) {
    __atomic_store_n(&ref->Value, (uintptr_t)nullptr, __ATOMIC_RELAXED);
    SWIFT_RT_ENTRY_CALL(swift_unownedRelease)(object);
    return nullptr;
  }
  auto result = swift_tryRetain(object);
  __atomic_store_n(&ref->Value, ptr, __ATOMIC_RELAXED);
  return result;
}

在這個實現中,獲取一個強引用須要更多複雜同步操做,在多線程競爭嚴重的狀況下,會帶來性能損耗。

在這裏第一次出現的 WeakReference 對象,是一個簡單的結構體,包含一個整型值字段指向目標對象,目標對象是使用 HeapObject 類來承載的每個運行時的 Swift 對象。

在 weak 引用詢問當前線程設置的 WR_READING 標識以後,從 WeakReference 容器中獲取 Swift 對象,若是對象再也不有效,或者在等待獲取資源時,它變成能夠進行析構,當前的引用會被設置爲 null

若是對象依然有效,獲取對象的嘗試將會成功。

所以,從這個角度來說,對 weak 引用的常規操做性能比 unowned 引用的更低(可是主要的問題仍是在可選值操做上面)。

結論

保守的使用 weak 引用是否明智呢?答案是否認的,不管是從性能的角度仍是代碼清晰的角度而言。

使用正確的捕獲修飾符類型,明確的代表代碼中的生命週期特性,當其餘人或者你本身在讀你的代碼時不容易誤解。

腳註


一、*蘋果第一次討論 weak/unowned 爭議能夠查看這裏,以後在 twitter 上 Joe Groff 對此也進行了討論,而且被 Michael Tsai 總結成文
這篇文章從意圖角度出發,提供了完整而且可操做的解釋。*


二、維基百科中能夠找到關於 AST 的解釋,還能夠從 Slava Pestov 的這篇文章中看到關於 Swift 編譯器中如何實現 AST 的一些細節。


三、關於 SIL 的更多信息,請查看詳盡的官方 SIL 指南,還有 2015 LLVM 開發者會議的視頻。Lex Chou 寫的 SIL 快速指南能夠點擊這裏查看


四、查看在 Swift 中如何進行名稱粉碎(name mangling)的細節,請查看 Lex Chou 的這篇文章


五、Mike Ash 在他的 Friday Q&A 中的一篇文章中討論瞭如何實現 weak 引用的一種實踐方法,這種方法與目前 Swift 的方法對比起來有一些過期,可是其中的解釋依然值得參考。

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg

相關文章
相關標籤/搜索