ARC內存管理以及循環引用

ARC:"Automatic Reference Counting",自動引用計數。Swift語言延續了OC的作法,也是利用ARC機制進行內存管理,和OC的ARC同樣,當一些類的實例不在須要的時候,ARC會釋放它們的內存。可是,在少數狀況下,ARC須要知道你的代碼之間的關係才能更好的爲你管理內存,和OC同樣,Swift中的ARC也存在循環引用致使內存泄露的狀況。html

1、ARC的工做機制

每當咱們建立一個類的新的實例的時候,ARC會從堆中分配一塊內存用來存儲有關該實例的信息。這塊內存將持有這個實例的類型信息以及和它關聯的屬性的值。另外,當這個實例再也不被須要的時候,ARC將回收這個實例所佔有的內存而且將這部份內存給其餘須要的實例用。這樣就能保證再也不被須要的實例不佔用多餘的內存。 可是,若是ARC釋放了正在使用的實例,那麼該實例的屬性將不能被訪問,方法將不能被調用,若是你訪問它的屬性或者調用它的方法時,應用會崩潰,由於你訪問了一個野指針。 爲了解決上述問題,ARC會跟蹤每一個類的實例正在被多少個屬性、常量或者變量引用,每當你將類實例賦值給屬性,常量或者變量的時候它就會被"強"引用一次,當它的引用計數爲0時,代表它再也不被須要,ARC就會銷燬它。 下面舉個例子介紹ARC是如何工做的bash

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}
複製代碼

上述代碼建立了一個名爲Person的類,該類聲明瞭一個非可選的類型的name常量,一個給name賦值的初始化方法,而且打印了一句話,用來標註初始化成功,同時聲明瞭一個析構函數,打印了一句標誌此實例被銷燬的信息。閉包

var reference1: Person?
var reference2: Person?
var reference3: Person?
複製代碼

上述代碼聲明瞭三個Person?類型的變量,這三個變量爲可選類型,因此被自動初始化爲nil,此時三個實例都沒有指向任何一個Person類的實例。app

reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
複製代碼

如今建立一個Person類的實例,而且賦值給reference1,此時控制檯會打印"John Appleseed is being initialized"函數

reference2 = reference1
reference3 = reference1
複製代碼

而後將該實例賦值給reference2reference3。如今該實例被三個"強"類型的指針引用。ui

reference1 = nil
reference2 = nil
複製代碼

如上所示,當咱們將其中兩個引用賦值給nil的時候,這兩個"強"引用被打破,可是這個Person的實例並無被釋放(釋放信息未打印),由於還存在一個對這個實例的強引用。spa

reference3 = nil
// Prints "John Appleseed is being deinitialized"
複製代碼

當咱們將第三個"強"引用打破的時候(賦值爲nil),能夠看到控制檯打印的"John Appleseed is being deinitialized"析構信息。3d

2、兩個類實例之間的循環引用

上述的例子中,ARC能夠很好的獲取一個實例的引用計數,而且當它的引用計數爲0的時候釋放它。可是在實際的開發過程當中,會存在一些特殊狀況,使ARC沒辦法獲得引用計數爲0這個關鍵點,就會形成這個實例的內存一直不被釋放,兩個類的實例相互"強"引用就會形成這種狀況,就是"循環引用"。 蘋果官方提供了兩種方法來解決兩個實例之間的循環引用,unowned引用和weak引用。指針

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") }
}
複製代碼

這個例子,定義了一個Person類和一個Apartment類。每個Person的實例都有一個name的屬性和一個apartment的可選屬性,初始化爲nil,由於並非每個人都擁有一個公寓,因此是可選屬性。一樣的,每個Apartment實例都有一個unit屬性和一個tenant的可選屬性,初始化爲nil,同理,不是每個公寓都有人租。同時,兩個類都定義了deinit方法,而且打印一段信息,用來讓咱們清楚這個實例什麼時候被銷燬。code

var john: Person?
var unit4A: Apartment?
複製代碼

分別定義一個Person類型和Apartment的變量,定義爲optional(可選類型),初始化爲nil

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
複製代碼

而後分別建立一個Person類的實例和Apartment類的實例,而且分別賦值給上面的定義的變量。

上圖爲此時變量和實例之間的強引用關係。 而後 john將擁有一座公寓 unit4A,公寓 unit4A將被 john承租。

