懸掛指針及內(nèi)存泄漏#
如果對象a指向?qū)ο骲皮璧,若對象b被釋放了舟扎,則此時對象a指向一個未知地址,這種情況叫做懸掛指針悴务。
如果對象a指向?qū)ο骲睹限,若對象a被釋放了,則此時沒有任何對象能夠指向?qū)ο骲讯檐,對象b無法被釋放羡疗,這種情況叫做內(nèi)存泄漏。
手動內(nèi)存管理#
在ARC出現(xiàn)之前别洪,ios的內(nèi)存管理是基于手動內(nèi)存管理叨恨,也叫做MRC。
為了防止懸掛指針及內(nèi)存泄漏挖垛,手動內(nèi)存管理基于一個引用計數(shù)(retain count)的概念痒钝,所有對象都可以增加或減少一個對象的引用計數(shù),當對象的引用計數(shù)大于0晕换,則該對象繼續(xù)存在午乓;當該對象的引用計數(shù)減少到0,則該對象自動銷毀闸准。NSObject實現(xiàn)了retain和release方法益愈,用于增加或減少引用計數(shù)。
具體的規(guī)則如下:
- 如果對象a通過調(diào)用初始化函數(shù)初始化了對象b夷家,則初始化函數(shù)增加b的引用計數(shù)蒸其。
- 如果對象a通過copy,mutableCopy或任何帶有copy字樣的方法獲得一個對象b的拷貝库快,則這些copy方法負責增加這個新的b的拷貝的引用計數(shù)摸袁。
- 如果對象a直接獲取一個對象b的引用(不是通過初始化或拷貝的方法),則對象a自己負責增加對象b的引用計數(shù)义屏。
- 如果對象a之前直接或間接的增加了對象b的引用計數(shù)靠汁,當對象a不再需要對象b的引用之后蜂大,對象a要負責減少對象b的引用計數(shù)。如果對象b的引用計數(shù)減少到0蝶怔,則對象b被釋放奶浦。
ARC#
ARC(以及Swift)出現(xiàn)之后,我們不能再調(diào)用retain和release方法了踢星。對引用計數(shù)的工作都由ARC自動完成澳叉。ARC被實現(xiàn)為編輯器的一部分,它將在幕后為我們自動加入retain和release方法沐悦。
但即使是在ARC的環(huán)境下成洗,仍然有一些內(nèi)存管理問題需要我們注意或進行手動處理。
循環(huán)引用#
在這里我直接使用官方文檔的例子:
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
這是最典型的強引用循環(huán)藏否,當john和unit4A釋放各自的對象之后瓶殃,Person和Apartment的實例不會銷毀,因為它們在互相引用對方秕岛。具體的解決辦法有weak和unowned碌燕。
Weak##
將上例的tenant屬性聲明為weak即可解決循環(huán)引用的問題。
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
此時如圖:
當john釋放時继薛,Person實例因為沒有strong引用修壕,所以也被銷毀了,tenant屬性是weak引用遏考,所以被ARC自動設(shè)置為nil慈鸠。循環(huán)引用的問題就此解決。
Weak引用利用了ARC的功能灌具,當一個引用被聲明為weak的時候青团,ARC并不會retain這個對象。ARC會記錄所有的weak引用以及它們所指向的對象咖楣,當某個對象的引用計數(shù)降到0并被銷毀之后督笆,ARC會自動將nil賦予這個引用,這也是swift中必須將weak引用聲明為optional var的原因诱贿。
Unowned##
與weak引用相類似娃肿,unowned引用也不會保存一個strong引用至它所指向的對象。
將一個引用聲明為Unowned珠十,意味著ARC對這個引用將不再起任何作用料扰。如果引用的對象被銷毀了,我們將真正面臨懸掛指針的問題焙蹭。所以晒杈,官方的說法是使用unowned引用必須確保unowned引用所指向的對象擁有與unowned引用相同的或更長的生命期限,因為這樣是最安全的孔厉。
還是使用官方的例子:
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
這個例子是使用unowned引用最安全的方法拯钻,因為unowned引用所指向的對象擁有與unowned引用相同的或更長的生命期限帖努,我們可以理解為兩個對象一損俱損的情況下,使用unowned最安全说庭。
但是然磷,如果此時CreditCard實例有另外一個對象指向它,那么當john被釋放了刊驴,Customer實例也被銷毀了,但是CreditCard實例仍然存在寡润,CreditCard類中的customer屬性將指向一個懸掛指針捆憎,我們需要手動的將nil賦予customer屬性。這種情況也是非常常見的梭纹。
一些特殊例子#
ARC不支持的delegate##
使用weak引用最常見的情況就是在delegate中躲惰,如下:
class ColorPickerController : UIViewController {
weak var delegate: ColorPickerDelegate? // ...
}
但是,一些內(nèi)置的Cocoa類使用了ARC不支持的引用(因為它們是非常老舊的代碼或有向后兼容的需求)变抽,這種屬性使用了assign關(guān)鍵字础拨,例如,AVSpeechSynthesizer的delegate屬性聲明如下:
@property(nonatomic, assign, nullable) id<AVSpeechSynthesizerDelegate> delegate;
在Swift中绍载,對應的聲明如下:
unowned(unsafe) var delegate: AVSpeechSynthesizerDelegate?
在Swift中诡宗,unowned和Objective-C的assign意義相同,都意味著ARC的內(nèi)存管理機制在這里不起作用击儡。unsafe關(guān)鍵字是Swift自己加上去的塔沃,作為更進一步的警告吧。
即使我們自己的代碼使用了ARC阳谍,但是Cocoa的內(nèi)部代碼沒有使用ARC蛀柴,這種情況依然能造成內(nèi)存錯誤。例如矫夯,我們將某個對象賦予AVSpeechSynthesizer的delegate屬性鸽疾,如果我們的對象被銷毀了,而這個delegate引用仍然存在训貌,我們就需要手動將nil賦予這個delegate引用制肮。這種情況與我在上一節(jié)中最后說的情況一樣。
Notification##
如果使用addObserver(_:selector:name:object:)方法注冊notification旺订,你其實是在該函數(shù)的第一個參數(shù)作為一個引用傳遞給notification center弄企,通常是self。notification center對這個對象的引用是non-ARC的unsafe的引用区拳,當我們的對象被銷毀后拘领,notification center將面臨懸掛指針的問題。這就是為什么我們必須要在對象銷毀前進行unregister樱调。這種情況與前面講的delegate例子相類似约素。
如果使用addObserver(forName:object:queue:using:)方法注冊notification届良,內(nèi)存管理的問題將更加復雜。
- addObserver(forName:object:queue:using:)方法返回的對象將被notification center retain圣猎,直到我們unregister它士葫。
- addObserver(forName:object:queue:using:)方法的最后一個參數(shù)using是一個closure,它可能會引用self送悔,這就意味著在unregister這個notification之前慢显,notification center還會對self進行retain。這同樣意味著我們不可能在deinit中進行unregister操作欠啤,因為在注冊之后荚藻,unregister之前,deinit不可能被調(diào)用洁段。
- 如果我們自己又引用了addObserver(forName:object:queue:using:)方法返回的對象应狱,該方法的using參數(shù)又引用了self,則形成了一個循環(huán)引用祠丝。
我們看下面的例子:
var observer : Any!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.observer = NotificationCenter.default.addObserver(forName: .woohoo, object:nil, queue:nil) { _ in
print(self.description)
}
}
因為unregister的操作不能在deinit中進行疾呻。
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self.observer)
}
現(xiàn)在完成了register和unregister的操作,但是view controller本身因為有循環(huán)引用的問題而出現(xiàn)了內(nèi)存泄漏的情況写半,可以發(fā)現(xiàn)deinit函數(shù)不會被調(diào)用岸蜗。
deinit {
print("deinit")
}
最簡單的解決方法是在closure中定義捕捉列表,將self聲明為unowned污朽。
self.observer = NotificationCenter.default.addObserver( forName: .woohoo, object:nil, queue:nil) {
[unowned self] _ in
print(self.description)
}
Timer##
Timer類文檔中有說明如下:“run loops maintain strong references to their timers”散吵。
scheduled-Timer(timeInterval:target:selector:userInfo:repeats:) 函數(shù)的文檔說明如下:“The timer main‐tains a strong reference to target until it (the timer) is invalidated.”
上面的文檔已經(jīng)說明當repeat timer沒有被invalidate之前,target參數(shù)(通常是self)是被run loops retain的蟆肆,這意味著在invalidate之前矾睦,target參數(shù)(通常是self)是無法釋放的,這同樣意味著我們不可能在deinit中進行invalidate操作炎功,因為在invalidate之前枚冗,deinit不可能被調(diào)用。與上例相似蛇损,我們可以在viewWillAppear和viewDidDisappear進行相關(guān)的操作:
var timer : Timer!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(fired), userInfo: nil, repeats: true)
self.timer.tolerance = 0.1
}
func fired(_ t:Timer) {
print("timer fired")
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.timer.invalidate()
}
在ios 10中赁温,我們可以使用scheduledTimer(withTimeInterval:repeats:block:)方法,該方法可以deinit函數(shù)中進行invalida淤齐。但是要注意股囊,如果在block參數(shù)(closure)中引用了self,同樣要解決循環(huán)引用的問題更啄。
var timer : Timer!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
[unowned self] // *
t in
self.fired(t)
}
self.timer.tolerance = 0.1
}
func fired(_ t:Timer) {
print("timer fired")
}
deinit {
self.timer.invalidate()
}
Objective-C 屬性#
在Objective-C中稚疹,一個@property的聲明包含一些關(guān)于內(nèi)存管理的描述,例如在UIViewController中祭务,view的屬性描述如下:
@property(null_resettable, nonatomic, strong) UIView *view;
strong表示setter方法將會retain傳遞進來的UIView對象内狗,Swift將上述聲明翻譯如下:
var view: UIView!
Swift默認的聲明就是strong怪嫌。
下面列出幾種Cocoa屬性的內(nèi)存管理描述:
strong, retain (no Swift equivalent):
這兩個描述是一個意思,retain來自于在ARC出現(xiàn)之前的時代柳沙。
weak (Swift weak):
該屬性將會利用ARC功能岩灭,傳遞進來的對象不會被retain,當該對象被銷毀后赂鲤,ARC將會自動賦予nil給該屬性噪径,所以該屬性必須聲明為optional var。
assign (Swift unowned(unsafe)):
ARC的內(nèi)存管理機制在對屬性不起作用蛤袒,如果該屬性所引用的對象被銷毀熄云,該屬性將變成一個懸掛指針。需要我們自己手動賦值為nil妙真。
copy (no Swift equivalent, or @NSCopying):
與strong和retain相似,不同點在于setter方法會通過發(fā)送copy方法拷貝傳遞進來的對象荚守。該對象必須遵循NSCopy珍德。
copy屬性的應用場合是如果一個immutable類有一個mutable子類,(如NSString和NSMutableString, 或者NSArray和NSMutableArray)矗漾,如果setter期望傳進來的是一個immutable類對象锈候,結(jié)果傳進來的是一個mutable子類對象(根據(jù)多態(tài)性)。為防止這種情況發(fā)生敞贡,copy屬性會使setter方法對傳進來的對象調(diào)用copy方法并創(chuàng)建一個新的屬于immutable類實例泵琳。
在Swift中,這種情況不會發(fā)生在string或array上誊役,因為string和array在Swift中實現(xiàn)為struct获列,本身就是值類型,并且通過拷貝的方式進行傳遞蛔垢。所以NSString和NSArray在Swift中對于的String和Array沒有任何特殊的標記击孩。但那些沒有橋接到Swift中的Cocoa類型則需要用到@NSCopying標記。例如UILabel中的attributedText屬性鹏漆,在Swift中聲明如下:
@NSCopying var attributedText: NSAttributedString?
因為NSAttributedString有一個mutable子類NSMutableAttributedString巩梢。
我們同樣可以在我們自己的代碼中使用@NSCopying標記,Swift會負責管理具體的拷貝操作艺玲。如下:
class StringDrawer {
@NSCopying var attributedString : NSAttributedString!
// ...
}
有時我們會遇到這種情況括蝠,我們的類,在類內(nèi)部可以使用mutable饭聚,但是類外部傳進來的必須是immutable的忌警,具體方法就是創(chuàng)建一個private的計算型屬性即可。
class StringDrawer {
@NSCopying var attributedString : NSAttributedString!
private var mutableAttributedString : NSMutableAttributedString! {
get {
if self.attributedString == nil {return nil}
return NSMutableAttributedString(attributedString:self.attributedString)
}
set {
self.attributedString = newValue
}
}
// ...
}