Unowned 還是 Weak?生命周期和性能對比

作者:Umberto Raimondi,原文鏈接,原文日期:2016-10-27
譯者:shanks;校對:Crystal Sun;定稿:CMB

每當(dāng)處理循環(huán)引用(retain cycles)時(shí),需要考量對象生命周期來選擇unowned或者weak標(biāo)識(shí)符,這已經(jīng)成為了一個(gè)共識(shí)。但是有時(shí)仍然會(huì)心存疑問,在具體的使用中應(yīng)該選擇哪一個(gè),或者退一步講,保守的只使用 weak 是不是一個(gè)好的選擇呢?

本文首先對循環(huán)引用的基礎(chǔ)知識(shí)做一個(gè)簡要介紹,然后會(huì)分析 Swift 源代碼的一些片段,講解 unownedweak 在生命周期和性能上的差異點(diǎn),希望看完本文以后,在的使用場景中,能使用正確的弱引用類型。

目錄:

GitHub 或者 zipped 獲取本文相關(guān)的 Playground 代碼。然后從這里獲取閉包案例和 SIL,SILGen 以及 LLVM IR 的輸出。

<a name="the_basic"></a>

基礎(chǔ)知識(shí)

眾所周知,Swift 利用古老并且有效的自動(dòng)引用計(jì)數(shù)(ARC, Automatic Reference Counting)來管理內(nèi)存,帶來的后果和在 Objective-C 中使用的情況類似,需要手動(dòng)使用弱引用來解決循環(huán)引用問題。

如果對 ARC 不了解,只需要知道的是,每一個(gè)引用類型實(shí)例都有一個(gè)引用計(jì)數(shù)與之關(guān)聯(lián),這個(gè)引用計(jì)數(shù)用來記錄這個(gè)對象實(shí)例正在被變量或常量引用的總次數(shù)。當(dāng)引用計(jì)數(shù)變?yōu)?0 時(shí),實(shí)例將會(huì)被析構(gòu),實(shí)例占有的內(nèi)存和資源都將變得重新可用。

當(dāng)有兩個(gè)實(shí)例通過某種形式互相引用時(shí),就會(huì)形成循環(huán)引用(比如:兩個(gè)類實(shí)例都有一個(gè)屬性指向?qū)Ψ降念悓?shí)例;雙向鏈表中兩個(gè)相鄰的節(jié)點(diǎn)實(shí)例等...), 由于兩個(gè)實(shí)例的引用計(jì)數(shù)都一直大于 0, 循環(huán)引用將會(huì)阻止這些實(shí)例的析構(gòu)。

為了解決這個(gè)問題,和其他一些有類似問題的語言一樣, 在 Swift 中,弱引用 的概念被提了出來,弱引用不會(huì)被 ARC 計(jì)算,也就是說,當(dāng)一個(gè)弱引用指向一個(gè)引用類型實(shí)例時(shí),引用計(jì)數(shù)不會(huì)增加。

弱引用不會(huì)阻止實(shí)例的析構(gòu), 只需要記住的是,在任何情況下,弱引用都不會(huì)擁有它指向的對象。在正式的場景中不是什么大問題,但是在我們處理這類引用的時(shí)候,需要意識(shí)到這一點(diǎn)。

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

雖然它們的作用類似,但與它們相關(guān)實(shí)例生命周期的假設(shè)會(huì)略有不同,并且具有不同的性能特征。

為了舉例說明循環(huán)引用,這里不使用大家期望看到的類之間的循環(huán)引用,而使用閉包的上下文案例,這在 Objective-C 日常開發(fā)中處理循環(huán)引用時(shí)經(jīng)常會(huì)遇到的情況。和類的循環(huán)引用類似,通過創(chuàng)建一個(gè)強(qiáng)引用指向外部實(shí)例,或捕獲它,阻止它析構(gòu)。

Objective-C ,按照標(biāo)準(zhǔn)的做法,定義一個(gè)弱引用指向閉包外部的實(shí)例,然后在閉包內(nèi)部定義強(qiáng)引用指向這個(gè)實(shí)例,在閉包執(zhí)行期間使用它。當(dāng)然,有必要在使用前檢查引用的有效性。

