Swift ARC-自動引用計數、內存管理

作者:fengsh998
原文地址:http://blog.csdn.net/fengsh998/article/details/31824179

Swift使用自動引用計數(ARC)來管理應用程序的內存使用。這表示內存管理已經是Swift的一部分,在大多數情況下,你並不需要考慮內存的管理。當實例並不再被需要時,ARC會自動釋放這些實例所使用的內存。

另外需要注意的:

引用計數僅僅作用於類實例上。結構和枚舉是值類型,而非引用類型,所以不能被引用存儲和傳遞。


swift的ARC工作過程

每當創建一個類的實例,ARC分配一個內存塊來存儲這個實例的信息,包含了類型信息實例的屬性值信息

另外當實例不再被使用時,ARC會釋放實例所佔用的內存,這些內存可以再次被使用。
但是,如果ARC釋放了正在被使用的實例,就不能再訪問實例屬性,或者調用實例的方法了。直接訪問這個實例可能造成應用程序的崩潰。就像空實例或遊離實例一樣。
爲了保證需要實例時實例是存在的,ARC對每個類實例,都追蹤有多少屬性、常量、變量指向這些實例。當有活動引用指向它時,ARC是不會釋放這個實例的。
爲實現這點,當你將類實例賦值屬性常量變量時,指向實例的一個強引用(strong reference)將會被構造出來。被稱爲強引用是因爲它穩定地持有這個實例,當這個強引用存在時,實例就不能夠被自動釋放,因此可以安全地使用。


<pre name="code" class="objc">class Teacher
{
    var tName : String

    init(name:String)
    {
        tName = name
        println("老師 \(tName) 實例初始化完成.")
    }
    
    func getName() -> String
    {
        return tName
    }
    
    func classing()
    {
        println("老師 \(tName) 正在給學生講課.")
    }
    
    deinit
    {
        println("老師 \(tName) 實例析構完成.")
    }
}

測試 ARC

func testArc()
{
    var teacher:Teacher? = Teacher(name:"張三")  //實例化一個Teacher對象將指向一個變量,此時產生了一個強引用(就好像OC中的引用計數+1)
    var refteacher:Teacher? = teacher            //再次產生強引用即(引用計數再+1)
    var refteacher2:Teacher? = teacher<span style="white-space:pre">		</span> <span style="font-family: Arial, Helvetica, sans-serif;">//再次產生強引用即(引用計數再+1)</span>

    
    refteacher = nil            //第一個引用對象爲nil並沒有使實例釋放,(引用計數-1)
    teacher?.classing()         //正常
    teacher = nil               //第二個引用對象爲nil並沒有使實例釋放,(引用計數-1)
    refteacher2!.classing()     //正常
    refteacher2 = nil           //第三個引用對象爲nil此時已沒有作何引用了,因此ARC回收,實例釋放.(引用計數-1)最後引用計數爲0,則自動調用析構
    refteacher2?.classing()     //不再有輸出
}

輸出結果

老師 張三 實例初始化完成.
老師 張三 正在給學生講課.
老師 張三 正在給學生講課.
老師 張三 實例析構完成.

從上面的例子來看,確實swift給我們自動管理了內存,很多時侯開發者都不需要考慮太多的內存管理。但真的是這樣嗎?真的安全嗎?作爲開發者要如何用好ARC?

儘管ARC減少了很多內存管理工作,但ARC並不是絕對安全的。下面來看一下循環強引用導至的內存泄漏


class Teacher
{
    var tName : String
    var student : Student?       //添加學生對象,初始時爲nil
    
    init(name:String)
    {
        tName = name
        println("老師 \(tName) 實例初始化完成.")
    }
    
    func getName() -> String
    {
        return tName
    }
    
    func classing()
    {
        println("老師 \(tName) 正在給學生 \(student?.getName()) 講課.")
    }
    
    deinit
    {
        println("老師 \(tName) 實例析構完成.")
    }
}

class Student
{
    var tName : String
    var teacher : Teacher?       //添加老師對象,初始時爲nil
    
    init(name:String)
    {
        tName = name
        println("學生 \(tName) 實例初始化完成.")
    }
    
    func getName() -> String
    {
        return tName
    }
    
    func listening()
    {
        println("學生 \(tName) 正在聽 \(teacher?.getName()) 老師講的課")
    }
    
    deinit
    {
        println("學生 \(tName) 實例析構化完成.")
    }
}

測試泄漏:

