Swift 中的內(nèi)存管理 —— 看看 ARC

Swift 作為一門現(xiàn)代的高級(jí)語言,自然少不了自動(dòng)內(nèi)存管理。我們知道,JAVA 中的內(nèi)存管理是通過垃圾回收完成的。作為從 Objective-C 時(shí)代演變過來的 Swift,自然也繼承了很多 OC 的特性。OC 中的內(nèi)存管理分為兩種:一種是垃圾回收,這種內(nèi)存管理方式只被運(yùn)用于 OS X(現(xiàn)在應(yīng)該叫 macOS)中;另一種就是我們經(jīng)常提到的 ARC(自動(dòng)引用計(jì)數(shù)),這種管理方式被同時(shí)用在 iOS 和 macOS 上。在 Swift 中,內(nèi)存管理依然是通過 ARC 進(jìn)行的,下面我們就深入地看一看。

什么是 ARC?

ARC,中文名叫自動(dòng)引用計(jì)數(shù),全名叫 Automatic Reference Counting。顧名思義,意思就是“自動(dòng)的”引用計(jì)數(shù)。

在我們深入了解 ARC 之前,我們先看一看什么叫做引用計(jì)數(shù)。我們先通過一個(gè)類來看一看 Swift 中對(duì)象在內(nèi)存中的各個(gè)階段。

假如我們有一個(gè) Person 類:

class Person {
    var name: String

    init(name: String) {
        self.name = name
        print("Person \(name) has been initialized.")
    }

    deinit {
        print("Person \(name) has been deallocated.")
    }
}

這個(gè)簡單的 Person 類,我們給它指定了一個(gè) name 的屬性,同時(shí)提供了 init 構(gòu)造方法和 deinit 析構(gòu)方法。在構(gòu)造方法中,我們讓控制臺(tái)輸出一條語句,提醒我們某個(gè)對(duì)象已經(jīng)初始化。同時(shí)在析構(gòu)方法中,我們同樣讓控制臺(tái)輸出某個(gè)對(duì)象的內(nèi)存已經(jīng)釋放。

首先我們創(chuàng)建一個(gè) person1 對(duì)象,然后看看控制臺(tái)的輸出:

do {
    let person1 = Person(name: "Kenneth")
}
控制臺(tái)

可以看到在 do 語法塊內(nèi),我們創(chuàng)建的對(duì)象先進(jìn)行了初始化,然后運(yùn)行完畢后釋放了內(nèi)存。

Swift 中一個(gè)對(duì)象的生存周期也正如此,包含:

  1. 內(nèi)存分配 (從棧或者堆中獲得內(nèi)存)
  2. 初始化 (執(zhí)行 init 函數(shù))
  3. 使用 (對(duì)象的使用)
  4. 析構(gòu)(執(zhí)行析構(gòu)函數(shù))
  5. 內(nèi)存釋放 (內(nèi)存返回到?;蛘叨褍?nèi))

ARC 正是基于這個(gè)周期幫助我們管理內(nèi)存的。當(dāng)我們創(chuàng)建一個(gè)對(duì)象,保存一個(gè)對(duì)這個(gè)對(duì)象的引用,ARC 就一直監(jiān)視我們對(duì)于該對(duì)象的引用,并記下當(dāng)前有多少個(gè)對(duì)于該對(duì)象的引用。當(dāng)引用數(shù)量 >0 時(shí),內(nèi)存將不會(huì)被釋放。當(dāng)引用數(shù)量等于 0 時(shí),ARC 認(rèn)為該對(duì)象不再被需要,于是便自動(dòng)地幫助我們銷毀該對(duì)象,并釋放該對(duì)象的內(nèi)存。

ARC

ARC 失效?

在大多數(shù)情況下,ARC 工作地很好,這也讓我們不需要對(duì)內(nèi)存管理花太多心思。ARC 遵循的原則簡單而且有效,但它并不是萬能的。在 ARC 的管理下,內(nèi)存泄漏仍然可能發(fā)生。

讓我們想象這樣一個(gè)情景:我們有兩個(gè)對(duì)象,這兩個(gè)對(duì)象不再被需要,但是這兩個(gè)對(duì)象互相引用了對(duì)方。

在上述情境中我們實(shí)際上不需要這兩個(gè)對(duì)象,但是因?yàn)檫@兩個(gè)對(duì)象互相引用了對(duì)方,這兩個(gè)對(duì)象的引用計(jì)數(shù)都是 1。因?yàn)檫@兩個(gè)對(duì)象的引用計(jì)數(shù)都是 1,ARC 將永遠(yuǎn)不會(huì)釋放這兩個(gè)對(duì)象的內(nèi)存。