為了更方便的處理循環(huán)引用,Swift 引入了一個(gè)新的概念,用于簡化和更加明顯地表達(dá)在閉包內(nèi)部外部變量的捕獲:捕獲列表(capture list)。使用捕獲列表,可以在函數(shù)的頭部定義和指定那些需要用在內(nèi)部的外部變量,并且指定引用類型(譯者注:這里是指 unownedweak)。

接下來舉一些例子,在各種情況下捕獲變量的表現(xiàn)。

當(dāng)不使用捕獲列表時(shí),閉包將會(huì)創(chuàng)建一個(gè)外部變量的強(qiáng)引用:

var i1 = 1, i2 = 1

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

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

閉包內(nèi)部對變量的修改將會(huì)改變外部原始變量的值,這與預(yù)期是一致的。

使用捕獲列表,閉包內(nèi)部會(huì)創(chuàng)建一個(gè)新的可用常量。如果沒有指定常量修飾符,閉包將會(huì)簡單地拷貝原始值到新的變量中,對于值類型和引用類型都是一樣的。

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

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

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

在上面的例子中,在調(diào)用 fStrong 之前定義函數(shù) fCopy ,在該函數(shù)定義的時(shí)候,私有常量已經(jīng)被創(chuàng)建了。正如你所看到的,當(dāng)調(diào)用第二個(gè)函數(shù)時(shí)候,仍然打印 i1 的原始值。

對于外部引用類型的變量,在捕獲列表中指定 weakunowned,這個(gè)常量將會(huì)被初始化為一個(gè)弱引用,指向原始值,這種指定的捕獲方式就是用來處理循環(huán)引用的方式。

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

兩個(gè) aClass 捕獲實(shí)例的不同的定義方式,決定了它們在閉包中不同的使用方式。

unowned 引用使用的場景是,原始實(shí)例永遠(yuǎn)不會(huì)為 nil,閉包可以直接使用它,并且直接定義為顯式解包可選值。當(dāng)原始實(shí)例被析構(gòu)后,在閉包中使用這個(gè)捕獲值將導(dǎo)致崩潰。

如果捕獲原始實(shí)例在使用過程中可能為 nil ,必須將引用聲明為 weak, 并且在使用之前驗(yàn)證這個(gè)引用的有效性。

<a name="the_question_unowned_or_weak"></a>

問題來了: unowened 還是 weak?

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

這個(gè)問題的答案可以簡單由原始對象和引用它的閉包的生命周期來解釋。

有兩個(gè)可能出現(xiàn)的場景:

  • 閉包和捕獲對象的生命周期相同,所以對象可以被訪問,也就意味著閉包也可以被訪問。外部對象和閉包有相同的生命周期(比如:對象和它的父對象的簡單返回引用)。在這種情況下,你應(yīng)該把引用定義為 unowned

一個(gè)經(jīng)典的案例是: [unowned self], 主要用在閉包中,這種閉包主要在他們的父節(jié)點(diǎn)上下文中做一些事情,沒有在其他地方被引用或傳遞,不能作用在父節(jié)點(diǎn)之外。

  • 閉包的生命周期和捕獲對象的生命周期相互獨(dú)立,當(dāng)對象不能再使用時(shí),閉包依然能夠被引用。這種情況下,你應(yīng)該把引用定義為 weak,并且在使用它之前驗(yàn)證一下它是否為 nil(請不要對它進(jìn)行強(qiáng)制解包).

一個(gè)經(jīng)典的案例是: [weak delegate = self.delegate!],可以在某些使用閉包的場景中看到,閉包使用的是完全無關(guān)(生命周期獨(dú)立)的代理對象。

當(dāng)無法確認(rèn)兩個(gè)對象之間生命周期的關(guān)系時(shí),是否不應(yīng)該去冒險(xiǎn)選擇一個(gè)無效 unowned 引用?而是保守選擇 weak 引用是一個(gè)更好的選擇?

