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

一、內(nèi)存分配

??值類型,比如說枚舉和結(jié)構(gòu)體,它們的內(nèi)存分配和管理都十分簡單。當(dāng)你新建一個值類型實例時,系統(tǒng)會自動為實例分配大小合適的內(nèi)存。任何傳遞實例的操作,比如說作為參數(shù)傳遞給函數(shù),以及存儲到屬性的操作,它們都會創(chuàng)建實例的副本。當(dāng)實例不再存在時,Swift會回收內(nèi)存。因此,我們不需要做任何事情來管理值類型的內(nèi)存。

??在Swift中,內(nèi)存管理這個議題,通常都是和引用類型,尤其是類相關(guān)的。跟值類型一樣,當(dāng)我們新建類實例時,系統(tǒng)會為實例分配內(nèi)存空間。但是,和值類型所不同的是,當(dāng)我們把類實例作為參數(shù)傳遞給函數(shù),或者將其存儲到屬性中時,不再是復(fù)制實例本身,而是對同一塊內(nèi)存創(chuàng)建新的引用。對于同一塊內(nèi)存擁有多個引用的情況,這意味著,只要其中任何一個引用修改了類的實例,那么所有的引用都將能看到這個變化的結(jié)果。

??和C語言不同,Swift并不需要我們手動的管理內(nèi)存,系統(tǒng)會自動為每個類實例維護一個引用計數(shù)(Reference Count)。只要引用計數(shù)大于0,實例就會一直存在;一旦引用計數(shù)變?yōu)?,實例就會被銷毀,而它所占用的內(nèi)存就會被回收,此時deinit方法就會被調(diào)用。因此,我們可以通過實現(xiàn)deinit方法來追蹤實例是否被銷毀。

二、循環(huán)引用

??在正式演示循環(huán)引用之前,我們先通過一個簡單的例子來觀察一下類實例從創(chuàng)建到最后被銷毀的全過程:

class Person: CustomStringConvertible {

    let name: String
    
    // 遵守CustomStringConvertible協(xié)議,實現(xiàn)
    // description計算屬性,自定義打印格式
    var description: String {
        return "\(name)"  // "Person(\(name))"
    }
    
    // 構(gòu)造函數(shù)
    init(name: String) {
        
        // 初始化私有屬性
        self.name = name
    }
    
    // 當(dāng)引用計數(shù)為0時,這個方法會被調(diào)用
    deinit {
        print("\(self)被銷毀了")
    }
}

// 創(chuàng)建一個Person實例,并且對其進行初始化
// 這里需要將實例變量james聲明為可選類型,
// 這樣后面就可以給它賦值nil,從而方便調(diào)用
// deinit方法
var james: Person? = Person(name: "James")
print("創(chuàng)建了一個Person類實例\(james!)")

// 默認(rèn)情況下,所有的引用都是強引用,這意味著當(dāng)我們創(chuàng)建
// Person實例james,并且給它賦值James時,引用計數(shù)是
// 加1的。當(dāng)我們再次給james賦值為nil時,引用計數(shù)是減1
// 的,這樣一來,deinit方法就會被調(diào)用,我們就能看到打印
james = nil

??程序運行之后,我們首先會看到Person實例james被創(chuàng)建,并且當(dāng)我們將其重置為nil時,它就會被銷毀(deinit方法被調(diào)用):

一個類實例從創(chuàng)建到銷毀的全過程.png

??接下來,我們要修改程序。假設(shè)James是一位資深的愛寵人士,它最近買了一條寵物狗,名字叫做旺財。我們先創(chuàng)建一個Dog類,然后再對上面的代碼進行修改:

// Dog.swift
class Dog: CustomStringConvertible {

    let name: String
    var owner: Person?
    
    // 自定義輸出格式
    var description: String {
        if let dogOwner = owner {
            return "\(name)的主人是\(dogOwner)."
        } else {
            return "\(name)是一條流浪犬。"
        }
    }
    
    // 構(gòu)造函數(shù)
    init(name: String) {
        
        // 初始化私有屬性
        self.name = name
    }
    
    // 實例被銷毀時調(diào)用
    deinit {
        print("\(self)被銷毀了")
    }
}

// Person.swift
class Person: CustomStringConvertible {

    let name: String
    var dogs = [Dog]()
    
    // 遵守CustomStringConvertible協(xié)議,實現(xiàn)
    // description計算屬性,自定義打印格式
    var description: String {
        return "\(name)"  // "Person(\(name))"
    }
    
    // 構(gòu)造函數(shù)
    init(name: String) {
        
        // 初始化私有屬性
        self.name = name
    }
    