john!.apartment = unit4A
unit4A!.tenant = john
複製代碼

由於能夠肯定兩個變量都被賦值爲相應類型的實例,因此此處用!對可選屬性強解包。 此時,兩個變量和實例以及兩個實例之間的"強"引用關係以下圖。

從圖中能夠看到兩個實例互相"強"引用,也就是說這兩個實例的引用計數永遠不會爲0,ARC也不會釋放這兩個實例的內存。

john = nil
unit4A = nil
複製代碼

當咱們將兩個變量設置爲nil,切斷他們與實例之間的"強"引用關係,此時兩個實例之間的"強"引用關係爲:

從圖中能夠看出,這兩個實例的引用計數仍然不爲0,它們佔用的內存仍是得不到釋放,所以就會形成內存泄露。

3、解決兩個類實例之間的循環引用

Swift提供了兩種辦法解決類實例之間的循環引用。weak引用和unowned引用。這兩種方法均可以使一個實例引用另外一個實例的時候,不用保持"強"引用。weak通常應用於其中一個實例具備更短的生命週期,或者能夠隨時設置爲nil的狀況下;unowned用於兩個實例具備差很少長的生命週期,或者說兩個實例都不能被設置爲nil

(1) weak引用

weak引用對所引用的實例不會保持"強"引用的關係。假如一個實例同時被若干個"強引用"和一個weak引用引用時,當全部其餘的"強"引用都被打破時該實例就會被ARC釋放,而且ARC會自動將這個weak引用置爲nil。所以,weak引用通常被聲明爲var,由於它會被ARC設置爲nil

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 }
    weak var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}
複製代碼

如今,咱們將Apartment類中的tenant變量聲明爲weak引用(在var關鍵字前加weak關鍵字),代表某公寓的承租人並不必定一直都是同一我的。

var john: Person?
var unit4A: Apartment?
 
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
 
john!.apartment = unit4A
unit4A!.tenant = john
複製代碼

而後和上文同樣,將兩個變量和實例關聯。此時,它們之間的引用關係以下圖。

Person實例仍然"強"引用 Apartment實例,可是 Apartment實例'weak'引用 Person實例。 johnunit4A兩個變量仍然"強"引用兩個實例。當咱們把 john變量對 Person實例的"強"引用打破的時候,即將 john設置爲 nil,就沒有其餘的"強"引用引用 Person實例,此時, Person實例被ARC釋放,同時 Apartment實例的 tenant變量被設置爲 nil

john = nil
// Prints "John Appleseed is being deinitialized"
複製代碼

而後將變量 unit4A設爲 nil,能夠看到 Apartment實例也被銷燬。

unit4A = nil
// Prints "Apartment 4A is being deinitialized"
複製代碼

(2) unowned引用

weak引用同樣,unowned引用也不會保持它和它所引用實例之間的"強"引用關係,而是保持一種非擁有(或未知)的關係,使用的時候也是用unowned關鍵字修飾聲明的變量。不一樣的是,兩個互相引用的對象具備差很少長的生命週期,而不是其中一個能夠提早被釋放(weak),有點同甘共苦的意思。 Swift要求unowned修飾的變量必須一直指向一個實例,而不是有些時候爲nil,所以,ARC也不會將這個變量設置爲nil,因此咱們通常將這個引用聲明爲非可選類型。PS:請確保你聲明的變量一直指向一個實例,若是這個實例被釋放了,而unowned變量還在引用它的話,你會獲得一個運行時錯誤,由於,這個變量是非可選類型的。

class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    deinit { print("\(name) is being deinitialized") }
}
 
class CreditCard {
    let number: UInt64
    unowned let customer: Customer
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card #\(number) is being deinitialized") }
}
複製代碼

上面這個例子定義了兩個類:CustomerCreditCard,每一個顧客均可能會有一張信用卡(可選類型),每一個信用卡都必定會有一個持有他們的顧客(非可選類型,卡片爲顧客定製)。所以,Customer類有一個CreditCard?類型的屬性,CreditCard類也有一個Customer類型的屬性,而且被聲明爲unowned,以此來打破循環引用。每張信用卡初始化的時候都須要一名持有它的顧客,由於信用卡自己就是爲顧客定製的。

var john: Customer?
複製代碼