答案是否定的,不僅僅是因?yàn)閷ο笊芷诹私馐且患匾氖虑?,而且這兩個(gè)修飾符在性能特性上也有很大的不同。

弱引用最常見的實(shí)現(xiàn)是,每次一個(gè)新的引用生成時(shí),都會(huì)把每個(gè)弱引用和它指向的對象信息存儲(chǔ)到一個(gè)附加表中。

當(dāng)沒有任何強(qiáng)引用指向一個(gè)對象時(shí),Swift 運(yùn)行時(shí)會(huì)啟動(dòng)析構(gòu)過程,但是在這之前,運(yùn)行時(shí)會(huì)把所有相關(guān)的弱引用置為 nil 。弱引用的這種實(shí)現(xiàn)方式我們稱之為"零和弱引用"。

這種實(shí)現(xiàn)有實(shí)際的開銷,考慮到需要額外實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu),需要確保在并發(fā)訪問情況下,對這個(gè)全局引用結(jié)構(gòu)所有操作的正確性。一旦析構(gòu)過程開始了,在任何環(huán)境中,都不允許訪問弱引用所指向的對象了。

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

Swift 中的每個(gè)對象保持了兩個(gè)引用計(jì)數(shù)器,一個(gè)是強(qiáng)引用計(jì)數(shù)器,用來決定 ARC 什么時(shí)候可以安全地析構(gòu)這個(gè)對象,另外一個(gè)附加的弱引用計(jì)數(shù)器,用來計(jì)算創(chuàng)建了多少個(gè)指向這個(gè)對象的 unowned 或者 weak 引用,當(dāng)這個(gè)計(jì)數(shù)器為零時(shí),這個(gè)對象將被 析構(gòu) 。

需要重點(diǎn)理解的是,只有等到所有 unowned 引用被釋放后,這個(gè)對象才會(huì)被真正地析構(gòu),然后對象將會(huì)保持未解析可訪問狀態(tài),當(dāng)析構(gòu)發(fā)生后,對象的內(nèi)容才會(huì)被回收。

每當(dāng) unowned 引用被定義時(shí),對應(yīng)的 unowned 引用計(jì)數(shù)會(huì)進(jìn)行原子級(jí)別地增加(使用原子gcc/llvm操作,進(jìn)行一系列快速且線程安全的基本操作,例如:增加,減少,比較,交換等),以保證線程安全。在增加計(jì)數(shù)之前,會(huì)檢查強(qiáng)引用計(jì)數(shù)以確保對象是有效的。

試圖訪問一個(gè)無效的對象,將會(huì)導(dǎo)致錯(cuò)誤的斷言,你的應(yīng)用在運(yùn)行時(shí)中會(huì)報(bào)錯(cuò)(這就是為什么這里的 unownd 實(shí)現(xiàn)方式叫做 unowned(safe) 實(shí)現(xiàn))

為了更好的優(yōu)化,應(yīng)用編譯時(shí)帶有 -OFastunowned 引用不會(huì)去驗(yàn)證引用對象的有效性,unowned 引用的行為就會(huì)像 Objective-C 中的 __unsafe_unretained 一樣。如果引用對象無效,unowned 引用將會(huì)指向已經(jīng)釋放垃圾內(nèi)存(這種實(shí)現(xiàn)稱之 unowned(unsafe))。

當(dāng)一個(gè) unowned 引用被釋放后,如果這時(shí)沒有其他強(qiáng)引用或 unowned 引用指向這個(gè)對象,那么最終這個(gè)對象將被析構(gòu)。這就是為什么一個(gè)引用對象不能在強(qiáng)引用計(jì)數(shù)器等于零的情況下,被析構(gòu)的原因,所有的引用計(jì)數(shù)器必須能夠被訪問用來驗(yàn)證 unowned 引用和強(qiáng)引用數(shù)量。