    // 當(dāng)引用計數(shù)為0時,這個方法會被調(diào)用
    deinit {
        print("\(self)被銷毀了")
    }
    
    // 買了寵物狗
    func buyDogs(_ dog: Dog) {
        dog.owner = self
        dogs.append(dog)
    }
}

// main.swift
// 創(chuàng)建一個Person實例
var james: Person? = Person(name: "James")
print("創(chuàng)建了一個Person類實例\(james!)")

// 創(chuàng)建一個Dog實例并且初始化
var wangcai: Dog? = Dog(name: "Wangcai")

// james買了寵物狗wangcai
james?.buyDogs(wangcai!)

// 重新賦值
james = nil
wangcai = nil

??運行上面的程序,你會發(fā)現(xiàn),除了main.swift中的print語句被打印了之外,Person.swift和Dog.swift中的deinit方法都沒有被調(diào)用:

循環(huán)引用的示例.png

??這也就是說,雖然我們最后給實例變量jameswangcai賦值為nil,但是它們最后都沒有被銷毀。之所以沒有被銷毀,是因為此時jameswangcai的引用計數(shù)都不為0。

??為什么會出現(xiàn)上面這種情況?在前面的注釋中我們說過,默認(rèn)情況下,所有的引用都是強引用,而我們恰好就創(chuàng)建了兩個強引用,以至于james強引用wangcai,而wangcai又強引用james,從而導(dǎo)致指向這兩個實例的變量沒有了,但是他們的內(nèi)存卻不會被回收。

??循環(huán)引用的一個很嚴(yán)重的后果就是內(nèi)存泄漏,也就是當(dāng)程序已經(jīng)不再需要這些內(nèi)存的時候,它并沒有將其交還給操作系統(tǒng)。當(dāng)然,如果整個應(yīng)用程序都停止了,這個應(yīng)用所有的內(nèi)存,包括泄漏的內(nèi)存都會被操作系統(tǒng)給回收。只不過,在整個應(yīng)用運行期間,過多的內(nèi)存泄漏會導(dǎo)致程序占用內(nèi)存過大,有可能會被操作系統(tǒng)殺掉的。所以,對于內(nèi)存本來就相對有限的iOS來說,應(yīng)用程序的內(nèi)存管理是一個值得重視的問題。

??我們已經(jīng)知道了上面實例沒有被銷毀的原因,以及實例所占用的內(nèi)存沒有被及時回收的后果,接下來就該知道怎么去避免這種事情的發(fā)生了。

三、循環(huán)引用問題的解決

??解決循環(huán)引用最主要的一個手段,就是將兩個相互強引用關(guān)系中的一個變?yōu)槿跻谩wift中提供了一個關(guān)鍵字weak來處理這種問題。我們修改Dog.swift中的代碼,用關(guān)鍵字weak來修飾屬性owner

class Dog: CustomStringConvertible {

    let name: String
    weak var owner: Person?
    
    // 自定義輸出格式
    var description: String {
        if let dogOwner = owner {
            return "\(name)的主人是\(dogOwner)."
        } else {
            return "\(name)是一條流浪犬。"
        }
    }
    
    // 構(gòu)造函數(shù)
    init(name: String) {
        
        // 初始化私有屬性
        self.name = name
    }
    
    // 實例被銷毀時調(diào)用
    deinit {
        print("\(self)被銷毀了")
    }
}

??值得注意的是,弱引用的使用是有條件的:(1)、弱引用修飾的屬性必須用關(guān)鍵字var來聲明,不能使用let;(2)、弱引用修飾的屬性必須聲明為可選類型。修改完成之后,再來運行程序,就能看到我們想要的結(jié)果了:

打破循環(huán)引用之后程序運行的結(jié)果.png

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

??為了演示閉包中的循環(huán)引用問題,我們先來新建一個Accountant類,用來記錄Person實例新增的資產(chǎn),并且根據(jù)實際需求修改Person.swift中的代碼:

// Accountant.swift
class Accountant {
    
    // 使用類型別名來定義一個閉包
    typealias NetWorthChanged = (Double) -> ()
    
    var netWorthChangedHandler: NetWorthChanged? = nil
    
    // 凈資產(chǎn)
    var netWorth: Double = 0 {
        
        // 監(jiān)聽netWorthChangedHandler的變化
        didSet {
            netWorthChangedHandler?(netWorth)
        }
    }

    // 增加了新的資產(chǎn)
    func gained(_ dog: Dog) {
        netWorth += dog.price
    }
}

// Person.swift
class Person: CustomStringConvertible {

    let name: String
    let accountant = Accountant()  // 注意,這個是強引用
    var dogs = [Dog]()
    
