ios 內(nèi)存管理,weak和unowned

懸掛指針及內(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)了retainrelease方法,用于增加或減少引用計數(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)用retainrelease方法了。對引用計數(shù)的工作都由ARC自動完成。ARC被實現(xiàn)為編輯器的一部分,它將在幕后為我們自動加入retainrelease方法。

但即使是在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
Paste_Image.png

這是最典型的強引用循環(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") }
}

此時如圖:

Paste_Image.png

當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!)
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? // ...
}

但是,一些內(nèi)置的Cocoa類使用了ARC不支持的引用(因為它們是非常老舊的代碼或有向后兼容的需求),這種屬性使用了assign關(guān)鍵字,例如,AVSpeechSynthesizer的delegate屬性聲明如下:

@property(nonatomic, assign, nullable) id<AVSpeechSynthesizerDelegate> delegate;

在Swift中,對應(yīng)的聲明如下:

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)存管理的問題將更加復(fù)雜。

  • 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)用。與上例相似,我們可以在viewWillAppearviewDidDisappear進行相關(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屬性的應(yīng)用場合是如果一個immutable類有一個mutable子類,(如NSStringNSMutableString, 或者NSArrayNSMutableArray),如果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,本身就是值類型,并且通過拷貝的方式進行傳遞。所以NSStringNSArray在Swift中對于的StringArray沒有任何特殊的標記。但那些沒有橋接到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 
    }
  }
// ...
}
最后編輯于
?著作權(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)容