看過不少分析Swift解決循環引用的文章,分析weak和unowned的區別等等,可能是不太符合我的思路,一直感覺很模糊,在平時使用的時候對什么時候用weak,什么時候用unowned方面還是不太明確,干脆自己在這方面進行了一次整理。
自動引用計數(ARC)
Swift和OC一樣,使用的是自動引用計數的機制來追蹤和管理APP的內存。顧名思義,自動引用計數是自動進行的,并不需要我們手動去參與內存的管理——當一個實例使用完了的時候,會自動對其占用的內存進行釋放。當然,ARC管理的只是引用類型,值類型的(比如結構體和枚舉)不在其管理范圍之內。
ARC其實就干了三件事:
- 為新創建的實例分配內存
- 確保使用中的實例不會被銷毀
- 確保使用完的實例被正確釋放,騰出占用的內存空間
上面三板斧的實現是靠ARC維護一個計數來實現的,當初始化的時候,引用計數為1;每次有新的對這個實例的引用的時候,引用計數加1;每次對應引用被置為nil時,引用計數減1;當引用計數為0的時候,該實例被銷毀,回收內存空間。
舉個例子吧,假如有一個類如下:
class Person {
let name: String
init(name: String) { self.name = name }
deinit { print("\(name)被注銷了") }
}
這個類很簡單,一個name的屬性,一個構造函數,一個析構函數。創建該類的新實例的時候,調用構造函數,銷毀該實例的時候,調用析構函數。
下面,我們創建一個Person類的實例,如下
Person.init(name: "Ivan")
我們只是創建了這個實例,在正常使用中我們是不會單純這樣做的,沒有意義。我們會把這個實例賦值給某個變量來進行使用。
let person1 = Person.init(name: "Ivan") // 引用計數加1,現在為1
這時候,person1和這個Person類的新實例直接建立了一個強引用,該實例的引用計數加1。也是因為該實例有強引用,所以它所在的內存空間不會被銷毀,在ARC眼中它還有利用價值。
假如這個實例也賦值給了其他變量,如下
let person2 = person1 // 引用計數加1,現在為2
let person3 = person1 // 引用計數加1,現在為3
let person4 = Person.init(name: "Jack") // 引用計數加1,這是個新的實例,這個實例引用計數現在為1
當變量對這個實例的引用被銷毀,即置為nil的時候,就會減少這個實例的引用計數,當引用計數為0 的是,這個實例即被銷毀,回收內存空間。
person1 = nil // 引用計數減1,現在為2
person2 = nil // 引用計數減1,現在為1
person3 = nil // 引用計數減1,現在Person類的這個實例被銷毀了
但是ARC畢竟不是智能的,默認它會把所有的引用歸為強引用,只要還在被其他的屬性、常量、變量所使用,它是不會被釋放的。但是凡事總有特殊情況,這時候就需要對ARC釋放內存的方式進行提示(weak,unowned)。
循環引用
墨菲定律:如果事情有變壞的可能,不管這種可能性有多小,它總會發生。
我們再舉一個例子,有下面2個相關類:
class Person {
let name: String
var pet: Dog?
init(name: String) { self.name = name }
deinit { print("\(name)被注銷了") }
}
class Dog {
let nickName: String
let owner: Person?
init(species: String) { self.species = species }
deinit { print("\(nickName)被注銷了") }
}
發現Person類多了一個Dog屬性,Dog類里面也有一個Person屬性。一個人可以擁有一只寵物狗,一只狗也可以擁有一個主人;同時因為一個人也可以沒有寵物,一只狗也可以是一只野狗,所以這兩個變量都是可選的。
那么問題來了:假如我們同時創建了這兩個類的實例并且賦值給了兩個變量會怎么樣?
var ivan = Person.init(name: "ivan")
var wawa = Dog.init(nickName: "wawa")
就像上圖一樣,ivan變量建立了對Person實例的強引用,wawa建立了對Dog實例的強引用。其實沒什么,因為兩者并沒有什么關系。但是,如果我們加上下面的語句:
ivan.pet = wawa
wawa.owner = ivan
那么一切都不一樣了,如下圖:
此時在之前兩個強引用的基礎上,多了Person實例中的pet變量對Dog實例的強引用,以及Dog實例的owner變量對Person實例的強引用。
如果這時候,我們結束了對這兩個實例的使用,想要銷毀它們來騰出內存空間,這時候就出問題了。
ivan = nil
wawa = nil
如上圖,我們的變量到實例直接的引用已經沒有了,但是這兩個實例會被銷毀嗎?答案是否定的。因為他們直接還相互存在引用,只要還有對實例的引用,那么實例就不會輕易被銷毀,內存空間也不會被正確釋放,這就是因為循環引用導致的內存泄漏。
解決循環引用導致的內存泄漏問題
從上面的例子里面可以看到,存在一種可能,ARC會維護一種永遠不會置為0的實例:如果兩個實例互相持有對方的強引用,那么會互相讓對方永遠至少存在1的引用計數,這就造成了循環強引用。
首先,我們在平時的類關系設計的時候就會事先考慮好,盡量去避免對象實例之間的相互持有,也就避免了循環引用。
當然,在設計上無法避免這樣的設定的時候,就可以對類關系之間的關系進行重新定義,把強引用改為弱引用或者無主引用。弱引用和無主引用允許發生了循環引用的兩個實例之間的一個實例引用另外一個實例而不保持強引用。
那么在什么時候用弱引用,什么時候用無主引用呢?
在兩個實例中,假如一個實例引用的另外一個實例具有更短的生命周期,那么就使用弱引用(weak)來引用這個實例,如果引用的實例具有相同的或者更長的生命周期的時候,那么就使用無主引用(unowned)。
這兩個在使用的時候一定要注意,假如你使用了無主引用引用了一個實例,你必須保證這個實例在引用者的生命周期內不會被銷毀,如果被引用的這個實例卻先一步over了,你依然訪問這個無主引用,那么就會導致崩潰。弱引用不會對其引用的實例保持強引用,也就不會去阻止ARC銷毀被引用的實例。
那么,如何去保證無主引用的實例不會被銷毀呢?一般來說,引用的這個實例是永遠存在的,不可能為nil。所以,我們可以這樣區分:兩個循環引用的實例所引用的屬性都允許為nil的時候,可以使用弱引用來解決;但是如果其中一個屬性的值不允許為nil的時候,即只要這個實例存在,就一定會引用著另外一個實例的屬性,那么就可以使用無主引用來解決了。
假如出現了這種情況:兩個實例所引用的屬性都不允許為nil,互相引用該怎么破?假如有兩個類,一個是國家Country,一個是城市City。城市肯定屬于一個國家,一個國家也肯定會有一個首都城市。這就是互相引用了不為nil的屬性。
如下所示:
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
我們可以看到在Country類里面有個capitalCity屬性,在City類里面有個country屬性。同時,在County類的構造器里面有對City的初始化賦值到自己的capitalCity屬性,在對City初始化的這個構造器里面有一個country參數引用了Country實例(self)。
構造過程沒有進行完的時候如何使用這個類的實例呢?這里使用了swift兩段式構造的特性,第一段構造,給每一個存儲型屬性指定一個初始值;第二段構造才會對屬性進行進一步定制。Country類里面的capitalCity類型設置為隱式解析可選類型,City!
表示這個可選類型屬性初始化的值為nil,但是不需要進行展開。因為有初始值nil,所以順利度過第一段構造,在name屬性也被構造器賦值后,其實所有屬性就已經初始化完成,這個類已經構造完成,所以在第二段構造過程中可以使用self
作為參數,為capitalCity進行重新賦值。
通過這種方式我們可以通過一條語句同時創建Country類和City類的實例,這樣就不會產生循環引用。在這里,我們一邊使用了無主引用,一邊使用了隱式解析,通過二段式構造的特性巧妙解決了相互引用的問題。
閉包中的循環引用
因為閉包也是引用類型,所以閉包也和一個類一樣,如果一個類的某個屬性引用了閉包,而這個閉包中又引用了這個類實例,那么就會出現閉包引起的循環引用問題。
swift很人性化的一點就是,假如你在閉包里面是用了這類實例的某個屬性或者某個方法,就一定會提示你在前面加上self
,以提醒你注意循環引用被你一不小心就制造了出來。
swift維護有一個閉包捕獲列表,列表的每一項都是由中括號括起來的一對值組成,第一個值是weak或者unowned,另外一個值是對類實例的引用或者是初始化后的變量,比如[unowned self], [weak delegate = self.delegate]等。
當閉包和它捕獲的實例相互引用并且是同時銷毀的時候,將閉包里面捕獲的引用定義為無主引用。如果被捕獲的引用可能會變為nil的時候,將它定義為弱引用。