Swift ARC(自動引用計(jì)數(shù)器)

我的博客

Swift 采用ARC的方式來管理和追蹤程序中的內(nèi)存使用情況。ARC的全稱(Automatic Reference Counting),一般叫做自動引用計(jì)數(shù)。在大多數(shù)情況下,開發(fā)者無需考慮內(nèi)存管理問題,當(dāng)不再需要使用實(shí)例對象時(shí),ARC會自動釋放這些內(nèi)存。

ARC的引用計(jì)數(shù)一般應(yīng)用于類的實(shí)例或閉包,而數(shù)組(Array)、字典(Dictionary)、字符串(String)、結(jié)構(gòu)體(Structure)、枚舉(enum)都是值類型,不是引用的方式來存儲和傳遞的。官方文檔的原文是:Reference counting applies only to instances of classes. Structures and enumerations are value types, not reference types, and aren’t stored and passed by reference.

關(guān)于值類型和引用類型的區(qū)別,可參考官方博客:Swift: Value and Reference Types

1、Swift中ARC是如何工作的

1.1、How ARC Works

  • 每次創(chuàng)建一個(gè)類的實(shí)例,ARC就會自動為其分配內(nèi)存,用來存儲這個(gè)實(shí)例及其相關(guān)的屬性
  • 當(dāng)該實(shí)例不再被使用時(shí),ARC會釋放這個(gè)實(shí)例所占用的內(nèi)存
  • 繼續(xù)訪問已釋放的實(shí)例,如調(diào)用其方法或?qū)傩裕敲纯赡軙斐沙绦騝rash
  • 為了解決訪問已釋放實(shí)例造成的crash問題,ARC會追蹤每個(gè)引用當(dāng)前實(shí)例累的屬性、常量、和變量的數(shù)量。只要有一個(gè)有效的引用,ARC就不會釋放這部分內(nèi)存。
  • 為此每次將一個(gè)類的實(shí)例賦值給一個(gè)屬性(也可以是常量或變量)。這個(gè)屬性就是這個(gè)實(shí)例的強(qiáng)引用。之所以稱為強(qiáng)引用,是因?yàn)樵搶傩詮?qiáng)持有這個(gè)實(shí)例,并且只要這個(gè)強(qiáng)引用還存在,就不能銷毀這個(gè)實(shí)例。

用代碼來說明,我有一個(gè)學(xué)生類,為其設(shè)置一個(gè)name屬性用來保存這個(gè)學(xué)生的姓名,當(dāng)我創(chuàng)建這類時(shí),ARC會自動為這個(gè)類創(chuàng)建一部分空間用來保存Student實(shí)例及其屬性。

為了更好的監(jiān)聽這個(gè)類的創(chuàng)建和銷毀,我分別在initdeinit方法中通過打印來監(jiān)聽。

class Student: NSObject {
    var name: String
    init(name: String) {
        self.name = name
        print("init------------------Student")
    }
    
    deinit {
        print("deinit------------------Student")
    }
}
var studentTom: Student? = Student(name: "Tom") // 引用計(jì)數(shù)為1
print("init------------------Student")

運(yùn)行上面的代碼,此時(shí)可以打看打印結(jié)果,說明此時(shí)的引用計(jì)數(shù)為1。此時(shí)沒有釋放這部分內(nèi)存,如果我將這個(gè)實(shí)例直接置為nil呢

studentTom = nil // 此時(shí)引用計(jì)數(shù)為0
print("deinit------------------Student")

當(dāng)調(diào)用了deinit方法說明引用計(jì)數(shù)為0,ARC會自動釋放該實(shí)例的內(nèi)存。

let studentName = studentTom!.name

當(dāng)我把studentTom置為nil后,再次調(diào)用studentTom就會crash。Xcode同時(shí)會拋出一段異常:

error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).

為了防止出現(xiàn)這種情況,一般在使用可選類型(Optionals)時(shí),應(yīng)該優(yōu)先做解析處理。

// 可選綁定解包
if let studentTom = studentTom {
    let studentName = studentTom.name
}
// guard 語法解包
guard let studentTom = studentTom else { return }
let studentName = studentTom.name

如果因?yàn)樾枰覍⒃搶W(xué)生信息進(jìn)行copy操作呢,此時(shí)引用計(jì)數(shù)就變成了2,為了驗(yàn)證我的猜想,修改代碼如下:

var studentCopy: Student? = studentTom // 引用計(jì)數(shù)為2
studentTom = nil // 引用計(jì)數(shù)為1

再次運(yùn)行代碼,發(fā)現(xiàn)并沒有調(diào)用deinit方法,當(dāng)我進(jìn)行copy操作的時(shí)候,其引用計(jì)數(shù)就變成了2,這時(shí)候再置為nil其引用計(jì)數(shù)是1,ARC并沒有釋放其內(nèi)存。此時(shí)需要將studentCopy的值置空,將其引用計(jì)數(shù)清空,ARC就會自動清理這部分內(nèi)存。

studentCopy = nil // 引用計(jì)數(shù)為0
deinit------------------Student

2、循環(huán)引用