Swift 的 weak 引用添加了附加層,間接地把 unowned 引用包裹到了一個(gè)可選容器里面,在指向的對象析構(gòu)之后變成空的情況下,這樣處理會(huì)更加的清晰。但是需要付出的代價(jià)是,附加的機(jī)制需要正確地處理可選值。

考慮到以上因素,在對象關(guān)系生命周期允許的情況下,優(yōu)先選擇使用 unowned 引用。但是這不是此故事的結(jié)局,接下來比較一下兩者性能1上的差別。

<a name="performance_a_look_under_the_hood"></a>

性能:深度探索

在查看 Swift 項(xiàng)目源碼驗(yàn)證之前,需要理解 ARC 如何管理這兩種引用類型,并且還需要解釋 swiftc,LLVMSIL 的相關(guān)知識(shí)。

接下來試著簡要介紹本文所需要的必備知識(shí)點(diǎn),如果想了解更多,將在最后的腳注中找到一些有用的鏈接。

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

Swiftcclang 一樣構(gòu)建在 LLVM 上,遵循 clang 編譯器相似的編譯流程。

在編譯過程的第一部分,使用一個(gè)特定語言前端進(jìn)行管理,swift 源代碼被解釋生成一個(gè)抽象語法樹(AST)表達(dá)2,然后抽象語法樹的結(jié)果從語義角度進(jìn)行分析,找出語義錯(cuò)誤。

在這個(gè)點(diǎn)上,對于其他的基于 LLVM 的編譯器來講,在通過一個(gè)附加步驟對源代碼進(jìn)行靜態(tài)分析后(必要時(shí)可以顯示錯(cuò)誤和警告),接著 IRGen 模塊 會(huì)把 AST 的內(nèi)容會(huì)轉(zhuǎn)換成一個(gè)輕量的和底層的機(jī)器無關(guān)的表示,我們稱之為 LLVM IR(LLVM 中間表示)。

盡管兩個(gè)模塊都需要做一些相同檢查,但是這兩個(gè)模塊是區(qū)分開的,在兩個(gè)模塊之間也存在許多重復(fù)的代碼。

IR 是一種靜態(tài)單賦值形式SSA-form)一致語言,可以看做注入了 LLVM 的虛擬機(jī)下的 RISC 類型匯編語言?;?SSA 將簡化接下來的編譯過程,從語言前端提供的中間表達(dá)會(huì)在 IR 進(jìn)行多重優(yōu)化。

需要重點(diǎn)注意的是,IR 其中一個(gè)特點(diǎn)是,它具有三種不同的形式:內(nèi)存表達(dá)(內(nèi)部使用),序列化位代碼形式(你已經(jīng)知道的位代碼形式)和可讀形式。

最后一種形式非常有用,用來驗(yàn)證 IR 代碼的最終結(jié)構(gòu),這個(gè)結(jié)構(gòu)將會(huì)傳入到整個(gè)過程中的最后一步,將會(huì)從機(jī)器獨(dú)立的 IR 代碼轉(zhuǎn)換成平臺(tái)相關(guān)的表達(dá)(比如:x86,ARM 等等)。最后一步將被 LLVM 平臺(tái)后端執(zhí)行。

那么 swiftc 和其他基于 LLVM 的編譯器有什么不同呢?

swiftc 和其他編譯器從結(jié)構(gòu)形式上的差別主要體現(xiàn)在一個(gè)附加組件,這個(gè)附加組件是 SILGen ,在 IRGen 之前,執(zhí)行代碼的監(jiān)測和優(yōu)化,生成一個(gè)高級(jí)的中間表達(dá),我們稱之為 SIL (Swift Intermediate Language,Swift 中間語言),最后 SIL 將會(huì)轉(zhuǎn)換成 LLVM IR。這一步加強(qiáng)了在單個(gè)軟件模塊上所有具體語言的檢查,并且簡化了 IRGen。