    // 遵守CustomStringConvertible協(xié)議,實現(xiàn)
    // description計算屬性,自定義打印格式
    var description: String {
        return "\(name)"  // "Person(\(name))"
    }
    
    // 構(gòu)造函數(shù)
    init(name: String) {
        
        // 初始化私有屬性
        self.name = name
        
        // 給accountant的netWorthChangedHandler賦值
        accountant.netWorthChangedHandler = { netWorth in
            
            self.netWorthDidChange(to: netWorth)
            return
        }
    }
    
    // 當(dāng)引用計數(shù)為0時,這個方法會被調(diào)用
    deinit {
        print("\(self)被銷毀了")
    }
    
    // 買了寵物狗
    func buyDogs(_ dog: Dog) {
        dog.owner = self
        dogs.append(dog)
        
        // 如果有新增的資產(chǎn),需要進行記錄
        accountant.gained(dog)
    }
    
    // 記錄凈資產(chǎn)的變化
    func netWorthDidChange(to netWorth: Double) {
        print("\(self)又買了一條狗,它現(xiàn)在新增資產(chǎn)的價值是\(netWorth)元。")
    }
}

??在前面,我們已經(jīng)解決了實例變量jameswangcai之間相互循環(huán)引用的問題。但是,此時如果運行程序,你又會發(fā)現(xiàn),實例變量jameswangcai又沒有被釋放:

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

??很顯然,程序中肯定又產(chǎn)生了循環(huán)引用。這是為什么呢?要弄清這個問題,我們必須先回顧一下閉包的基礎(chǔ)知識首先,閉包是一種特殊的函數(shù),它可以捕獲和存儲其所在上下文環(huán)境中的變量和常量,即使定義這些變量和常量的原作用域已經(jīng)不存在了,它仍然可以在其函數(shù)體內(nèi)部引用和修改這些值。其次,閉包是引用類型,這意味著當(dāng)你把閉包賦值給變量或者常量時,實際上是讓這個變量或者常量指向這個閉包,也就是說我們并沒有為這個閉包創(chuàng)建新的副本。最后,在閉包中涉及當(dāng)前類的屬性,或者調(diào)用當(dāng)前類的函數(shù)時,必須明確使用self.。知道這些東西以后,再回過頭去看之前的代碼,很容易就明白為什么我們的項目中存在循環(huán)引用了。

??首先,Person類中有一個Accountant類型的屬性accountant。因此,Person類對Accountant類有一個強引用;其次,默認(rèn)情況下,閉包對它里面捕獲的變量或者常量有一個強引用。而我們在Person類中對Accountant類的屬性netWorthChangedHandler進行賦值時,是通過self.進行的,而此時self恰恰是指代Person類。因此,Accountant類又對Person類有一個強引用。那么,如何打破這種循環(huán)引用的關(guān)系呢?

// 在閉包中調(diào)用當(dāng)前類的方法時,如果沒有使用self.會報如下錯誤:
Call to method '方法名' in closure requires explicit 'self.' to make capture semantics explicit

// 在閉包中使用當(dāng)前類的屬性時,如果沒有使用self.會報如下錯誤:
Reference to property '屬性名' in closure requires explicit 'self.' to make capture semantics explicit

??一個比較好的辦法是,改變閉包捕獲的語義,使捕獲變?yōu)槿跻谩榇?,我們需要使?strong>捕獲列表(Capture List)。捕獲列表的語法是,在閉包參數(shù)列表的前面,加上帶方括號的變量列表,通過這種方式來告訴Accountant類使用弱引用來捕獲self(Person):

// 構(gòu)造函數(shù)
init(name: String) {
    
    // 初始化私有屬性
    self.name = name
    
    // 給Accountant類的屬性netWorthChangedHandler賦值
    accountant.netWorthChangedHandler = { [weak self] netWorth in
        
        self?.netWorthDidChange(to: netWorth)
        return
    }
}

??需要注意的是,此時self是弱引用,而所有的弱引用實例,都必須是可選類型,因此需要將原先的self.修改為self?.。再次運行程序,我們又可以看到Person的實例james和Dog的實例wangcai被正常釋放了:

閉包中循環(huán)引用的解決.png

五、逃逸閉包和非逃逸閉包

