級(jí)別: ★★☆☆☆
標(biāo)簽:「iOS」「內(nèi)存管理」「Objective-C」
作者: MrLiuQ
審校: QiShare團(tuán)隊(duì)
前言:
這幾篇文章是小編在鉆研《Effective Objective-C 2.0》的知識(shí)產(chǎn)出毯辅,其中包含作者和小編的觀點(diǎn)晋渺,以及小編整理的一些demo。希望能幫助大家以簡(jiǎn)潔的文字快速領(lǐng)悟原作者的精華叉钥。
在這里摊册,QiShare團(tuán)隊(duì)向原作者M(jìn)att Galloway表達(dá)誠摯的敬意肤京。
文章目錄如下:
iOS 編寫高質(zhì)量Objective-C代碼(一)
iOS 編寫高質(zhì)量Objective-C代碼(二)
iOS 編寫高質(zhì)量Objective-C代碼(三)
iOS 編寫高質(zhì)量Objective-C代碼(四)
iOS 編寫高質(zhì)量Objective-C代碼(五)
iOS 編寫高質(zhì)量Objective-C代碼(六)
iOS 編寫高質(zhì)量Objective-C代碼(七)
iOS 編寫高質(zhì)量Objective-C代碼(八)
本篇的主題是iOS中的 “內(nèi)存管理機(jī)制”。
說到iOS內(nèi)存管理茅特,逃不過iOS的兩種內(nèi)存管理機(jī)制:MRC & ARC忘分。
先簡(jiǎn)單介紹一下:
MRC(manual reference counting): “手動(dòng)引用計(jì)數(shù)” ,由開發(fā)者管理內(nèi)存白修。
ARC(automatic reference counting):“自動(dòng)引用計(jì)數(shù)”妒峦,從iOS 5
開始支持,由編譯器幫忙管理內(nèi)存兵睛。
蘋果引入ARC機(jī)制的原因猜測(cè):
iOS 4
之前肯骇,所有iOS開發(fā)者必須要手動(dòng)管理內(nèi)存,即手動(dòng)管理對(duì)象的內(nèi)存分配和釋放祖很。首先笛丙,不斷插入retain
、release
等內(nèi)存管理語句假颇,大大加大了工作量和代碼量胚鸯。其次,在面對(duì)一些多線程并發(fā)操作時(shí)拆融,開發(fā)者手動(dòng)管理內(nèi)存并不簡(jiǎn)單蠢琳,還可能會(huì)帶來很多無法預(yù)知的問題。
所以镜豹,蘋果從iOS 5
開始引入ARC機(jī)制傲须,由編譯器幫忙管理內(nèi)存。在編譯期趟脂,編譯器會(huì)自動(dòng)加上內(nèi)存管理語句泰讽。這樣,開發(fā)者可以更加關(guān)注業(yè)務(wù)邏輯。
下面進(jìn)入正題:編寫高質(zhì)量Objective-C代碼(五)——內(nèi)存管理篇已卸。
一佛玄、理解引用計(jì)數(shù)
- 引用計(jì)數(shù)工作原理:
這里引入《Objective-C 高級(jí)編程 iOS與OSX多線程和內(nèi)存管理》這本書的例子:
很經(jīng)典的圖解:
解釋:
1.開燈:引申為:“ 創(chuàng)建對(duì)象 ”。
2.關(guān)燈:引申為:“ 銷毀對(duì)象 ”累澡。
解釋:
1.有人來上班打卡了:開燈梦抢。——(創(chuàng)建對(duì)象愧哟,計(jì)數(shù)為1)
2.又有人來了:保持開燈奥吩。——(保持對(duì)象蕊梧,計(jì)數(shù)為2)
3.又有人來了:保持開燈霞赫。——(保持對(duì)象肥矢,計(jì)數(shù)為3)
4.有人下班打卡了:保持開燈端衰。——(保持對(duì)象甘改,計(jì)數(shù)為2)
5.又有人下班了:保持開燈旅东。——(保持對(duì)象楼誓,計(jì)數(shù)為1)
6.所有員工全下班了:關(guān)燈玉锌∶樱——(銷毀對(duì)象疟羹,計(jì)數(shù)為0)
場(chǎng)景 | 對(duì)應(yīng)OC的動(dòng)作 | 對(duì)應(yīng)OC的方法 |
---|---|---|
上班開燈 | 生成對(duì)象 | alloc/new/copy/mutableCopy等 |
需要照明 | 持有對(duì)象 | retain |
不需要照明 | 解除持有 | release |
下班關(guān)燈 | 銷毀對(duì)象 | dealloc |
如果覺得本書中的例子說的有點(diǎn)抽象難懂,沒關(guān)系禀倔,請(qǐng)看下面圖解示例:
提示:實(shí)箭頭為強(qiáng)引用榄融,虛箭頭為弱引用。
- 屬性存取方法中的內(nèi)存管理:
這里有個(gè)set方法的例子:
- (void)setObject:(id)object {
[object retain];// Added by ARC
[_object release];// Added by ARC
_object = object;
}
解釋:set方法將保留新值救湖,釋放舊值愧杯,然后更新實(shí)例變量。這三個(gè)語句的順序很重要鞋既。
如果先release
再retain
力九。那么該對(duì)象可能已經(jīng)被回收,此時(shí)retain
操作無效邑闺,因?yàn)閷?duì)象已釋放跌前。這時(shí)實(shí)例變量就變成了懸掛指針。懸掛指針:指針指nil的指針陡舅。
- 自動(dòng)釋放池:
細(xì)心的同學(xué)會(huì)發(fā)現(xiàn)抵乓,在我們寫iOS程序時(shí),main
函數(shù)里就有一個(gè)autoreleasepool
(自動(dòng)釋放池)。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
autorelease
能延長對(duì)象的生命周期灾炭,在對(duì)象跨越“方法調(diào)用邊界”后(就是}
后)依然可以存活一段時(shí)間茎芋。
- 循環(huán)引用:
循環(huán)引用(retain cycle
)又稱為“保留環(huán)”。
形成循環(huán)引用的原因:是對(duì)象之間互相通過強(qiáng)指針指向?qū)Ψ剑ɑ蛘哒f互相強(qiáng)持有對(duì)方)蜈出。
在開發(fā)中田弥,我們不希望出現(xiàn)循環(huán)引用,因?yàn)闀?huì)造成內(nèi)存泄漏铡原。
解決方案:有一方使用弱引用(weak reference
)皱蹦,解開循環(huán)引用,讓多個(gè)對(duì)象都可以釋放眷蜈。
PS:關(guān)于如何檢驗(yàn)項(xiàng)目中有無內(nèi)存泄漏:參考這篇博客沪哺。
二、以ARC簡(jiǎn)化引用計(jì)數(shù)
酌儒,在ARC環(huán)境下辜妓,禁止??調(diào)用:retain
、release
忌怎、autorelease
籍滴、dealloc
方法。
使用ARC時(shí)必須遵循的方法命名規(guī)則:
若方法名以alloc
榴啸、new
孽惰、copy
、mutableCopy
開頭鸥印,則規(guī)定返回的對(duì)象歸調(diào)用者勋功。變量的內(nèi)存管理語義:
對(duì)比一下MRC和ARC在代碼上的區(qū)別
MRC環(huán)境下:
- (void)setObject:(id)object {
[_object release];
_object = [object retain];
}
這樣會(huì)出現(xiàn)一種邊界情況,如果新值和舊值是同一個(gè)對(duì)象库说,那么會(huì)先釋放掉狂鞋,object就變成懸掛指針。
ARC環(huán)境下:
- (void)setObject:(id)object {
_object = object;
}
ARC會(huì)用一種更安全的方式解決邊界問題:先保留新值潜的,再釋放舊值骚揍,最后更新實(shí)例變量。
同時(shí)啰挪,ARC可以通過修飾符來改變局部變量和實(shí)例變量的語義:
修飾符 | 語義 |
---|---|
__strong | 默認(rèn)信不,強(qiáng)持有,保留此值亡呵。 |
__weak | 不保留此值抽活,安全。對(duì)象釋放后政己,指針置nil酌壕。 |
__unsafe_unretained | 不保留此值掏愁,不安全。對(duì)象釋放后卵牍,指針依然指向原地址(即不置nil)果港。 |
__autoreleasing | 此值在方法返回時(shí)自動(dòng)釋放。 |
- ARC如何清理實(shí)例變量:
MRC中糊昙,開發(fā)者需要在dealloc
中動(dòng)插入必要的清理代碼(cleanup code)辛掠。
而ARC會(huì)借用Objective-C++
的一項(xiàng)特性來完成清理任務(wù),回收OC++對(duì)象時(shí)释牺,會(huì)調(diào)用C++的析構(gòu)函數(shù):底層走.cxx_destruct
方法萝衩。而當(dāng)釋放OC對(duì)象時(shí),ARC在.cxx_destruct
底層方法中添加所需要的清理代碼(這個(gè)方法底層的某個(gè)時(shí)機(jī)會(huì)調(diào)用dealloc
方法)没咙。
不過如果有非OC的對(duì)象猩谊,還是要重寫dealloc
方法。比如CoreFoundation
中的對(duì)象或是malloc()
分配在堆中的內(nèi)存依然需要清理祭刚。這時(shí)要適時(shí)調(diào)用CFRetain
/CFRelease
牌捷。
- (void)dealloc {
CFRelease(_coreFoundationObject);
free(_heapAllocatedMemoryBlob);
}
三、dealloc方法中只釋放引用并解除監(jiān)聽
調(diào)用dealloc
方法時(shí)涡驮,對(duì)象已經(jīng)處于回收狀態(tài)了暗甥。這時(shí)不能調(diào)用其他方法,尤其是異步執(zhí)行某些任務(wù)又要回調(diào)的方法捉捅。如果異步執(zhí)行完回調(diào)的時(shí)候?qū)ο笠呀?jīng)摧毀撤防,會(huì)直接crash。
dealloc
方法里要做些釋放相關(guān)的事情棒口,比如:
- 釋放指向其他對(duì)象的引用寄月。
- 取消訂閱KVO。
- 取消NSNotificationCenter通知陌凳。
舉個(gè)例子:
- KVO:
- (void)viewDidLoad {
//....
[webView addObserver:self forKeyPath:@"canGoBack" options:NSKeyValueObservingOptionNew context:nil];
[webView addObserver:self forKeyPath:@"canGoForward" options:NSKeyValueObservingOptionNew context:nil];
[webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:nil];
[webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
self.backItem.enabled = self.webView.canGoBack;
self.forwardItem.enabled = self.webView.canGoForward;
self.title = self.webView.title;
self.progressView.progress = self.webView.estimatedProgress;
self.progressView.hidden = self.webView.estimatedProgress>=1;
}
- (void)dealloc {
[self.webView removeObserver:self forKeyPath:@"canGoBack"];//< 移除KVO
[self.webView removeObserver:self forKeyPath:@"canGoForward"];
[self.webView removeObserver:self forKeyPath:@"title"];
[self.webView removeObserver:self forKeyPath:@"estimatedProgress"];
}
- NSNotificationCenter:
- (void)viewDidLoad {
//......
// 添加響應(yīng)通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(tabBarBtnRepeatClick) name:BQTabBarButtonDidRepeatClickNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(titleBtnRepeatClick) name:BQTitleButtonDidRepeatClickNotification object:nil];
}
// 移除通知
- (void)dealloc {
// [[NSNotificationCenter defaultCenter] removeObserver:self name:BQTabBarButtonDidRepeatClickNotification object:nil];
// [[NSNotificationCenter defaultCenter] removeObserver:self name:BQTitleButtonDidRepeatClickNotification object:nil];
// 或者使用一個(gè)語句全部移除
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
四剥懒、編寫“ 異常安全代碼 ”時(shí)留意內(nèi)存管理問題
異常只應(yīng)在發(fā)生嚴(yán)重錯(cuò)誤后拋出。
用的不好會(huì)造成內(nèi)存泄漏:在try
塊中合敦,如果先保留了某個(gè)對(duì)象,然后在釋放它之前又拋出了異常验游,那么除非catch塊能解決問題充岛,否則對(duì)象所占內(nèi)存就會(huì)泄漏。
原因:C++
的析構(gòu)函數(shù)由Objective-C
的異常處理例程來運(yùn)行耕蝉。由于拋出異常會(huì)縮短生命期崔梗,所以發(fā)生異常時(shí)必須析構(gòu),不然就內(nèi)存泄漏垒在,而這時(shí)如果文件句柄(file handle)等系統(tǒng)資源沒有正確清理蒜魄,就會(huì)發(fā)生內(nèi)存泄漏。
- 捕獲異常時(shí),一定要將
try
塊內(nèi)所創(chuàng)立的對(duì)象清理干凈谈为。 - ARC下旅挤,編譯器默認(rèn)不生成安全處理異常所需的清理代碼。如要開啟伞鲫,請(qǐng)手動(dòng)打開:
-fobjc-arc-exceptions
標(biāo)志粘茄。但很影響性能。所以建議最好還是不要用秕脓。但有種情況是可以使用的:Objective-C++
模式柒瓣。
PS:在運(yùn)行期系統(tǒng),C++
與Objective-C
的異撤图埽互相兼容芙贫。也就是說其中任一語言拋出的異常,能用另一語言所編的“異常處理程序”捕獲傍药。而在編寫Objective-C++
代碼時(shí)屹培,C++處理異常所用的代碼與ARC實(shí)現(xiàn)的附加代碼類似,編譯器自動(dòng)打開-fobjc-arc-exceptions
標(biāo)志怔檩,其性能損失不大褪秀。
最后,還是建議:
- 異常只用于處理嚴(yán)重的錯(cuò)誤(fatal error薛训,致命錯(cuò)誤)
- 對(duì)于一些不那么嚴(yán)重的錯(cuò)誤(nonfatal error媒吗,非致命錯(cuò)誤),有兩種解決方案:
- 讓對(duì)象返回
nil
或者0
(例如:初始化的參數(shù)不合法乙埃,方法返回nil或0) - 使用
NSError
- 讓對(duì)象返回
五闸英、以弱引用避免循環(huán)引用(避免內(nèi)存泄漏)
這條比較簡(jiǎn)單,內(nèi)容主旨就是標(biāo)題:以弱引用避免循環(huán)引用(Retain Cycle)
- 為了避免因循環(huán)引用而造成內(nèi)存泄漏介袜。這時(shí)甫何,某些引用需要設(shè)置為弱引用(
weak
)。 - 使用弱引用
weak
遇伞,ARC下辙喂,對(duì)象釋放時(shí),指針會(huì)置nil
鸠珠。
六巍耗、以 “自動(dòng)釋放池塊” 降低內(nèi)存峰值
- 默認(rèn)情況下:自動(dòng)釋放池需要等待線程執(zhí)行下一次事件循環(huán)時(shí)才清空,通常for循環(huán)會(huì)不斷創(chuàng)建新對(duì)象加入自動(dòng)釋放池里渐排,循環(huán)結(jié)束才釋放炬太。因此,可能會(huì)占用大量?jī)?nèi)存驯耻。
- 手動(dòng)加入自動(dòng)釋放池塊(
@autoreleasepool
):每次for循環(huán)都會(huì)直接釋放內(nèi)存亲族,從而降低了內(nèi)存的峰值炒考。
尤其,在遍歷處理一些大數(shù)組或者大字典的時(shí)候霎迫,可以使用自動(dòng)釋放池來降低內(nèi)存峰值斋枢,例如:
NSArray *qiShare = /*一個(gè)很大的數(shù)組*/
NSMutableArray *qiShareMembersArray = [NSMutableArray new];
for (NSStirng *name in qiShare) {
@autoreleasepool {
QiShareMember *member = [QiShareMember alloc] initWithName:name];
[qiShareMembersArray addObject:member];
}
}
PS:自動(dòng)釋放池的原理:排布在“棧”中女气,對(duì)象執(zhí)行autorelease消息后杏慰,系統(tǒng)將其放入最頂端的池里(進(jìn)棧),而清空自動(dòng)釋放池就是把對(duì)象銷毀(出棧)炼鞠。而調(diào)用出棧的時(shí)機(jī):就是當(dāng)前線程執(zhí)行下一次事件循環(huán)時(shí)缘滥。
七、用 “僵尸對(duì)象” 調(diào)試內(nèi)存管理問題
如上圖谒主,勾選這里可以開啟僵尸對(duì)象設(shè)置朝扼。開啟之后,系統(tǒng)在回收對(duì)象時(shí)霎肯,不將其真正的回收擎颖,而是把它的isa指針
指向特殊的僵尸類(zombie class),變成僵尸對(duì)象观游。僵尸類能夠響應(yīng)所有的選擇子搂捧,響應(yīng)方式為:打印一條包含消息內(nèi)容以及其接收者的消息,然后終止應(yīng)用程序懂缕。
僵尸對(duì)象簡(jiǎn)單原理:在Objective-C的運(yùn)行期程序庫允跑、Foundation框架以及CoreFoundation框架的底層加入了實(shí)現(xiàn)代碼。在系統(tǒng)即將回收對(duì)象時(shí)搪柑,通過一個(gè)環(huán)境變量
NSZombieEnabled
識(shí)別是僵尸對(duì)象——不徹底回收聋丝,isa
指針指向僵尸類并且響應(yīng)所有選擇子。
八工碾、不要使用retainCount
在蘋果引入ARC之后retainCount已經(jīng)正式廢棄弱睦,任何時(shí)候都沒法調(diào)用這個(gè)retainCount方法來查看引用計(jì)數(shù)了,因?yàn)檫@個(gè)值實(shí)際上已經(jīng)沒有準(zhǔn)確性了(而且在ARC環(huán)境下也調(diào)用不了)渊额。但是在MRC下還是可以正常使用的况木。
最后,特別致謝:《Effective Objective-C 2.0》第五章端圈。
了解更多iOS及相關(guān)新技術(shù)焦读,請(qǐng)關(guān)注我們的公眾號(hào):
關(guān)注我們的途徑有:
QiShare(簡(jiǎn)書)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公眾號(hào))
推薦文章:
iOS與JS交互之WKWebView-WKUIDelegate協(xié)議
如果360推出辣椒水,各位女士會(huì)買嗎舱权?
從撒狗糧帶你了解WoT連接場(chǎng)景