ASTIR 的轉(zhuǎn)換分為兩個(gè)步驟。SILGenAST 源代碼轉(zhuǎn)換為原始 SIL ,然后編譯器進(jìn)行 Swift 語法檢查(需要時(shí)打印錯(cuò)誤或者警告信息),優(yōu)化有效的原始 SIL ,通過一些步驟最后生成標(biāo)準(zhǔn)化 SIL 。如上面的示意圖顯示那樣,標(biāo)準(zhǔn)化 SIL 的最后轉(zhuǎn)化為 LLVM IR

再次強(qiáng)調(diào),SIL 是一個(gè) SSA 類型語言,使用附加的結(jié)構(gòu)擴(kuò)展了 Swift 的語法。它依賴 Swift 的類型系統(tǒng),并且能理解 Swift 的定義,但是需要重點(diǎn)記住的是,當(dāng)編譯一個(gè)手寫的 SIL 源碼(是的,可以手動(dòng)寫 SIL 然后編譯它)時(shí),高階的 Swift 代碼或者函數(shù)內(nèi)容將被編譯器忽略。

在接下來的章節(jié),我們將分析一個(gè)標(biāo)準(zhǔn)化 SIL 的案例,來理解 unownedweak 引用如何被編譯器處理。一個(gè)包含捕獲列表的基本閉包的例子,查看這個(gè)例子生成的 SIL 代碼,可以看到被編譯器添加的所有 ARC 相關(guān)的函數(shù)調(diào)用。

GitHub 或者 zipped 獲取本文相關(guān)的 Playground 代碼。然后從這里獲取閉包案例和 SIL ,SILGen 以及 LLVM IR 的輸出。

<a name="deconstructing_capture_lists_handling"></a>

捕獲列表處理解析

接下來看看一個(gè)簡單的 Swift 的例子,定義兩個(gè)類變量,然后在一個(gè)閉包中對他們進(jìn)行弱引用的捕獲:

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 源代碼,生成標(biāo)準(zhǔn)化 SIL 代碼。原始SIL 可以使用 -emit-silgen 選項(xiàng)來生成。

運(yùn)行以上命令以后,會(huì)發(fā)現(xiàn) swiftc 產(chǎn)生了許多代碼。通過查看 swiftc 輸出代碼的片段,學(xué)習(xí)一下基本的 SIL 指令,理解整個(gè)結(jié)構(gòu)。

在下面代碼中需要的地方添加了一些多行注釋(編譯器也生成了一些單行注釋),希望這些注釋已經(jīng)足夠說清楚發(fā)生了什么:

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

/* 
  只有在 SIL 內(nèi)部使用的特殊的導(dǎo)入
*/
import Builtin                  
import Swift
import SwiftShims

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

// c2
sil_global hidden @_Tv4sample2c2CS_6aClass : $aClass

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

...

/*
  層次作用域定義表示原始代碼的位置。
  每個(gè) SIL 指示將會(huì)指向它生成的 `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 }

/*
    自動(dòng)生成的 @main 函數(shù)包含了我們原始全部作用域的代碼。
    這里沿用了熟悉的 c main() 函數(shù)結(jié)構(gòu),接收參數(shù)個(gè)數(shù)和參數(shù)數(shù)組兩個(gè)輸入,這個(gè)函數(shù)遵循 c 調(diào)用約定。
    這個(gè)函數(shù)包含了需要調(diào)用閉包的指令。
*/
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
/*
  入口定義頭部為 % 符號(hào),后面跟隨一個(gè)數(shù)字 id。
  每當(dāng)一個(gè)新的入口定義時(shí)(或者函數(shù)開頭定義函數(shù)參數(shù)),編譯器在入口行尾根據(jù)它的值(叫做 users)添加一個(gè)注釋。
  對于其他指令,需要提供 id 號(hào)。
  在這里,入口 0 將被用來計(jì)算入口 4 的內(nèi)容,入口 1 將被用來創(chuàng)建入口 10 的值。
*/
// %0                                             // user: %4
// %1                                             // user: %10
/*
  每一個(gè)函數(shù)被分解成一系列的基本指令塊,每一個(gè)指令塊結(jié)束于一個(gè)終止指令(一個(gè)分支或者一個(gè)返回)。
  這一系列的指令塊表示函數(shù)所有可能的執(zhí)行路徑。
*/
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
...
  /*
    每一個(gè) SIL 指令都包含一個(gè)引用,指向源代碼的位置,包括指令具體從源代碼中哪個(gè)地方來,屬于哪一個(gè)作用域。
    在后面分析具體的方法會(huì)看到這些內(nèi)容。
  */
  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
  ...
}

