iOS 編寫高質(zhì)量Objective-C代碼(五)

級(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)存分配和釋放祖很。首先笛丙,不斷插入retainrelease等內(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è)語句的順序很重要鞋既。
如果先releaseretain力九。那么該對(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)用:retainrelease忌怎、autorelease籍滴、dealloc方法。

  • 使用ARC時(shí)必須遵循的方法命名規(guī)則:
    若方法名以alloc榴啸、new孽惰、copymutableCopy開頭鸥印,則規(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)志怔檩,其性能損失不大褪秀。

最后,還是建議:

  1. 異常只用于處理嚴(yán)重的錯(cuò)誤(fatal error薛训,致命錯(cuò)誤)
  2. 對(duì)于一些不那么嚴(yán)重的錯(cuò)誤(nonfatal error媒吗,非致命錯(cuò)誤),有兩種解決方案:
    • 讓對(duì)象返回nil或者0(例如:初始化的參數(shù)不合法乙埃,方法返回nil或0)
    • 使用NSError

五闸英、以弱引用避免循環(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)景

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市仑嗅,隨后出現(xiàn)的幾起案子宴倍,更是在濱河造成了極大的恐慌张症,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,744評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸵贬,死亡現(xiàn)場(chǎng)離奇詭異俗他,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)阔逼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門兆衅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人嗜浮,你說我怎么就攤上這事羡亩。” “怎么了危融?”我有些...
    開封第一講書人閱讀 163,105評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵畏铆,是天一觀的道長。 經(jīng)常有香客問我吉殃,道長辞居,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,242評(píng)論 1 292
  • 正文 為了忘掉前任蛋勺,我火速辦了婚禮瓦灶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘抱完。我一直安慰自己贼陶,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,269評(píng)論 6 389
  • 文/花漫 我一把揭開白布乾蛤。 她就那樣靜靜地躺著每界,像睡著了一般。 火紅的嫁衣襯著肌膚如雪家卖。 梳的紋絲不亂的頭發(fā)上眨层,一...
    開封第一講書人閱讀 51,215評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音上荡,去河邊找鬼趴樱。 笑死,一個(gè)胖子當(dāng)著我的面吹牛酪捡,可吹牛的內(nèi)容都是我干的叁征。 我是一名探鬼主播,決...
    沈念sama閱讀 40,096評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼逛薇,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼捺疼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起永罚,我...
    開封第一講書人閱讀 38,939評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤啤呼,失蹤者是張志新(化名)和其女友劉穎卧秘,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體官扣,經(jīng)...
    沈念sama閱讀 45,354評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡翅敌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,573評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了惕蹄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蚯涮。...
    茶點(diǎn)故事閱讀 39,745評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖卖陵,靈堂內(nèi)的尸體忽然破棺而出遭顶,到底是詐尸還是另有隱情,我是刑警寧澤赶促,帶...
    沈念sama閱讀 35,448評(píng)論 5 344
  • 正文 年R本政府宣布液肌,位于F島的核電站,受9級(jí)特大地震影響鸥滨,放射性物質(zhì)發(fā)生泄漏嗦哆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,048評(píng)論 3 327
  • 文/蒙蒙 一婿滓、第九天 我趴在偏房一處隱蔽的房頂上張望老速。 院中可真熱鬧,春花似錦凸主、人聲如沸橘券。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽旁舰。三九已至,卻和暖如春嗡官,著一層夾襖步出監(jiān)牢的瞬間箭窜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評(píng)論 1 269
  • 我被黑心中介騙來泰國打工衍腥, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留磺樱,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,776評(píng)論 2 369
  • 正文 我出身青樓婆咸,卻偏偏與公主長得像竹捉,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子尚骄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,652評(píng)論 2 354