func testMemoryLeak()
{
    var teacher :Teacher?
    var student :Student?
    
    teacher = Teacher(name:"陳峯")   //(引用計數爲1)
    student = Student(name:"徐鴿")   //<span style="font-family: Arial, Helvetica, sans-serif;">(引用計數爲1)</span>
    
    teacher!.student = student  //賦值後將產生"學生"對象的強引用 (引用計數+1)
    student!.teacher = teacher  //賦值後將產生"老師"對象的強引用 (引用計數+1)
    
    teacher!.classing()         //因爲我清楚地知道teacher對象不可能爲空,所以我用!解包
    student!.listening()
    
    //下面的代碼,寫與不寫都不能使對象釋放
    teacher = nil           //引用計數-1 但還不能=0,所以不會析構
    student = nil<span style="white-space:pre">	</span>    //引用計數-1 但還不能=0,所以也不會析構
    
    println("釋放後輸出")
    
    teacher?.classing()<span style="white-space:pre">		</span>//因爲我不能確定teacher對象是否爲空,所以必須用?來訪問。
    student?.listening()
    
}

輸出結果:

老師 陳峯 實例初始化完成.
學生 徐鴿 實例初始化完成.
老師 陳峯 正在給學生 徐鴿 講課.
學生 徐鴿 正在聽 陳峯 老師講的課
釋放後輸出

自始至終都沒有調用deinit。因此就會泄漏,此時已經不能採取任何措拖來釋放這兩個對象了,只有等APP的生命週期結束

實例之間的相互引用,在日常開發中是很常見的一種,哪麼如何避免這種循環強引用導致的內存泄漏呢?

可以通過在類之間定義爲弱引用(weak)或無宿主引用的(unowned)變量可以解決強引用循環這個問題

弱引用方式:

弱引用並不保持對所指對象的強烈持有,因此並不阻止ARC對引用實例的回收。這個特性保證了引用不成爲強引用循環的一部分。指明引用爲弱引用是在生命屬性或變量時在其前面加上關鍵字weak。

注意
弱引用必須聲明爲變量,指明它們的值在運行期可以改變。弱引用不能被聲明爲常量
因爲弱引用可以不含有值,所以必須聲明弱引用爲可選類型。因爲可選類型使得Swift中的不含有值成爲可能。

 因此只需要將上述的例子任意一個實例變量前加上weak關鍵詞即可,如:


    weak var student : Student?
或
    weak var teacher : Teacher?

下面來測試一下weak var student : Student?設爲弱引用後,測試釋放的時間點(情況一)

    var teacher :Teacher?
    var student :Student?
    
    teacher = Teacher(name:"陳峯")
    student = Student(name:"徐鴿")
    
    teacher!.student = student  //賦值後將產生"學生"對象的強引用
    student!.teacher = teacher  //賦值後將產生"老師"對象的強引用
    
    teacher!.classing()
    student!.listening()
    
    teacher = nil               //此時將沒有馬上調用析構,要等student釋放後纔會釋放
    //student = nil
    
    println("釋放後輸出")
    
    teacher?.classing()<span style="white-space:pre">		</span>//前面已設爲nil,所以沒有輸出
    student?.listening()        

經測試輸出

老師 陳峯 實例初始化完成.			//執行teacher = Teacher(name:"陳峯")
學生 徐鴿 實例初始化完成.			//執行student = Student(name:"徐鴿")
老師 陳峯 正在給學生 徐鴿 講課.      //執行teacher!.classing()
學生 徐鴿 正在聽 陳峯 老師講的課		//執行student!.listening()
釋放後輸出                       //執行println("釋放後輸出")
學生 徐鴿 正在聽 陳峯 老師講的課		//執行student?.listening()
學生 徐鴿 實例析構化完成.			//學生對象先釋放
老師 陳峯 實例析構完成.             //此時由於學生對象釋放了,此時沒有了引用,也可以進行析構了

如果  weak var teacher : Teacher?

再來進行測試:(情況二)


    var teacher :Teacher?
    var student :Student?
    
    teacher = Teacher(name:"陳峯")
    student = Student(name:"徐鴿")
    
    teacher!.student = student  //賦值後將產生"學生"對象的強引用
    student!.teacher = teacher  //賦值後將產生"老師"對象的強引用
    
    teacher!.classing()
    student!.listening()
    
    teacher = nil               //此時將沒有馬上調用析構,要等student釋放後纔會釋放
    //student = nil
    
    println("釋放後輸出")
    
    teacher?.classing()
    student?.listening()        //此時並不因爲

輸出結果:

老師 陳峯 實例初始化完成.
學生 徐鴿 實例初始化完成.
老師 陳峯 正在給學生 徐鴿 講課.
學生 徐鴿 正在聽 陳峯 老師講的課
老師 陳峯 實例析構完成.
釋放後輸出
學生 徐鴿 正在聽 nil 老師講的課
學生 徐鴿 實例析構化完成.

經測試得出結論:

當A類中包函有B類的弱引用的實例,同時,B類中存在A的強引用實例時,如果A釋放,也不會影響B的析放,但A的內存回收要等B的實例釋放後纔可以回收。(情況一的結果)

當A類中包函有B類的強引用的實例時,如果A釋放,則不會影響B的析放。(情況二的結果)

無宿主引用方式:

和弱引用一樣,無宿主引用也並不持有實例的強引用。但和弱引用不同的是,無宿主引用通常都有一個值。因此,無宿主引用並不定義成可選類型。指明爲無宿主引用是在屬性變量聲明的時候在之前加上關鍵字unowned
因爲無宿主引用爲非可選類型,所以每當使用無宿主引用時不必使用?。無宿主引用通常可以直接訪問。但是當無宿主引用所指實例被釋放時,ARC並不能將引用值設置爲nil,因爲非可選類型不能設置爲nil。
注意
在無宿主引用指向實例被釋放後,如果你想訪問這個無宿主引用,將會觸發一個運行期錯誤(僅當能夠確認一個引用一直指向一個實例時才使用無宿主引用)。在Swift中這種情況也會造成應用程序的崩潰,會有一些不可預知的行爲發生。因此使用時需要特別小心。


將前面例子改爲無宿主引用:


class Teacher
{
    var tName : String
    var student : Student?              //學生對象的強引用,實例可以爲nil
    
    init(name:String)
    {
        tName = name
        println("老師 \(tName) 實例初始化完成.")
    }
    
    func getName() -> String
    {
        return tName
    }
    
    func classing()
    {
        println("老師 \(tName) 正在給學生 \(student?.getName()) 講課.")
    }
    
    deinit
    {
        println("老師 \(tName) 實例析構完成.")
    }
}

class Student
{
    var tName : String
    unowned var teacher : Teacher           //無宿主引用,不可以設置爲nil
    
    init(name:String,tcher :Teacher)
    {
        tName = name
        teacher = tcher    //因爲無宿主引用不能設爲可選型,所在必須要初始化
        println("學生 \(tName) 實例初始化完成.")
    }
    
    func getName() -> String
    {
        return tName
    }
    
    func listening()
    {
        println("學生 \(tName) 正在聽 \(teacher.getName()) 老師講的課")
    }
    
    deinit
    {
        println("學生 \(tName) 實例析構化完成.")
    }
}

測試無宿主引用:


func testNotOwner()
{
    var teacher :Teacher?               //聲明可選型變量
    
    teacher  = Teacher(name:"陳峯")
    
    var student = Student(name: "徐鴿",tcher: teacher!)
    
    //進行相互引用
    teacher!.student = student
    student.teacher = teacher!
    
    teacher!.classing()
    student.listening()
    
    teacher = nil
    println("老師對象釋放後")
    
    teacher?.classing()
    student.listening() //error 因爲在前面的teacher設爲nil時,隱式的將student對象給釋放了,因此這裏再訪問就會crash
}

輸出結果:

老師 陳峯 實例初始化完成.
學生 徐鴿 實例初始化完成.
老師 陳峯 正在給學生 徐鴿 講課.
學生 徐鴿 正在聽 陳峯 老師講的課
老師 陳峯 實例析構完成.
老師對象釋放後
Program ended with exit code: 9(lldb)  //會crash,thead1:Exc_BREAKPOINT(code=EXC_i386_BPT,subcode=0x0)

所以使用無宿主引用時,就需要特別小心,小心別人釋放時,順帶釋放了強引用對象,所以要想別人釋放時不影響到原實例,可以使用弱引用這樣就算nil,也不會影響。

上面介紹了,當某個類中的實例對象如果在整個生命週期中,有某個時間可能會被設爲nil的實例,使用弱引用,如果整個生命週期中某一實例,一旦構造,過程中不可能再設爲nil的實例變量,通常使用無宿主引用。但時有些時侯,在兩個類中的相互引用屬性都一直有值,並且都不可以被設置爲nil。這種情況下,通常設置一個類的實例爲無宿主屬性,而另一個類中的實例變量設爲的隱式裝箱可選屬性(即!號屬性)

如下面的例子,每位父親都有孩子(沒孩子能叫父親麼?),每個孩子都有一個親生父親