在這種情況下,內(nèi)存泄漏就不可避免地發(fā)生。我們把這種情況叫做引用循環(huán),更準(zhǔn)確的說,是強(qiáng)引用循環(huán)。

強(qiáng)引用循環(huán)

讓我們用一段代碼來演示這個(gè)場(chǎng)景。

首先我們新建一個(gè)簡單的 iPhone 類,這個(gè)類有兩個(gè)屬性,一個(gè)是型號(hào),一個(gè)是主人:

class iPhone {
    let model: String
    var owner: Person?
    
    init(model: String) {
        self.model = model
        print("iPhone \(model) has benn initialized.")
    }
    
    deinit {
        print("iPhone \(model) has benn deallocated")
    }
}

然后我們?cè)?do 語法塊里面新建一個(gè) iPhone 對(duì)象:

do {
    let person1 = Person(name: "Kenneth")
    let iphone1 = iPhone(model: "7 Plus")
}

然后我們看控制臺(tái)的輸出語句,這個(gè)時(shí)候 ARC 工作正常,兩個(gè)對(duì)象都在運(yùn)行完成后被釋放了內(nèi)存:

控制臺(tái)

下面我們?cè)?Person 類中新建一個(gè)屬性,這個(gè)屬性的類型是一個(gè) iPhone 類的數(shù)組,表示該人擁有的所有 iPhone。同時(shí)我們將該屬性的 setter 方法設(shè)為私有,這樣強(qiáng)制了使用我們自定義的方法進(jìn)行 set。

private(set) var iphones: [iPhone] = [iPhone]()
    func add(iphone: iPhone) {
        iphones.append(iphone)
        iphone.owner = self
    }

然后我們?cè)?do 語法塊中添加一條語句:

do {
    let person1 = Person(name: "Kenneth")
    let iphone1 = iPhone(model: "7 Plus")
    person1.add(iphone: iphone1)
}

這時(shí)候我們?cè)倏纯刂婆_(tái):

少了兩條內(nèi)存釋放的控制臺(tái)

可以看到這個(gè)時(shí)候的控制臺(tái)少輸出了兩天釋放內(nèi)存的提示。這說明我們剛才的這兩個(gè)對(duì)象并沒有被釋放。

為什么?

就像我們上面所說的一樣,這個(gè)時(shí)候發(fā)生了強(qiáng)引用循環(huán)。我們的 person1 對(duì)象中的 iphones 屬性保有了 iphone1 對(duì)象,而我們 iphone1 中的 owner 屬性又保有了 person1。這個(gè)就是我們所說的強(qiáng)引用循環(huán)。

破解之道?

弱引用(Weak Reference)

為了打破所謂的強(qiáng)引用循環(huán),我們開始思考,有沒有一種辦法能夠讓我們同時(shí)可以進(jìn)行引用但是又不增加引用計(jì)數(shù)呢?弱引用就是這樣誕生的。

所謂弱引用,就是很弱的引用——它引用一個(gè)對(duì)象而不增加該對(duì)象的引用計(jì)數(shù)。

弱引用

上圖中,虛線箭頭表示一個(gè)弱引用。可以看到對(duì)象 2 弱引用對(duì)象 1 時(shí),對(duì)象 1 的引用計(jì)數(shù)并沒有增加。

當(dāng)變量 1 和變量 2 不存在后,對(duì)象 1 的引用計(jì)數(shù)變成 0,對(duì)象 1 會(huì)被 ARC 銷毀,銷毀后對(duì)象 1 對(duì)對(duì)象 2 的引用也不存在,對(duì)象 2 也會(huì)被 ARC 銷毀。強(qiáng)引用循環(huán)就這樣被打破了。

針對(duì)上一段的代碼,我們只要在 iPhone 類的 owner 屬性前加一個(gè) weak,即可以把該引用改為弱引用:

weak var owner: Person?

這時(shí)候我們?cè)倏纯刂婆_(tái):

控制臺(tái)

可以看到這個(gè)時(shí)候兩個(gè)對(duì)象的內(nèi)存都會(huì)被正常釋放,弱引用起作用了。

你可能會(huì)問,那我們保存的弱引用這個(gè)時(shí)候指向了什么呢?我們回過頭來看一看 owner 屬性的類型,實(shí)際上是一個(gè) Optional。聰明的你肯定能猜到,弱引用指向的對(duì)象如果內(nèi)存被釋放,會(huì)自動(dòng)變?yōu)榭找茫簿褪?nil。