前面說ARC為了保證被使用實(shí)例對象不被提前釋放,而采用了強(qiáng)引用的方式。那么針對這種情況,對開發(fā)者而言是否就一勞永逸了呢,答案是否定的,當(dāng)兩個(gè)實(shí)例之前形成強(qiáng)持有環(huán)時(shí),這兩個(gè)實(shí)例的內(nèi)存就永遠(yuǎn)不會得到釋放,這就需要開發(fā)者來做一些處理保證這部分內(nèi)存能夠在不需要時(shí)得到釋放。

2.1、循環(huán)引用是如何產(chǎn)生的

  • 兩個(gè)實(shí)例彼此保持對方的強(qiáng)引用,使得每個(gè)實(shí)例都使對方的保持有效時(shí)會發(fā)生循環(huán)引用。

    舉例,現(xiàn)在我有一個(gè)老師類,對于老師和學(xué)生而言,老師要知道學(xué)生的信息,學(xué)生也要知道老師的信息,如老師的姓氏,所教授的課程等。

// 表示老師所教授的課程 
enum Course {
   case language // 語文
   case english // 英語
   case calculus // 微積分
   case quantumMechanics // 量子力學(xué)
   case geology // 地質(zhì)學(xué)
}
class Teacher: NSObject {
   let lastName: String
   let course: Course
   var student: Student?
 
   init(lastName: String, course: Course) {
       self.lastName = lastName
       self.course = course
       print("init------------------Teacher")
   }
 
    deinit {
       print("deinit------------------Teacher")
    }
}

class Student: NSObject {
   var name: String
   var teacher: Teacher?
 
   init(name: String) {
       self.name = name
       print("init------------------Student")
   }
 
   deinit {
       print("deinit------------------Student")
   }
}
var studentTom: Student? = Student(name: "Tom")
var teacherMars: Teacher? = Teacher(lastName: "Mars", course: .calculus)

teacherMars?.student = studentTom
studentTom?.teacher = teacherMars

teacherMars = nil
studentTom = nil

運(yùn)行上面的代碼,發(fā)現(xiàn)無論如何都不會調(diào)用deinit方法。是因?yàn)樗麄兏髯砸眠@自己的對象,studentTomteacherseacherMarsstudent屬性又相互引用了對方,此時(shí)在他們的引用計(jì)數(shù)都變成了2,于是就造成一個(gè)引用循環(huán)。他們之間的引用關(guān)系如下圖所示:

強(qiáng)引用環(huán).png

2.2、如何避免循環(huán)引用

為了解決上面的引用循環(huán)問題,根據(jù)屬性是否可選而采取不同的解決方案,當(dāng)屬性為可選時(shí)可以用weak關(guān)鍵字修飾,表示該屬性為弱引用。當(dāng)屬性不可選時(shí),可以用unowned關(guān)鍵字來修飾。無論是weak還是unowned,他們的思路都是一樣的,不讓某種形式的引用增加引用計(jì)數(shù)就好了。

2.2.1 弱引用

在上面的例子中,只需對任意一個(gè)屬性設(shè)置為弱引用即可,當(dāng)然也可以把兩個(gè)屬性都設(shè)置為weak,不過沒有這么做的必要。

weak var student: Student?

此時(shí)兩個(gè)實(shí)例之間的關(guān)系圖如下所示:

弱引用環(huán).png

當(dāng)我在弱引用下來釋放studentTom的內(nèi)存時(shí),會是什么結(jié)果呢此時(shí)兩個(gè)實(shí)例之間的關(guān)系如下所示:

studentTom = nil
teacherMars?.student
print("------------------\(String(describing: teacherMars?.student))")
init------------------Student
init------------------Teacher
deinit------------------Student
------------------nil

通過上面打印的結(jié)果來看,studentTom實(shí)例的內(nèi)存順利釋放了,那么當(dāng)studentTomnil時(shí),ARC根據(jù)當(dāng)前的情況進(jìn)行了操作呢?

  • 首先Student對象就不再有任何strong reference了,ARC會立即回收這部分內(nèi)存,同時(shí)Teacher對象的引用計(jì)數(shù)也會減一;
  • 其次當(dāng)Student對象被回收調(diào)之后,teacher這個(gè)strong reference也就不存在了。Teacher的引用計(jì)數(shù)就會減一;
  • 由于student是一個(gè)weak reference,它的值會自動設(shè)置為nil,通過teacherMars?.student打印的結(jié)果為nil可以確認(rèn)這一點(diǎn)。
弱引用01.png

當(dāng)我將其中任意一個(gè)屬性設(shè)置為弱引用后,這時(shí)候把teacherMarsstudentTom都設(shè)置為nil,ARC就能過順利回收所有的內(nèi)存,此時(shí)他們的關(guān)系如圖所示:

teacherMars = nil
studentTom = nil

打印結(jié)果:

init------------------Student
init------------------Teacher
deinit------------------Student
------------------nil
deinit------------------Teacher
弱引用02.png

