做者:Umberto Raimondi,原文連接,原文日期:2016-10-27
譯者:shanks;校對:Crystal Sun;定稿:CMBhtml
每當處理循環引用(retain cycles)時,須要考量對象生命週期來選擇unowned
或者weak
標識符,這已經成爲了一個共識。可是有時仍然會心存疑問,在具體的使用中應該選擇哪個,或者退一步講,保守的只使用 weak 是否是一個好的選擇呢?前端
本文首先對循環引用的基礎知識作一個簡要介紹,而後會分析 Swift 源代碼的一些片斷,講解 unowned
和 weak
在生命週期和性能上的差別點,但願看完本文之後,在的使用場景中,能使用正確的弱引用類型。git
目錄:github
從 GitHub 或者 zipped 獲取本文相關的 Playground 代碼。而後從這裏獲取閉包案例和 SIL,SILGen 以及 LLVM IR 的輸出。
衆所周知,Swift
利用古老而且有效的自動引用計數(ARC, Automatic Reference Counting)來管理內存,帶來的後果和在 Objective-C 中使用的狀況相似,須要手動使用弱引用來解決循環引用問題。
若是對 ARC
不瞭解,只須要知道的是,每個引用類型實例都有一個引用計數與之關聯,這個引用計數用來記錄這個對象實例正在被變量或常量引用的總次數。當引用計數變爲 0
時,實例將會被析構,實例佔有的內存和資源都將變得從新可用。
當有兩個實例經過某種形式互相引用時,就會造成循環引用(好比:兩個類實例都有一個屬性指向對方的類實例;雙向鏈表中兩個相鄰的節點實例等...), 因爲兩個實例的引用計數都一直大於 0
, 循環引用將會阻止這些實例的析構。
爲了解決這個問題,和其餘一些有相似問題的語言同樣, 在 Swift
中,弱引用 的概念被提了出來,弱引用不會被 ARC
計算,也就是說,當一個弱引用指向一個引用類型實例時,引用計數不會增長。
弱引用不會阻止實例的析構, 只須要記住的是,在任何狀況下,弱引用都不會擁有它指向的對象。在正式的場景中不是什麼大問題,可是在咱們處理這類引用的時候,須要意識到這一點。
在 Swift 中有 2 種 弱 引用形式,unowned
和 weak
。
雖然它們的做用相似,但與它們相關實例生命週期的假設會略有不一樣,而且具備不一樣的性能特徵。
爲了舉例說明循環引用,這裏不使用你們指望看到的類之間的循環引用,而使用閉包的上下文案例,這在 Objective-C
平常開發中處理循環引用時常常會遇到的狀況。和類的循環引用相似,經過建立一個強引用指向外部實例,或捕獲它,阻止它析構。
在 Objective-C
,按照標準的作法,定義一個弱引用指向閉包外部的實例,而後在閉包內部定義強引用指向這個實例,在閉包執行期間使用它。固然,有必要在使用前檢查引用的有效性。
爲了更方便的處理循環引用,Swift
引入了一個新的概念,用於簡化和更加明顯地表達在閉包內部外部變量的捕獲:捕獲列表(capture list)。使用捕獲列表,能夠在函數的頭部定義和指定那些須要用在內部的外部變量,而且指定引用類型(譯者注:這裏是指 unowned
和 weak
)。
接下來舉一些例子,在各類狀況下捕獲變量的表現。
當不使用捕獲列表時,閉包將會建立一個外部變量的強引用:
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
的原始值。
對於外部引用類型的變量,在捕獲列表中指定 weak
或 unowned
,這個常量將會被初始化爲一個弱引用,指向原始值,這種指定的捕獲方式就是用來處理循環引用的方式。
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
, 而且在使用以前驗證這個引用的有效性。
在實際使用中如何選擇這兩種弱引用類型呢?
這個問題的答案能夠簡單由原始對象和引用它的閉包的生命週期來解釋。
有兩個可能出現的場景:
閉包和捕獲對象的生命週期相同,因此對象能夠被訪問,也就意味着閉包也能夠被訪問。外部對象和閉包有相同的生命週期(好比:對象和它的父對象的簡單返回引用)。在這種狀況下,你應該把引用定義爲 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)
實現)
爲了更好的優化,應用編譯時帶有 -OFast
,unowned
引用不會去驗證引用對象的有效性,unowned
引用的行爲就會像 Objective-C
中的 __unsafe_unretained
同樣。若是引用對象無效,unowned
引用將會指向已經釋放垃圾內存(這種實現稱之 unowned(unsafe)
)。
當一個 unowned
引用被釋放後,若是這時沒有其餘強引用或 unowned
引用指向這個對象,那麼最終這個對象將被析構。這就是爲何一個引用對象不能在強引用計數器等於零的狀況下,被析構的緣由,全部的引用計數器必須可以被訪問用來驗證 unowned
引用和強引用數量。
Swift 的 weak
引用添加了附加層,間接地把 unowned
引用包裹到了一個可選容器裏面,在指向的對象析構以後變成空的狀況下,這樣處理會更加的清晰。可是須要付出的代價是,附加的機制須要正確地處理可選值。
考慮到以上因素,在對象關係生命週期容許的狀況下,優先選擇使用 unowned
引用。可是這不是此故事的結局,接下來比較一下二者性能1上的差異。
在查看 Swift
項目源碼驗證以前,須要理解 ARC
如何管理這兩種引用類型,而且還須要解釋 swiftc
,LLVM
和 SIL
的相關知識。
接下來試着簡要介紹本文所須要的必備知識點,若是想了解更多,將在最後的腳註中找到一些有用的連接。
使用一個圖來解釋 swiftc 整個編譯過程的包含的模塊:
Swiftc
和 clang 同樣構建在 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。
從 AST
到 IR
的轉換分爲兩個步驟。SILGen 把 AST
源代碼轉換爲原始 SIL
,而後編譯器進行 Swift
語法檢查(須要時打印錯誤或者警告信息),優化有效的原始 SIL
,經過一些步驟最後生成標準化 SIL
。如上面的示意圖顯示那樣,標準化 SIL
的最後轉化爲 LLVM IR
。
再次強調,SIL
是一個 SSA
類型語言,使用附加的結構擴展了 Swift
的語法。它依賴 Swift
的類型系統,而且能理解 Swift
的定義,可是須要重點記住的是,當編譯一個手寫的 SIL
源碼(是的,能夠手動寫 SIL
而後編譯它)時,高階的 Swift
代碼或者函數內容將被編譯器忽略。
在接下來的章節,咱們將分析一個標準化 SIL
的案例,來理解 unowned
和 weak
引用如何被編譯器處理。一個包含捕獲列表的基本閉包的例子,查看這個例子生成的 SIL
代碼,能夠看到被編譯器添加的全部 ARC
相關的函數調用。
從 GitHub 或者 zipped 獲取本文相關的 Playground 代碼。而後從這裏獲取閉包案例和
SIL
,SILGen
以及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.cpp,HeapObject.h,RefCount.h 和 Heap.cpp、 SwiftObject.mm 中的少許定義。容器實現能夠在 MetadataImpl.h 找到,可是本文不展開討論。
這些文件中定義大多數的 ARC
方法都有三種變體,一種是對 Swift
對象的基礎實現,另外兩種實現是針對非原生 Swift 對象的:橋接對象和未知對象。後面兩種變體這裏不予討論。
第一個討論指令集和 unowned
引用相關。
在 HeapObject.cpp 文件中間能夠看到對 unowned_retain
和 unowned_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_unownedRetain
是 unowned_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。