ios 內存管理,weak和unowned
懸掛指針及內存泄漏
如果對象a指向對象b,若對象b被釋放了,則此時對象a指向一個未知地址,這種情況叫做懸掛指針。
如果對象a指向對象b,若對象a被釋放了,則此時沒有任何對象能夠指向對象b,對象b無法被釋放,這種情況叫做內存泄漏。
手動內存管理
在ARC出現之前,ios的內存管理是基于手動內存管理,也叫做MRC。
為了防止懸掛指針及內存泄漏,手動內存管理基于一個引用計數(retain count)的概念,所有對象都可以增加或減少一個對象的引用計數,當對象的引用計數大于0,則該對象繼續存在;當該對象的引用計數減少到0,則該對象自動銷毀。NSObject實現了 retain 和 release 方法,用于增加或減少引用計數。
具體的規則如下:
- 如果對象a通過調用初始化函數初始化了對象b,則初始化函數增加b的引用計數。
- 如果對象a通過 copy , mutableCopy 或任何帶有 copy 字樣的方法獲得一個對象b的拷貝,則這些copy方法負責增加這個新的b的拷貝的引用計數。
- 如果對象a直接獲取一個對象b的引用(不是通過初始化或拷貝的方法),則對象a自己負責增加對象b的引用計數。
- 如果對象a之前直接或間接的增加了對象b的引用計數,當對象a不再需要對象b的引用之后,對象a要負責減少對象b的引用計數。如果對象b的引用計數減少到0,則對象b被釋放。
ARC
ARC(以及Swift)出現之后,我們不能再調用 retain 和 release 方法了。對引用計數的工作都由ARC自動完成。ARC被實現為編輯器的一部分,它將在幕后為我們自動加入 retain 和 release 方法。
但即使是在ARC的環境下,仍然有一些內存管理問題需要我們注意或進行手動處理。
循環引用
在這里我直接使用官方文檔的例子:
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</code></pre>

Paste_Image.png
這是最典型的強引用循環,當john和unit4A釋放各自的對象之后,Person和Apartment的實例不會銷毀,因為它們在互相引用對方。具體的解決辦法有weak和unowned。
Weak
將上例的tenant屬性聲明為weak即可解決循環引用的問題。
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") }
}</code></pre>
此時如圖:

Paste_Image.png
當john釋放時,Person實例因為沒有strong引用,所以也被銷毀了,tenant屬性是weak引用,所以被ARC自動設置為nil。循環引用的問題就此解決。
Weak引用利用了ARC的功能,當一個引用被聲明為weak的時候,ARC并不會retain這個對象。ARC會記錄所有的weak引用以及它們所指向的對象,當某個對象的引用計數降到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!)</code></pre>