??我們先來看一下什么叫做逃逸(escaping)。逃逸,是指傳遞給一個函數(shù)的閉包可能會在該函數(shù)返回之后被調(diào)用。也就是說,閉包逃脫出了接收它作為參數(shù)的函數(shù)作用域。比如說,像我們上面提到的屬性netWorthChangedHandler,它就是逃逸的。非逃逸閉包(non-escaping closure),就是指在函數(shù)返回之后不可能被調(diào)用的閉包。因此,它不可能產(chǎn)生強引用,也就不需要顯式的使用self。以函數(shù)參數(shù)形式聲明的閉包默認(rèn)是非逃逸的,其它場景中的閉包都是逃逸的。

??接下來,我們通過一個示例來演示一下非逃逸閉包。修改Accountant類中的gained(_: )方法,給它增加一個閉包參數(shù)(也就是函數(shù)參數(shù)):

// 增加了新的資產(chǎn)
func gained(_ dog: Dog, completionHandler: () -> ()) {
    netWorth += dog.price
    completionHandler()
}

??這樣一修改,之前所有調(diào)用gained(_: )方法的地方肯定會報錯。因此,來到Person.swift這個類中,修改買了寵物狗方法中的代碼如下:

// 買了寵物狗
func buyDogs(_ dog: Dog) {
    // dog.owner = self
    // dogs.append(dog)
    
    // 如果有新增的資產(chǎn),需要進行記錄
    accountant.gained(dog) {
        
        // 在這個閉包中不需要寫self.dog.owner
        // 因為編譯器知道傳遞給函數(shù)gained(_ : completionHandler: )
        // 的閉包是非逃逸的。因為,以函數(shù)參數(shù)形式聲明的閉包默認(rèn)都是非逃逸的。
        // 所有非逃逸閉包都不可能產(chǎn)生強引用,所以就不需要顯式的使用self了
        dog.owner = self
        dogs.append(dog)
    }
}

??需要特別強調(diào)的是,在gained(_ : completionHandler: )這個方法中,參數(shù)completionHandler是函數(shù)形式(閉包是特殊函數(shù)),而所有作為函數(shù)參數(shù)形式的閉包,默認(rèn)都是非逃逸的,也就是不可能產(chǎn)生強引用,為此也就是不需要顯式的使用關(guān)鍵字self了。

??那么,又該如何告訴編譯器此閉包是逃逸的呢?修改Person.swift中的代碼,給它新增一個方法:

func useNetWorthChangedHandler(handler: @escaping (Double) -> ()) {
    
    // 將閉包參數(shù)handler賦值給Accountant類的屬性netWorthChangedHandler,
    // 而Accountant類的netWorthChangedHandler是存儲屬性,這也就是意味著
    // 在函數(shù)返回之后需要調(diào)用它,也就是說閉包是會逃脫函數(shù)的作用域的。而閉包參數(shù)
    // 在函數(shù)中默認(rèn)是非逃逸的,為此,需要用@escaping告訴編譯器,handler是非逃逸的
    accountant.netWorthChangedHandler = handler
}

??將一個閉包作為參數(shù)傳遞給函數(shù),并且告訴編譯器,這個閉包參數(shù)是逃逸的,這種用法在項目中存在著廣泛的用途,應(yīng)該要掌握。比如說,我之前的項目QTRadio,請求網(wǎng)絡(luò)數(shù)據(jù)的方法中就用到了逃逸閉包:

extension NavBarViewModel {
    
    /// 請求網(wǎng)絡(luò)數(shù)據(jù)并將其轉(zhuǎn)換為模型
    func requestData(completionHandler: @escaping () -> ()) {
        
        // 通過Alamofrie來發(fā)送網(wǎng)絡(luò)請求
        NetworkTools.shareTools.requestData(kRequestURL, .get, parameters: ["wt": "json", "v": "6.0.4", "deviceid": "093e8b7e24c02246fe92373727e4a92c", "phonetype": "iOS", "osv": "11.1.1", "device": "iPhone", "pkg": "com.Qting.QTTour"]) { (result) in
            
            /// 將JSON數(shù)據(jù)轉(zhuǎn)成字典
            guard let resultDict = result as? [String: Any] else { return }
            
            /// 根據(jù)字典中的關(guān)鍵字data取出字典中的數(shù)組數(shù)據(jù)
            guard let resultArray = resultDict["data"] as? [[String: Any]] else { return }
            
            /// 遍歷數(shù)組resultArray,取出它里面的字典
            for dict in resultArray {
                
                // 將字典轉(zhuǎn)為模型
                let item = NavBarModel(dict: dict)
                
                // 將轉(zhuǎn)換完成的模型存儲起來
                self.navBarModelArray.append(item)
            }
            
            // 數(shù)據(jù)回調(diào)
            completionHandler()
        }
    }
}

??詳細(xì)代碼參見ios-step-by-step,如果有好的修改建議,請給我留言,本人將十分感謝。

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

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

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