...

/* 
  下面是一系列自動(dòng)生成的`aClass`的方法,包括: init/deinit, setter/getter 和其他一些工具方法。
  每個(gè)方法前的注釋是編譯器添加的,用來說明代碼的具體作用。
*/

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

/*
  @guaranteed 參數(shù)表示保證在整個(gè)周期內(nèi)調(diào)用此方法都有效。
*/
// aClass.deinit
sil hidden @_TFC4clos6aClassd : $@convention(method) (@guaranteed aClass) -> @owned Builtin.NativeObject {
    ...
}

/*
  [transparent] 修飾的方法是內(nèi)聯(lián)的小方法
*/
// 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 修飾符表示這個(gè)對象將被調(diào)用者擁有。
*/
// 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>) -> () {
    ...
    /* 關(guān)于閉包的 SIL 代碼, 見下文 */

    ...
}

...
/* 
  sil_vtable 定義所有關(guān)于 aClass 類的虛函數(shù)表。
  sil_vtable 包含了期望的所有自動(dòng)生成的方法。
*/
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
}

現(xiàn)在回到主函數(shù),看看兩個(gè)類實(shí)例如何被獲取到,并如何傳遞給調(diào)用他們的閉包。

在這里,所有標(biāo)識(shí)都被重新整理,使得代碼片段更加可讀。

// 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>>>):
  ...
  /*
    全局變量的引用使用三個(gè)入口來放置。
  */
  %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 引用計(jì)數(shù)。
  */
  %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 的處理會(huì)更加復(fù)雜一些。
    alloc_box 創(chuàng)建了一個(gè)這個(gè)變量的引用數(shù)容器,變量將會(huì)存在這個(gè)容器的堆中。
    容器創(chuàng)建以后,將會(huì)創(chuàng)建一個(gè)可選變量,指向 c2,并且可選變量會(huì)存儲(chǔ)在容器里。容器會(huì)增加所包含值的技術(shù),正如下面看到的一樣,一旦容器被遷移,可選值就會(huì)被釋放。
    在這里,c2 的值將被存儲(chǔ)在這個(gè)可選值中,對象將暫時(shí)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 將被標(biāo)記為 tagged,并且變量變?yōu)?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
  /*
    創(chuàng)建一個(gè)閉包對象,綁定方法到參數(shù)中。
  */
  %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 的容器變量進(jìn)行釋放(使用 對應(yīng)匹配的 *_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
  /*
     加載原先存儲(chǔ)的閉包對象,增加強(qiáng)引用然后調(diào)用它。
  */
   %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
  ...
}

閉包有一個(gè)更加復(fù)雜的結(jié)構(gòu):

/*
  閉包參數(shù)被標(biāo)記為 @sil, 指定參數(shù)如何被計(jì)數(shù),有一個(gè) unowned 的 aClass 類變量 c2, 和另外一個(gè)包含 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
/*
  下面的函數(shù)包含三塊,后面兩塊的執(zhí)行依賴可選值 c2 具體的值。
*/
bb0(%0 : $@sil_unowned aClass, %1 : $@box @sil_weak Optional<aClass>):
...
  /*
    c1 被強(qiáng)計(jì)數(shù)。
  */
  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
  /*
    使用內(nèi)部自帶包,傳入一個(gè)整型字面量到整型結(jié)構(gòu)中,初始化了一個(gè)值為 42 的整型值。
    這個(gè)值將被設(shè)置為 c1 的新值,完成以后這個(gè)變量將會(huì)被釋放。
    在這里,我們第一次看到 class_method 指令,用于獲取 vtable 中的函數(shù)引用。
  */
  %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。
    獲取可選值,然后根據(jù)它的內(nèi)容執(zhí)行接下來的分支語句。

    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
  /*
    跳轉(zhuǎn)到閉包的結(jié)尾。
  */
  br bb3, loc "sample.swift":11:18, scope 16        // id: %14