Paste_Image.png
這個例子是使用unowned引用最安全的方法,因為unowned引用所指向的對象擁有與unowned引用相同的或更長的生命期限,我們可以理解為兩個對象一損俱損的情況下,使用unowned最安全。
但是,如果此時CreditCard實例有另外一個對象指向它,那么當john被釋放了,Customer實例也被銷毀了,但是CreditCard實例仍然存在,CreditCard類中的customer屬性將指向一個懸掛指針,我們需要手動的將nil賦予customer屬性。這種情況也是非常常見的。
一些特殊例子
ARC不支持的delegate
使用weak引用最常見的情況就是在delegate中,如下:
class ColorPickerController : UIViewController {
weak var delegate: ColorPickerDelegate? // ...
}
但是,一些內置的Cocoa類使用了ARC不支持的引用(因為它們是非常老舊的代碼或有向后兼容的需求),這種屬性使用了 assign 關鍵字,例如, AVSpeechSynthesizer 的delegate屬性聲明如下:
@property(nonatomic, assign, nullable) id<AVSpeechSynthesizerDelegate> delegate;
在Swift中,對應的聲明如下:
unowned(unsafe) var delegate: AVSpeechSynthesizerDelegate?
在Swift中, unowned 和Objective-C的 assign 意義相同,都意味著ARC的內存管理機制在這里不起作用。 unsafe 關鍵字是Swift自己加上去的,作為更進一步的警告吧。
即使我們自己的代碼使用了ARC,但是Cocoa的內部代碼沒有使用ARC,這種情況依然能造成內存錯誤。例如,我們將某個對象賦予 AVSpeechSynthesizer 的delegate屬性,如果我們的對象被銷毀了,而這個delegate引用仍然存在,我們就需要手動將nil賦予這個delegate引用。這種情況與我在上一節中最后說的情況一樣。
Notification
如果使用 addObserver(_:selector:name:object:) 方法注冊notification,你其實是在該函數的第一個參數作為一個引用傳遞給notification center,通常是self。notification center對這個對象的引用是non-ARC的unsafe的引用,當我們的對象被銷毀后,notification center將面臨懸掛指針的問題。這就是為什么我們必須要在對象銷毀前進行unregister。這種情況與前面講的delegate例子相類似。
如果使用 addObserver(forName:object:queue:using:) 方法注冊notification,內存管理的問題將更加復雜。
- addObserver(forName:object:queue:using:) 方法返回的對象將被notification center retain,直到我們unregister它。
- addObserver(forName:object:queue:using:) 方法的最后一個參數using是一個closure,它可能會引用self,這就意味著在unregister這個notification之前,notification center還會對self進行retain。這同樣意味著我們不可能在 deinit 中進行unregister操作,因為在注冊之后,unregister之前, deinit 不可能被調用。
- 如果我們自己又引用了 addObserver(forName:object:queue:using:) 方法返回的對象,該方法的using參數又引用了self,則形成了一個循環引用。
我們看下面的例子:
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)
}
現在完成了register和unregister的操作,但是view controller本身因為有循環引用的問題而出現了內存泄漏的情況,可以發現 deinit 函數不會被調用。
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:) 函數的文檔說明如下:“The timer main‐tains a strong reference to target until it (the timer) is invalidated.”
上面的文檔已經說明當repeat timer沒有被invalidate之前,target參數(通常是self)是被run loops retain的,這意味著在invalidate之前,target參數(通常是self)是無法釋放的,這同樣意味著我們不可能在 deinit 中進行invalidate操作,因為在invalidate之前, deinit 不可能被調用。與上例相似,我們可以在 viewWillAppear 和 viewDidDisappear 進行相關的操作:
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 函數中進行invalida。但是要注意,如果在block參數(closure)中引用了self,同樣要解決循環引用的問題。
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 的聲明包含一些關于內存管理的描述,例如在 UIViewController 中,view的屬性描述如下:
@property(null_resettable, nonatomic, strong) UIView *view;
strong表示setter方法將會retain傳遞進來的UIView對象,Swift將上述聲明翻譯如下:
var view: UIView!
Swift默認的聲明就是strong。
下面列出幾種Cocoa屬性的內存管理描述:
strong, retain (no Swift equivalent) :
這兩個描述是一個意思,retain來自于在ARC出現之前的時代。
weak (Swift weak) :
該屬性將會利用ARC功能,傳遞進來的對象不會被retain,當該對象被銷毀后,ARC將會自動賦予nil給該屬性,所以該屬性必須聲明為optional var。
assign (Swift unowned(unsafe)) :
ARC的內存管理機制在對屬性不起作用,如果該屬性所引用的對象被銷毀,該屬性將變成一個懸掛指針。需要我們自己手動賦值為nil。
copy (no Swift equivalent, or @NSCopying) :
與strong和retain相似,不同點在于setter方法會通過發送 copy 方法拷貝傳遞進來的對象。該對象必須遵循NSCopy。
copy 屬性的應用場合是如果一個immutable類有一個mutable子類,(如 NSString 和 NSMutableString , 或者 NSArray 和 NSMutableArray ),如果setter期望傳進來的是一個immutable類對象,結果傳進來的是一個mutable子類對象(根據多態性)。為防止這種情況發生, copy 屬性會使setter方法對傳進來的對象調用copy方法并創建一個新的屬于immutable類實例。
在Swift中,這種情況不會發生在string或array上,因為string和array在Swift中實現為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!
// ...
}
有時我們會遇到這種情況,我們的類,在類內部可以使用mutable,但是類外部傳進來的必須是immutable的,具體方法就是創建一個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
}
}
// ...
}
來自:http://www.jianshu.com/p/09a0b5ce35f8