無主引用(Unowned Reference)

弱引用 weak 作為解決強(qiáng)引用循環(huán)的一個(gè)關(guān)鍵字,它將會(huì)在被引用對(duì)象的內(nèi)存釋放后變?yōu)?nil。Swift 中還提供了另外一個(gè)關(guān)鍵字——unowned,它也能達(dá)到引用對(duì)象而不增加其引用計(jì)數(shù)的效果。和 weak 關(guān)鍵字不同的是,weak 關(guān)鍵字會(huì)在對(duì)象內(nèi)存釋放后變?yōu)?nil,而 unowned 會(huì)繼續(xù)保有對(duì)該對(duì)象的引用,即使該引用已經(jīng)變?yōu)闊o效。

在上一段,我們知道 weak 關(guān)鍵字描述的屬性必須申明為可變變量并且是 Optional 類型,這是因?yàn)樗赡軙?huì)變成 nil。

unowned 關(guān)鍵字則必須不是 Optional 類型,也就是說我們?cè)谠L問 unowned 變量時(shí),不需要進(jìn)行對(duì)于 Optional 類型的解包操作,可以直接訪問。然而這也造成了如果我們的 unowned 變量所指向的對(duì)象內(nèi)存被釋放后,我們?cè)僭L問這個(gè)變量會(huì)出現(xiàn)運(yùn)行時(shí)錯(cuò)誤。

三種引用可使用的情景:

| | var | let | Optional | 非 Optional |
| ------------- |:-------------:|
| 強(qiáng)引用(Strong) | ?? | ??|??|??|
| 弱引用(Weak) | ?? | ?|??|?|
| 無主引用(Unowned) | ?? | ??|?|??|

我們用一段代碼來演示 unowned 關(guān)鍵字使用的情景。假設(shè)我們現(xiàn)在有兩個(gè)類,一個(gè) Customer 類,和一個(gè) CreditCard 類。我們知道,一張信用卡一定有一個(gè)主人,而一個(gè)消費(fèi)者不一定有一張信用卡。因而我們需要在 Customer 類設(shè)計(jì)一個(gè) Optional 的信用卡屬性,然后在 CreditCard 類設(shè)計(jì)一個(gè)非 Optional 的消費(fèi)者類。同時(shí)我們?yōu)榱吮苊鈴?qiáng)引用循環(huán)的產(chǎn)生,我們把 CreditCard 類中的消費(fèi)者屬性設(shè)計(jì)為 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") }

}

然后我們創(chuàng)建兩個(gè)對(duì)象,看一看控制臺(tái)是否正常輸出:

do {
    let cus1 = Customer(name: "Kenneth")
    let cus2 = CreditCard(number: 6222136579841254, customer: cus1)
}
控制臺(tái)

在這種情況下,CreditCard 類和 Person 類相互引用了對(duì)方,但是因?yàn)槲覀儼?CreditCard 類中的 customer 屬性設(shè)計(jì)成 unowned,就不會(huì)出現(xiàn)強(qiáng)引用循環(huán)。

閉包中的循環(huán)引用

TBD...

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

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

  • 作為一門現(xiàn)代的高級(jí)編程語言,Swift代替我們進(jìn)行了對(duì)象的創(chuàng)建和銷毀等相關(guān)的內(nèi)存管理。它使用了一個(gè)優(yōu)雅的技術(shù),叫做...
    Maru閱讀 2,296評(píng)論 4 17
  • 29.理解引用計(jì)數(shù) Objective-C語言使用引用計(jì)數(shù)來管理內(nèi)存,也就是說,每個(gè)對(duì)象都有個(gè)可以遞增或遞減的計(jì)數(shù)...
    Code_Ninja閱讀 1,725評(píng)論 1 3
  • 內(nèi)存管理 簡述OC中內(nèi)存管理機(jī)制。與retain配對(duì)使用的方法是dealloc還是release,為什么?需要與a...
    丶逐漸閱讀 2,080評(píng)論 1 16
  • 內(nèi)存管理 ARC處理原理 ARC是Objective-C編譯器的特性,而不是運(yùn)行時(shí)特性或者垃圾回收機(jī)制,ARC所做...
    b485c88ab697閱讀 11,338評(píng)論 3 47
  • 結(jié)緣美樂家是在去武曄姐的辦公室,當(dāng)時(shí)我的工作事業(yè)走下坡路即于清零最迷茫的時(shí)候整個(gè)人精神狀態(tài)都是一種空洞。一頭長發(fā)干...
    愛阿臻閱讀 451評(píng)論 1 0

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