class Father
{
    let children : Children!                    //聲明爲隱式可選類型
    let fathername : String
    init(name:String,childName:String)
    {
        self.fathername = name
        self.children = Children(name: childName,fat:self) //初始化時產生相互引用
    }
    
    deinit
    {
        println("father deinited.")
    }
}

class Children
{
    unowned let father : Father                 //聲明爲無宿主類型
    let name : String
    init(name:String ,fat : Father)
    {
        self.name = name
        self.father = fat
    }
    
    deinit
    {
        println("children deinited.")
    }
}

測試代碼:

    var fa = Father(name: "王五",childName: "王八")
    println("\(fa.fathername) 有個小孩叫 \(fa.children.name)")

輸出結果:

王五 有個小孩叫 王八
father deinited.
children deinited.

同樣可以看到,儘管是循環引用,但還是能正常回收。

另外,還有一種情況,當自身的閉包對自身(self) 的強引用,也會導致內存泄漏。

例子:


class CpuFactory
{
    let cpuName : String
    let cpuRate : Double
    init(cpuName:String,rate:Double)
    {
        self.cpuName = cpuName
        self.cpuRate = rate
    }
    
    //聲明一個閉包
    @lazy var someClosure: (Int, String) -> String = {
        //下面這句不可以註釋編譯器會報Tuple types '(Int,String)'and'()'hava a different number of elements (2 vs. 0)
        [unowned self] (index: Int, stringToProcess: String) -> String in
        // closure body goes here
        
        return "A \(self.cpuName)"  //閉包中引用self
    }
    
    //聲明一個閉包,同樣閉包中引用self
    @lazy var machining: () -> String = {
        [unowned self] in      //這句可以註釋(按照書上說,使用這句可以解釋閉包的強引用,但個人實踐,不管加不加這句,都不會釋放,即這樣寫有內存泄漏)
        // closure body goes here
        
        if self.cpuRate > 10
        {
            return "\(self.cpuName) i7 2.5G"
        }
        else
        {
            return "\(self.cpuName) i3 2.0G"
        }
    }
    
    //聲明一個閉包,但閉包中將自身作爲參數傳進去(可以避去內存泄漏)
    @lazy var machining2 : (CpuFactory) -> String = {

        [unowned self] (cpu:CpuFactory) -> String in
        
        if cpu.cpuRate > 10
        {
            return "\(cpu.cpuName) i7 2.5G"
        }
        else
        {
            return "\(cpu.cpuName) i3 2.0G"
        }
    }
    
    deinit
    {
        println("Cpu Factroy is deinited.")
    }
}

在這個例子中有三個閉包,分別是帶參,和不帶參,對於帶參的 不能省略[unowned self] (paramers) in操作。否則會編譯不過,另外,書中沒有提到的,只有聲明爲@lazy的閉包中纔可以使用[unowned self] 否則在普通閉包中使用也會報錯。還有一點書中講到當自身閉包中使用self.時會產生強引用,導至內存泄漏,因此加上[unowned self ] in 這句可以破壞這種強引用,從而使內存得到釋放,但經本人親自驗證,就算加上了也沒有釋放。

測試


func testClosure()
{
    var cpu : CpuFactory? = CpuFactory(cpuName: "Core",rate: 5)
//    println(cpu!.machining())
    println(cpu!.machining2(cpu!))
//    println(cpu!.someClosure(3,"hello"))
    
    cpu = nil
}

分別單獨驗證各句輸出結果:

func testClosure()
{
    var cpu : CpuFactory? = CpuFactory(cpuName: "Core",rate: 5)
    println(cpu!.machining())
    cpu = nil
}

輸出:

Core i3 2.0G

顯然cpu = nil也不會釋放內存。

再來看第二個。

func testClosure()
{
    var cpu : CpuFactory? = CpuFactory(cpuName: "Core",rate: 5)
    println(cpu!.machining2(cpu!))
    cpu = nil
}

輸出

Core i3 2.0G
Cpu Factroy is deinited.

可見使用自身作爲參數傳參時,可以釋放內存。
同樣再測試第三種

func testClosure()
{
    var cpu : CpuFactory? = CpuFactory(cpuName: "Core",rate: 5)
    println(cpu!.someClosure(3,"hello"))
    cpu = nil
}

輸出

A Core

其實第三和第一種是一樣的,都是引用了self.但第一種可以把[unowned self ]in  句註釋和不註釋的情況下進行測試,可以發現結果是一樣的,並沒有釋放內存。


實在令人有點費解。。。。。。



發佈了45 篇原創文章 · 獲贊 2 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章