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通過copymutableCopy或任何帶有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中绍载,對應的聲明如下:

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)用。與上例相似蛇损,我們可以在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屬性的應用場合是如果一個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)系作者
  • 序言:七十年代末若治,一起剝皮案震驚了整個濱河市慨蓝,隨后出現(xiàn)的幾起案子感混,更是在濱河造成了極大的恐慌,老刑警劉巖礼烈,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件弧满,死亡現(xiàn)場離奇詭異,居然都是意外死亡此熬,警方通過查閱死者的電腦和手機庭呜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來犀忱,“玉大人募谎,你說我怎么就攤上這事∫趸悖” “怎么了数冬?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長搀庶。 經(jīng)常有香客問我拐纱,道長,這世上最難降的妖魔是什么哥倔? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任秸架,我火速辦了婚禮,結(jié)果婚禮上咆蒿,老公的妹妹穿的比我還像新娘东抹。我一直安慰自己,他們只是感情好沃测,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布缭黔。 她就那樣靜靜地躺著,像睡著了一般芽突。 火紅的嫁衣襯著肌膚如雪试浙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天寞蚌,我揣著相機與錄音田巴,去河邊找鬼。 笑死挟秤,一個胖子當著我的面吹牛壹哺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播艘刚,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼管宵,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起箩朴,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤岗喉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后炸庞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钱床,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年埠居,在試婚紗的時候發(fā)現(xiàn)自己被綠了查牌。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡滥壕,死狀恐怖纸颜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情绎橘,我是刑警寧澤胁孙,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站称鳞,受9級特大地震影響浊洞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜胡岔,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望枷餐。 院中可真熱鬧靶瘸,春花似錦、人聲如沸毛肋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽润匙。三九已至诗眨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間孕讳,已是汗流浹背匠楚。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留厂财,地道東北人芋簿。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像璃饱,于是被迫代替她去往敵國和親与斤。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

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