關(guān)于如何使用weak修飾的屬性總結(jié):

  • 弱引用不會增加實(shí)例的引用計(jì)數(shù),因此不會阻止ARC銷毀被引用的實(shí)例。所以使用弱引用后,即使兩個(gè)實(shí)例互相持有也不會形成強(qiáng)引用環(huán)。
  • 弱引用只能申明為變量類型,因?yàn)檫\(yùn)行時(shí)他的值可能會改變。弱引用絕對不能申明為常量。在Swift中,用var關(guān)鍵字申明的為變量,用let關(guān)鍵字申明的為常量。
  • 因?yàn)槿跻每梢詻]有值,用弱引用來修飾的變量必須是可選類型。
2.2.3 無主引用

雖然weak解決了循環(huán)引用的問題,但是不是所有的屬性都是可選的,如果有一個(gè)不可以為nil的屬性造成了循環(huán)引用,該怎么辦呢?

  • 我可以把這個(gè)不可為nil的屬性修改為可以為nil
  • 采用Swift為開發(fā)者提供的另一種解決方案,使用無主引用

和弱引用相似,無主引用也不強(qiáng)制有實(shí)例對象。和弱引用不同的是,無主引用默認(rèn)始終有值。在屬性和變量前添加unowned關(guān)鍵字,就可以申明一個(gè)無主引用。

為了演示這個(gè)過程,我為每個(gè)學(xué)生添加了家庭作業(yè)homeWork屬性,當(dāng)然并不是所有的學(xué)生都會按時(shí)寫作業(yè),所以homeWork的類型是optional,然后來實(shí)現(xiàn)HomeWork類;

// 家庭作業(yè)
var homeWork: HomeWork?
class HomeWork: NSObject {
    let student: Student
    let course: Course
  
    init(student: Student, course: Course) {
        self.student = student
        self.course = course
        print("init------------------HomeWork")
    }
    
    deinit {
        print("deinit------------------HomeWork")
    }
}

這里既然有了家庭作業(yè),那么我就要知道是誰寫的,是哪門課程的作業(yè),這里studentNamecourse就不能是一個(gè)optional。

var david: Student? = Student(name: "David Taylor")
var homeWork: HomeWork? = HomeWork(student: david!, course: .quantumMechanics)

此處假設(shè)學(xué)生david完成了作業(yè),那么可以用下面的代碼來表示:

david?.homeWork = homeWork 
init------------------Student
init------------------HomeWork

運(yùn)行代碼,發(fā)現(xiàn)并沒有調(diào)用deinit方法,此時(shí)學(xué)生davidhomeWork就形成了一個(gè)引用循環(huán),他們之間的持有的關(guān)系是davidhomeWork各自引用著自己的對象,davidhomeWork互相引用著彼此。

homework001.png

那么此時(shí),我將david置為nil呢?

david = nil
init------------------Student
init------------------HomeWork

運(yùn)行代碼,發(fā)現(xiàn)依舊沒有調(diào)用deinit方法,此時(shí)雖然david實(shí)例為nil,實(shí)例homeWork也離開了自己的作用域。此時(shí)在內(nèi)存中david.homeWorkhomeWork.student之間的引用關(guān)系依舊會把這兩個(gè)對象保持在內(nèi)存中,他們關(guān)系如下圖所示:

homework002.png

當(dāng)然此處可以使用weak關(guān)鍵字將其中任意一個(gè)強(qiáng)持有改成弱引用來解決這個(gè)問題。此處也可以使用系統(tǒng)提供的另一種解決方案:非可選類型的屬性前加unowned,無主引用解決循環(huán)引用問題。

unowned let student: Student

我們可以將任意一個(gè)強(qiáng)引用的屬性前加unowned,就可以解決這個(gè)問題,唯一不同的是Strong reference變成了unowned reference,此時(shí)他們之間的引用關(guān)系是:

homework03.png

這時(shí)候再次運(yùn)行代碼:

david = nil
homeWork = nil

打印結(jié)果如下:

init------------------Student
init------------------HomeWork
deinit------------------Student
deinit------------------HomeWork

可以看到davidhomeWork都可以正常的被回收了,當(dāng)davidnil時(shí)Student對象就會被ARC回收,而當(dāng)homeWorknil時(shí),homeWork也就失去了他的作用也會被ARC回收其內(nèi)存。

homework004.png

如果我調(diào)用被釋放的內(nèi)存之后會怎樣呢?修改代碼如下:

homeWork = nil
homeWork!.student

程序執(zhí)行時(shí)會crash并提示:

Unexpectedly found nil while unwrapping an Optional value

所以使用unowned雖然能解決不可選屬性循環(huán)引用問題,但在實(shí)際開發(fā)中也應(yīng)該注意在使用無主引用時(shí)要確保引用始終指向一個(gè)未銷毀的實(shí)例

雖然使用weakunowned解決了Class中的強(qiáng)引用循環(huán)問題,但是Class并不是Swift中唯一的引用類型,Swift中Closure也是引用類型,至于Closure如何解決內(nèi)存管理問題,可參考官方文檔the swift programming language: Closures


相關(guān)鏈接:

《the swift programming language》Automatic Reference Counting

Handling non-optional optionals in Swift

本文demo

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

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