Swift Reference Cycle中的weak,unowned,Closure Capture List

截圖Xcode版本:Xcode 10.1

如果您在用Swift做iOS開發,且暫時不是很清楚什么時候用weak、什么時候用unowned、或者不是很清楚什么是closure capture list,那么,此文尚值一讀。

TL;DR(太長不看版)

  • weak還是用unowned,和對象的lifetime(生命周期)有關;
  • 如果兩個對象的生命周期完全和對方沒關系(其中一方什么時候賦值為nil,對對方都沒影響),請用weak(來解決Reference Cycle);
  • 如果你有十足信心確保:其中一個對象銷毀,另一個對象也要跟著銷毀,這時候,可以(謹慎)用unowned(來解決Reference Cycle);
  • closure capture list,是在closures(閉包)內,把capture(捕抓)到的對象、值,放到一個方括號中的語法。在方括號(capture list)中,可以利用weak、unowned關鍵字把默認的strong reference 改為非strong reference,從而解決closures和類實例(class instance)之間的Reference Cycle;
  • Xcode 8 推出的工具Debug Memory Graph可以在App運行時十分方便定位到產生Reference Cycle的代碼。

ARC定義

上面的關鍵字,都和Swift的內存管理機制ARC(Automatic Reference Counting/自動引用計數 )有關,而且都是在解決Reference Cycle(引用循環)需要用到的關鍵字。

Swift的官方文檔Automatic Reference Counting中并沒有對ARC進行定義,但是可以參考Objective-C中關于ARC的定義,因為Objective-C中的ARC和Swift的非常相似(very similar)。

Automatic Reference Counting (ARC) is a compiler feature that provides automatic memory management of Objective-C objects.

從定義可知,ARC是編譯器提供的一個特性,用于自動管理內存。

結論就是:在大部分情況下,開發者無需操心內存管理的事情:

In most cases, this means that memory management “just works” in Swift, and you do not need to think about memory management yourself.

不過,剩下的這「小部分」情況,也夠大家頭大的……

這「小部分」情況是什么呢,就是Reference Cycle。

用weak解決Reference Cycle

Reference Cycle是什么

什么是Reference Cycle、Reference Cycle有什么危害?

因為官方文檔舉例用了Person和Apartment兩個classes,所以這里舉個可能不太恰當的例子:

想象一下,房地產商在北京建了一套房子Apartment,然后出租給一個租客Tenant。突然某天,晴天一個霹靂,租客意外掛了,同時房地產商又接了P2P暴雷的接力棒——也暴了。這時候,你把這個Apartment想象成電腦中的一塊內存,因為知道這個Apartment存在的兩方都被導演安排去領飯盒了,這個Apartment就白白浪費在城市中了,如果陸續出現很多這種情況,這個城市很多房產就浪費掉了——好比如電腦中的內存被浪費掉。

上面的情況,可以把它簡單理解為Reference Cycle,它會導致內存浪費——內存浪費到一定程度,你的程序可能會crash,所以要避免。下面用官方文檔的圖示進一步闡述:

image

▲1. 左邊是我們潛在租客(Tenant)john,右邊是我們房地產商新建的一個Apartment,起了個很洋氣的名字unit4A。可以看到,john還沒租到房子——apartment屬性為nil;房子unit4A也還沒找到租客——tenant屬性為nil,大家各不相干。

image

▲2. 第二張圖可以看到,apartment屬性和tenant屬性都有值了,而且中間多了兩個strong的箭頭,表示他們的關系。可以把它們理解成租客和房東簽訂了合同,確立了租賃關系(但是這個「合同」是有問題的,會導致Reference Cycle)。

image

▲3. 第三張圖,我們看到,租客和房地產商都被導演安排去領飯盒了——都被賦值了nil(上面的兩個strong箭頭不見了)。不過因為他們之前簽的合同沒有第三方知道,所以大家都以為這個房子還在住人,導致房子沒有流回租賃市場,造成浪費。

以上用了一個不太恰當的比喻描述Reference Cycle。

而在Xcode的debug工具Debug Memory Graph,則是用圖片這樣描繪的:

image

感覺挺形象的(后面會說明Debug Memory Graph的簡單用法)

weak 關鍵字

那怎么解決呢?用weak這個關鍵字,繼續看圖示:

image

▲4. 這張圖和圖2有個小區別,就是下面的strong箭頭變成成了灰色的weak。打個比方,他們重新簽訂合同,規定租客兩個月不交租的話,就失去房子的租賃權,要被回收、再出租。

image

▲5 . 一語中的,租客john真的狗帶了(被賦值為nil),同時他對Apartment的strong reference也隨之消失。而Apartment指向Person實例的是weak reference,不持有Person實例,所以 tenant重設為nil。房子可以重新出租給其他人。但是,如果這時候房地產商也暴雷倒閉了,就出現以下情況:

image

▲6 .房子現在成為無主孤魂了——房地產商不持有,租客也不持有。所以超級管理員——政府就知道可以回收再利用了。

上面舉例說明了類實例之間的Reference Cycle和其「解決」方法——用weak關鍵字修飾屬性,下面看官方文檔的代碼:

// 這種寫法,會引起Reference Cycle,因為大家都是strong reference,互相持有對方,最后得不到釋放。
class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

// 修正:Apartment改為這種寫法,即可解決Reference Cycle
class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    // 具體就是改了這里,tenant屬性,用weak關鍵字修飾 
    weak var tenant: Person? // 因為tenant有可能會是nil,所以是Optional Type,可以理解為,房子不一定有租客。(weak修飾的,一定是Optional Type)
    deinit { print("Apartment \(unit) is being deinitialized") }
}

用unowned解決Reference Cycle

再舉個不恰當的例子:

想象一下,我們有「Customer/客戶」和「CreditCard/信用卡」兩個類。這種情況和「租客」和「房子」的不同點在于,「租客」和「房子」都可以作為獨立的存在,它們的lifetime(生命周期)沒有跟對方沒有直接的因果關系。而「客戶」和「信用卡」的關系則不同:「客戶」可以單獨存在,「信用卡」不行。「信用卡」被創造出來的前提是——肯定先有「客戶」(聯想一下現實生活:銀行都是在用戶申請信用卡之后才制卡的,不可能預先制造一堆卡——因為卡上要印「客戶」的名字)。所以,「客戶」的lifetime(生命周期)一定是和「信用卡」一樣、或者更長的。

怎么表達這種關系呢?Swift中用的是unowned關鍵字:

class Customer {
    let name: String
    var card: CreditCard? // 「客戶」不一定有「信用卡」,所以這里是Optional Type
    init(name: String) {
        self.name = name
    }
    deinit { print("\(name) is being deinitialized") }
}

class CreditCard {
    let number: UInt64 
    // 這里用unowned,因為「客戶/Customer」和「信用卡」的lifetime一樣,或者比「信用卡」更長
    unowned let customer: Customer // 有「信用卡」,就一定有「客戶」,所以這里不能用Optional Type(nonoptional)
    // 有「客戶」才能創建「信用卡」,所以init方法,要傳Customer參數
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card #\(number) is being deinitialized") }
}

這時候的圖示如下:

image

▲7 .比起上面「租客」和「房子」的關系,右邊「信用卡」這個instance,少了一個strong refrence指向它。

之所以叫unowned,可能是因為「Customer」可以擁有(own)「CreditCard」,但是「CreditCard」不能擁有(does not own )「Customer」(或者是:除了指定「Customer」這個owner外,不可以有其他owner,Who knows?)。

小結weak和unowned

個人總結兩者的異同:

  • 相同點

    weakunowned都可以解決Reference Cycle,所以他們相同的地方:

    • 都不會對object進行reference count(引用計數)加1的操作
    • 都可以解決reference cycle這個問題(這句好像有點廢)
  • 不同點

    • weak修飾的屬性,只能是變量(var),同時只能是Optional類型,因為在模擬實際情境中,這個屬性有可能是沒有具體值的。換言之你需要手動檢查解包后才能使用——所以朝陽群眾說這樣更安全;
    • unowned修飾的屬性,不能是Optional類型(一定是nonoptional類型),(想象一樣,銀行肯定要有了「客戶」之后,才能制作該「客戶」的「信用卡」);
    • weak屬性,初始化后也可以為nil;
    • unowned屬性,初始化后一定都有值;
    • weakunowned更安全(原因見「不同點」第一條);
    • unownedweak性能好一點點(出處——倒數第二段)