而後聲明一個Customer?類型的變量john,初始化爲nil。接着建立一個Customer的實例,而且將它賦值給john(讓john引用它、指向它都是一個意思)。

john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
複製代碼

(第一句代碼賦值以後,咱們知道john確定不是nil,因此用!解包不會有問題) 而後,兩個實例之間的引用關係爲:

Customer實例"強"引用 CreditCard實例, CreditCard實例'unowned'引用 Customer實例,接着,咱們將 johnCustomer實例的"強"引用打破,即將 john設置爲 nil

john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
複製代碼

能夠看到 Customer實例和 CreditCard實例都被銷燬了。 john被設置爲 nil以後,就沒有"強"引用引用 Customer實例,因此, Customer實例被釋放,也就沒有"強"引用引用 CreditCard實例,所以 CreditCard實例也被釋放。 以上例子證實,兩種方式均可以解決循環引用的問題,可是要注意它們使用的範圍。weak修飾的變量能夠被設置爲nil(引用的實例的生命週期短於另外一個實例),unowned修飾的變量必需要指向一個實例(形成循環引用的兩實例的生命週期差很少長,不會出現一方被提早釋放的狀況),一旦它被釋放了,就千萬別再使用了。

4、閉包引發的循環引用

Swift中的閉包是一種獨立的函數代碼塊,它能夠像一個類的實例同樣在代碼中賦值、調用和傳遞,也能夠被認爲某個匿名函數的實例,其實就是OC中的block。它和類同樣也是引用類型的,因此它的函數體中使用的引用都是"強"引用。

class HTMLElement {
    
    let name: String
    let text: String?
    
    lazy var asHTML: () -> String = {
        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")
    }
    
}
複製代碼

上述例子中,閉包被賦值給asHTML變量,因此閉包被HTMLElement實例"強"引用,而閉包又捕獲(關於閉包捕獲變量,參考官方文檔Capturing Values)了HTMLElement的實例中的textname屬性,所以它又"強"引用HTMLElement實例,這樣就形成了循環引用,由於text屬性可能爲空,因此定義爲可選屬性。

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
複製代碼

咱們建立一個HTMLElement實例,並將它賦值給paragraph變量,而後訪問它的asHTML屬性。此時的內存示例爲下圖,能夠看到HTMLElement實例和閉包之間的循環引用。

當咱們將 paragraph 設置爲 nil時,控制檯並無打印任何銷燬信息,由於循環引用。
上圖爲使用 Instruments分析獲得的循環引用以及形成的內存泄漏。

5、使用unowned和weak解決循環引用

經過上文(三)的分析,咱們知道unowned引用對實例的非擁有關係,所以,咱們能夠經過以下方式解決循環引用:

lazy var asHTML: () -> String = {
        [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }
複製代碼

[unowned self] in,這段代碼,表明閉包中的self指針都被unowned修飾。這樣就可使閉包對實例的"強"引用變成'unowned'引用,從而打破循環引用。 當HTML的element爲標題的時候,此時若是text屬性爲空,咱們想返回一個默認的text做爲標題,而不是隻有<h/>這種標籤。

let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
    return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// Prints "<h1>some default text</h1>"
複製代碼

這段代碼也會形成HTMLElement對其自身的循環引用。咱們仍然可使用unowned關鍵字打破循環引用:

heading.asHTML = {
    [unowned heading] in
    return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
// Prints "<h1>some default text</h1>"
// Prints "h1 is being deinitialized"
複製代碼

unowned會使閉包中對heading的"強"都改成'unowned'引用。 或者,可使用weak屬性打破循環引用:

weak var weakHeading = heading
heading.asHTML = {
    return "<\(weakHeading!.name)>\(weakHeading!.text ?? defaultText)</\(weakHeading!.name)>"
}
// Prints "<h1>some default text</h1>"
//Prints "h1 is being deinitialized"
複製代碼

上文(三)中可知,weak修飾的變量爲可選類型,並且,咱們對變量進行了一次賦值,就能夠確保weakHeading指向heading引用的實例,因此能夠放心的使用!對它解包。 上面這段代碼一樣可使閉包對HTMLElement實例的"強"引用變爲weak引用,從而打破循環引用。 (ARC會自動回收不被使用的對象,因此不用手動將變量設置爲nil

本文參考Automatic Reference Counting

相關文章
相關標籤/搜索