// %15                                            // users: %21, %20, %19, %16
bb2(%15 : $aClass):                               // Preds: bb0
  /*
    調(diào)用 aClass 的 setter,設(shè)置它的值為 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 指令帶來的性能的差異點(diǎn),對不同階段每種類型的捕獲變量做一個(gè)快速的對比:

動(dòng)作 Unowned Weak
預(yù)先調(diào)用 #1 對象進(jìn)行 unowned_retain 操作 創(chuàng)建一個(gè)容器,并且對象進(jìn)行 strong_retain 操作。創(chuàng)建一個(gè)可選值,存入到容器中,然后釋放可選值
預(yù)先調(diào)用 #2 strong_retain_unowned,unowned_retain 和 strong_release strong_retain
閉包執(zhí)行 strong_retain_unowned,unowned_release load_weak, 打開可選值, strong_release
調(diào)用之后 unowned_release strong_release

正如上面看到的 SIL 代碼段那樣,處理 weak 引用會(huì)涉及到更多的工作,因?yàn)樾枰幚硪眯枰目蛇x值。

參照官方文檔的描述,這里對涉及到的所有 ARC 指令做一個(gè)簡要的解釋:

  • unowned_retain增加堆對象中的 unowned 引用計(jì)數(shù)。
  • strong_retain_unowned斷言對象的強(qiáng)引用計(jì)數(shù)大于 0,然后增加這個(gè)引用計(jì)數(shù)。
  • strong_retain增加對象的強(qiáng)引用計(jì)數(shù)。
  • load_weak不是真正的 ARC 調(diào)用,但是它將增加可選值指向?qū)ο蟮膹?qiáng)引用計(jì)數(shù)。
  • strong_release減少對象的強(qiáng)引用計(jì)數(shù)。如果釋放操作把對象強(qiáng)引用計(jì)數(shù)變?yōu)?,對象將被銷毀,然后弱引用將被清除。當(dāng)整個(gè)強(qiáng)引用計(jì)數(shù)和 unowned 引用計(jì)數(shù)都為0時(shí),對象的內(nèi)存才會(huì)被釋放。
  • unowned_release減少對象的 unowned 引用計(jì)數(shù)。當(dāng)整個(gè)強(qiáng)引用計(jì)數(shù)和 unowned 引用計(jì)數(shù)都為 0 時(shí),對象的內(nèi)存才會(huì)被釋放。

接下來深入到 Swift 運(yùn)行時(shí)看看,這些指令都是如何被實(shí)現(xiàn)的,相關(guān)的代碼文件有:HeapObject.cpp,HeapObject.hRefCount.hHeap.cpp、 SwiftObject.mm 中的少量定義。容器實(shí)現(xiàn)可以在 MetadataImpl.h 找到,但是本文不展開討論。

這些文件中定義大多數(shù)的 ARC 方法都有三種變體,一種是對 Swift 對象的基礎(chǔ)實(shí)現(xiàn),另外兩種實(shí)現(xiàn)是針對非原生 Swift 對象的:橋接對象和未知對象。后面兩種變體這里不予討論。

第一個(gè)討論指令集和 unowned 引用相關(guān)。

HeapObject.cpp 文件中間可以看到對 unowned_retainunowned_release 的實(shí)現(xiàn)方法:

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 的具體實(shí)現(xiàn),簡單地進(jìn)行 unowned 引用計(jì)數(shù)的原子增加操作(這里定義為weakRefCount),swift_unownedRelease 更加復(fù)雜,原因之前也描述過,當(dāng)沒有其他 unowned 引用存在時(shí),它需要執(zhí)行對象的析構(gòu)操作。

但是整體來講都不復(fù)雜,在這里可以看到 doDecrementShouldDeallocate 方法,這個(gè)方法在上面代碼中被一個(gè)命名類似的方法調(diào)用了。這個(gè)方法沒有做太多,swift_slowDealloc 只是釋放給定的指針。

到此已經(jīng)有了一個(gè)對象的 unowned 引用,另外一個(gè)指令,strong_retain_unowned 用來創(chuàng)建一個(gè)強(qiáng)引用:

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);
}

因?yàn)槿跻脩?yīng)該指向了這個(gè)對象,要使用斷言來驗(yàn)證對象是否被弱引用,一旦斷言通過,將嘗試進(jìn)行增加強(qiáng)引用計(jì)數(shù)的操作。一旦對象在進(jìn)程中已經(jīng)被釋放,嘗試將會(huì)失敗。

所有類似于 tryIncrement 通過某種形式修改引用計(jì)數(shù)的方法都放到 RefCount.h 中,需要使用原子操作進(jìn)行這些任務(wù)。

接下來討論下 weak 引用的的實(shí)現(xiàn),正如之前看到的那樣,swift_weakLoadStrong 用來獲取容器中可選值中強(qiáng)引用的對象。

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;
}

在這個(gè)實(shí)現(xiàn)中,獲取一個(gè)強(qiáng)引用需要更多復(fù)雜同步操作,在多線程競爭嚴(yán)重的情況下,會(huì)帶來性能損耗。

在這里第一次出現(xiàn)的 WeakReference 對象,是一個(gè)簡單的結(jié)構(gòu)體,包含一個(gè)整型值字段指向目標(biāo)對象,目標(biāo)對象是使用 HeapObject 類來承載的每一個(gè)運(yùn)行時(shí)的 Swift 對象。

在 weak 引用詢問當(dāng)前線程設(shè)置的 WR_READING 標(biāo)識(shí)之后,從 WeakReference 容器中獲取 Swift 對象,如果對象不再有效,或者在等待獲取資源時(shí),它變成可以進(jìn)行析構(gòu),當(dāng)前的引用會(huì)被設(shè)置為 null。

如果對象依然有效,獲取對象的嘗試將會(huì)成功。

因此,從這個(gè)角度來講,對 weak 引用的常規(guī)操作性能比 unowned 引用的更低(但是主要的問題還是在可選值操作上面)。

<a name="conclusion"></a>

結(jié)論

保守的使用 weak 引用是否明智呢?答案是否定的,無論是從性能的角度還是代碼清晰的角度而言。

使用正確的捕獲修飾符類型,明確的表明代碼中的生命周期特性,當(dāng)其他人或者你自己在讀你的代碼時(shí)不容易誤解。

<a name="footnotes"></a>

腳注

<a name="1"></a>
1、蘋果第一次討論 weak/unowned 爭議可以查看這里,之后在 twitter 上 Joe Groff 對此也進(jìn)行了討論,并且被 Michael Tsai 總結(jié)成文。
這篇文章從意圖角度出發(fā),提供了完整并且可操作的解釋。

<a name="2"></a>
2、維基百科中可以找到關(guān)于 AST 的解釋,還可以從 Slava Pestov 的這篇文章中看到關(guān)于 Swift 編譯器中如何實(shí)現(xiàn) AST 的一些細(xì)節(jié)。

<a name="3"></a>
3、*關(guān)于 SIL 的更多信息,請查看詳盡的官方 SIL 指南,還有 2015 LLVM 開發(fā)者會(huì)議的視頻。Lex Chou 寫的 SIL 快速指南可以點(diǎn)擊這里查看。 *

<a name="4"></a>
4、查看在 Swift 中如何進(jìn)行名稱粉碎(name mangling)的細(xì)節(jié),請查看 Lex Chou 的這篇文章。

<a name="5"></a>
5、Mike Ash 在他的 Friday Q&A 中的一篇文章中討論了如何實(shí)現(xiàn) weak 引用的一種實(shí)踐方法,這種方法與目前 Swift 的方法對比起來有一些過時(shí),但是其中的解釋依然值得參考。

本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請?jiān)L問 http://swift.gg。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容