下面這張插圖,比較直觀描繪出strong、weak、unowned在屬性聲明時的異同(圖片來源:ARC and Memory Management in Swift):

image

那么,問題來了,我究竟什么時候用weak,什么時候用unowned?

官方文檔給出的答案是:

Unlike a weak reference, however, an unowned reference is used when the other instance has the same lifetime or a longer lifetime.

對于什么時候用unownedWhen to use strong, weak and unowned reference types in Swift and why一文給出類似的答案:

The rule here is to use it if we can guarantee that the lifecycle of the referenced object is equal or greater than the lifetime of the variable pointing to it. In that case we know for sure that the object will not be deallocated and we can safely use it.

上面用對象的「lifetime/生命周期」來解釋,相對抽象,感覺也不好判斷,在具體實踐中或許可以這樣判斷:

  • 當兩個屬性在實際情況中都允許是nil的時候(「Person」中的「apartment」,「Apartment」中的「tenant」,初始化后,都可以為nil):用weak;
  • 當一個屬性允許是nil(「Customer」中的屬性「card」),另一個屬性不允許是nil(「CreditCard」中的「customer」,「CreditCard」初始化成功后,屬性customer一定要有值):就用unowned。

什么?你現在還像我一樣黑人問號?那可以簡單點:當你不知道用weak還是用unowned的時候,用weak吧。為什么?因為群眾說weak更安全——畢竟安全第一。

補充:用unowened + Implicitly Unwrapped Optional解決Reference Cycle

上面說了兩種情況:

  • 兩個屬性同時允許是nil;
  • 一個屬性允許是nil,另一個不允許是nil。

官方文檔還描述了第三種情況:兩個屬性都不允許是nil——初始化完成后,一定都要有值。(官方文檔舉例:「Country/國家」一定會有「capitalCity/首都」,「capitalCity」也一定會有它所在的「Country」)

class Country {
    let name: String
    var capitalCity: City! // 用Implicitly Unwrapped Optional的方式(就是加個感嘆號),表示初始化后屬性一定有值,不為nil(備注:還是Optional類型,初始化前的默認值也是nil)
    init(name: String, capitalName: String) {
        self.name = name
        // 其實到這里為止,就算是初始化完成了,因為name賦值了,capitalCity也有默認值nil。所以下面這句不寫也不會報錯。另外,因為初始化完成,所以可以調用selfe了
        // 下面這句,是為了滿足實際初始化需求:初始化結束后,capitalCity一定有值
        self.capitalCity = City(name: capitalName, country: self)
    }
}

class City {
    let name: String
    // 這里和上面的unowned用法一致
    unowned let country: Country
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}

// 這樣一句代碼,就可以創建兩個實例了(而且他兩的lifetime都一樣:同時創建、同時銷毀——所以可以用unowned)
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")

上述情況,就是用unowenedImplicitly Unwrapped Optional解決Reference Cycle。

Implicitly Unwrapped Optional就是在聲明capitalCity這個Optional屬性時,加上嘆號,用來表示初始化后一定有值(「國家」建立了,就一定要有「首都」啊),并且后面也可以不解包直接訪問。

用capture list解決Reference Cycle

Closures(閉包)和class instance(類實例)之間,也有可能產生Reference Cycle,這種情況用capture list解決。

