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

一进苍、內(nèi)存分配

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

??在Swift中,內(nèi)存管理這個議題蜒程,通常都是和引用類型绅你,尤其是類相關(guān)的伺帘。跟值類型一樣昭躺,當我們新建類實例時,系統(tǒng)會為實例分配內(nèi)存空間伪嫁。但是领炫,和值類型所不同的是,當我們把類實例作為參數(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
    }
    
    // 當引用計數(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!)")

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

??程序運行之后,我們首先會看到Person實例james被創(chuà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
    }
    
    // 當引用計數(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)上面這種情況?在前面的注釋中我們說過麸粮,默認情況下溉苛,所有的引用都是強引用,而我們恰好就創(chuàng)建了兩個強引用弄诲,以至于james強引用wangcai愚战,而wangcai又強引用james娇唯,從而導(dǎo)致指向這兩個實例的變量沒有了,但是他們的內(nèi)存卻不會被回收寂玲。

??循環(huán)引用的一個很嚴重的后果就是內(nèi)存泄漏塔插,也就是當程序已經(jīng)不再需要這些內(nèi)存的時候,它并沒有將其交還給操作系統(tǒ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
        }
    }
    
    // 當引用計數(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)部引用和修改這些值颤绕。其次奥务,閉包是引用類型氯葬,這意味著當你把閉包賦值給變量或者常量時帚称,實際上是讓這個變量或者常量指向這個閉包闯睹,也就是說我們并沒有為這個閉包創(chuàng)建新的副本瞻坝。最后,在閉包中涉及當前類的屬性衙荐,或者調(diào)用當前類的函數(shù)時砌函,必須明確使用self.讹俊。知道這些東西以后仍劈,再回過頭去看之前的代碼贩疙,很容易就明白為什么我們的項目中存在循環(huán)引用了这溅。

??首先悲靴,Person類中有一個Accountant類型的屬性accountant庇楞。因此吕晌,Person類對Accountant類有一個強引用睛驳;其次,默認情況下爪瓜,閉包對它里面捕獲的變量或者常量有一個強引用蝶缀。而我們在Person類中對Accountant類的屬性netWorthChangedHandler進行賦值時翁都,是通過self.進行的,而此時self恰恰是指代Person類谅猾。因此柄慰,Accountant類又對Person類有一個強引用。那么税娜,如何打破這種循環(huán)引用的關(guān)系呢?

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

// 在閉包中使用當前類的屬性時,如果沒有使用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ù)形式聲明的閉包默認是非逃逸的,其它場景中的閉包都是逃逸的响牛。

??接下來玷禽,我們通過一個示例來演示一下非逃逸閉包。修改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ù)形式聲明的閉包默認都是非逃逸的。
        // 所有非逃逸閉包都不可能產(chǎn)生強引用焕阿,所以就不需要顯式的使用self了
        dog.owner = self
        dogs.append(dog)
    }
}

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

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

func useNetWorthChangedHandler(handler: @escaping (Double) -> ()) {
    
    // 將閉包參數(shù)handler賦值給Accountant類的屬性netWorthChangedHandler,
    // 而Accountant類的netWorthChangedHandler是存儲屬性彻秆,這也就是意味著
    // 在函數(shù)返回之后需要調(diào)用它楔绞,也就是說閉包是會逃脫函數(shù)的作用域的。而閉包參數(shù)
    // 在函數(shù)中默認是非逃逸的唇兑,為此酒朵,需要用@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()
        }
    }
}

??詳細代碼參見ios-step-by-step黑毅,如果有好的修改建議,請給我留言具帮,本人將十分感謝博肋。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蜂厅,隨后出現(xiàn)的幾起案子匪凡,更是在濱河造成了極大的恐慌,老刑警劉巖掘猿,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件病游,死亡現(xiàn)場離奇詭異,居然都是意外死亡稠通,警方通過查閱死者的電腦和手機衬衬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來改橘,“玉大人滋尉,你說我怎么就攤上這事》芍鳎” “怎么了狮惜?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長碌识。 經(jīng)常有香客問我碾篡,道長,這世上最難降的妖魔是什么筏餐? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任开泽,我火速辦了婚禮,結(jié)果婚禮上魁瞪,老公的妹妹穿的比我還像新娘穆律。我一直安慰自己,他們只是感情好佩番,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布众旗。 她就那樣靜靜地躺著,像睡著了一般趟畏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上滩租,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天赋秀,我揣著相機與錄音利朵,去河邊找鬼。 笑死猎莲,一個胖子當著我的面吹牛绍弟,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播著洼,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼樟遣,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了身笤?” 一聲冷哼從身側(cè)響起豹悬,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎液荸,沒想到半個月后瞻佛,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡娇钱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年伤柄,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片文搂。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡适刀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出煤蹭,到底是詐尸還是另有隱情笔喉,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布疯兼,位于F島的核電站然遏,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏吧彪。R本人自食惡果不足惜待侵,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望姨裸。 院中可真熱鬧秧倾,春花似錦、人聲如沸傀缩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赡艰。三九已至售淡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背揖闸。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工揍堕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人汤纸。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓衩茸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親贮泞。 傳聞我的和親對象是個殘疾皇子楞慈,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容