在講Closures中的Reference Cycle前,先明確以下幾點:

  • Closures是Reference Type——所以才有可能產生Reference Cycle

  • 在Closures內,使用Closures外的常量、變量,這種行為被定義為「capture」,有以下幾種語法表現:

    // 如果什么都不寫,直接使用。默認是strong類型的capture(想象一下,這時候就有一個粗粗的箭頭指向self)
    // 下面這句,意思就是把title實例capture到closure里來用(為什么強制寫self,下面解釋)
    myFunction { print(self.title) }                    // implicit strong capture
    
    // 把capture的對象放在方括號,是顯式地聲明capture行為,也是strong類型的capture
    myFunction { [self] in print(self.title) }          // explicit strong capture
    
    // 顯式地聲明capture回來的實例,是weak類型的reference
    // 因為weak reference只能是optional類型,所以使用時要解包處理(感嘆號強制解包)
    myFunction { [weak self] in print(self!.title) }    // weak capture
    
    // 顯式地聲明capture回來的實例是unowned類型的reference
    myFunction { [unowned self] in print(self.title) }  // unowned capture
    
    • 上面closures的第一種寫法,在closure內,使用外面的title,Swift強制要加上self,否則編譯報錯。原因是為了提醒用戶,這里「capture」了self的實例,有可能會造成Reference Cycle,要多加注意。
    • 方括號內,可以放多個值;
    • Closures內方括號放若干個值,這種語法,叫做「Capture List」;
    • 如果顯式地把「Capture List」寫出來,就一定要和in關鍵字搭配使用——即使Closures中沒有參數、沒有返回值;
    • 對于Value Type,顯式地用方括號capture回來的值,會copy一份到closures里面(是不能修改的let常量),這時候和原來外面的值就沒關系了;如果不是寫在「Capture List」里,closures內外就共享一個值;
    • 而對于Reference Type,無論是否顯式地寫「Capture List」,指向的都是同一個Reference;「Capture List」的作用,是用于聲明是weak,還是unowned類型的Reference。
    • Closures、classes實例之間的Reference Cycle,就是用這種方法(Capture List)來解決的。

先看看Closures、classes實例之間的Reference Cycle長啥樣:

image

▲ 這是官方文檔的示意圖。可以看到,實例化一個HTMLElement對象后:asHTML屬性指向closure,而closure因為capture了self,也指向HTMLElement對象(self),最后造成Reference Cycle。

image

再看看Xcode中Debug Memory Graph描繪出來的圖示,也很形象,有一個箭頭,跑了一圈,又回到了HTMLElement對象自身。

而在代碼中,表現是這樣的:

class HTMLElement {
    
    let name: String
    let text: String?

    lazy var asHTML: () -> String = {
        // 如果沒有寫capture list(方括號內加若干屬性),默認是strong reference的。
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

// 這時候只要創建HTMLElement實例,就會Reference Cycle
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
paragraph = nil // 賦值為nil,也不會調用deinit()銷毀對象

而解決辦法,就是上面說的Capture List

class HTMLElement {

    let name: String
    let text: String?

    // 在closure里面,用Capture List,將默認的Strong Reference,聲明為不增加Reference Count的unowned self(當然,用weak self也有一樣的效果,下面說明具體區別)
    // 注意,用Capture List,后面就一定要用in
    lazy var asHTML: () -> String = {
        [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

看上述代碼,用[unowned self],把原來默認的strong reference手動改為unowned referenc,即可解決問題。

而關于用weak還是unowned,和class實例之間的Reference Cycle類似。你能確保closure和它capture回來的對象一直引用對方(初始化后一直有值,不可能為nil)、并且會同時銷毀,就用unowned;如果closure capture回來對象,有可能在某一時刻會變成nil(有可能為nil),就用weak

什么?也還是不明白?那就不負責任地說一句:用weak吧~

Debug Memory Graph

Debug Memory Graph是Xcode 8開始有的一個新工具,將內存中的對象可視化。致力于回答一個問題:

Why does this object exist?

這個工具可以很方便地幫你檢查出項目中可能存在的內存問題,也是檢查是否有Reference Cycle的神器,具體應用可看如下圖示:

image

WWDC2016: Visual Debugging with Xcode 24:40有詳細介紹Debug Memory Graph

Reference

When to use strong, weak and unowned reference types in Swift and why

strong, weak, unowned - Reference Counting in Swift

Memory Management in Swift: Understanding Strong, Weak and Unowned References

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容