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

級別: ★★☆☆☆
標籤:「iOS」「內存管理」「Objective-C」
做者: MrLiuQ
審校: QiShare團隊php

前言: 這幾篇文章是小編在鑽研《Effective Objective-C 2.0》的知識產出,其中包含做者和小編的觀點,以及小編整理的一些demo。但願能幫助你們以簡潔的文字快速領悟原做者的精華。 在這裏,QiShare團隊向原做者Matt Galloway表達誠摯的敬意。html

文章目錄以下:
iOS 編寫高質量Objective-C代碼(一)
iOS 編寫高質量Objective-C代碼(二)
iOS 編寫高質量Objective-C代碼(三)
iOS 編寫高質量Objective-C代碼(四)
iOS 編寫高質量Objective-C代碼(五)
iOS 編寫高質量Objective-C代碼(六)
iOS 編寫高質量Objective-C代碼(七)
iOS 編寫高質量Objective-C代碼(八)git


本篇的主題是iOS中的 「內存管理機制」github

說到iOS內存管理,逃不過iOS的兩種內存管理機制:MRC & ARC
先簡單介紹一下:
MRC(manual reference counting): 「手動引用計數」 ,由開發者管理內存。 ARC(automatic reference counting):「自動引用計數」,從iOS 5開始支持, 由編譯器幫忙管理內存。web

蘋果引入ARC機制的緣由猜想:

iOS 4以前,全部iOS開發者必需要手動管理內存,即手動管理對象的內存分配和釋放。首先,不斷插入retainrelease等內存管理語句,大大加大了工做量和代碼量。其次,在面對一些多線程併發操做時,開發者手動管理內存並不簡單,還可能會帶來不少沒法預知的問題。
因此,蘋果從iOS 5開始引入ARC機制,由編譯器幫忙管理內存。在編譯期,編譯器會自動加上內存管理語句。這樣,開發者能夠更加關注業務邏輯。編程

下面進入正題:編寫高質量Objective-C代碼(五)——內存管理篇數組

1、理解引用計數

  • 引用計數工做原理:

這裏引入《Objective-C 高級編程 iOS與OSX多線程和內存管理》這本書的例子: 很經典的圖解:安全

解釋:
1.開燈:引伸爲:「 建立對象 」
2.關燈:引伸爲:「 銷燬對象 」
bash

解釋:
1.有人來上班打卡了:開燈。——(建立對象,計數爲1)
2.又有人來了:保持開燈。——(保持對象,計數爲2)
3.又有人來了:保持開燈。——(保持對象,計數爲3)
4.有人下班打卡了:保持開燈。——(保持對象,計數爲2)
5.又有人下班了:保持開燈。——(保持對象,計數爲1)
6.全部員工全下班了:關燈。——(銷燬對象,計數爲0)微信


場景 對應OC的動做 對應OC的方法
上班開燈 生成對象 alloc/new/copy/mutableCopy等
須要照明 持有對象 retain
不須要照明 解除持有 release
下班關燈 銷燬對象 dealloc

若是以爲本書中的例子說的有點抽象難懂,不要緊,請看下面圖解示例:
提示:實箭頭爲強引用,虛箭頭爲弱引用。

  • 屬性存取方法中的內存管理:

這裏有個set方法的例子:

- (void)setObject:(id)object {

   [object retain];// Added by ARC
   [_object release];// Added by ARC

   _object = object; 
}
複製代碼

解釋:set方法將保留新值,釋放舊值,而後更新實例變量。這三個語句的順序很重要。 若是先releaseretain。那麼該對象可能已經被回收,此時retain操做無效,由於對象已釋放。這時實例變量就變成了懸掛指針。(懸掛指針:指針指nil的指針。)

  • 自動釋放池: 細心的同窗會發現,在咱們寫iOS程序時,main函數裏就有一個autoreleasepool(自動釋放池)。
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
複製代碼

autorelease能延長對象的生命週期,在對象跨越「方法調用邊界」後(就是}後)依然能夠存活一段時間。

  • 循環引用:

循環引用(retain cycle)又稱爲「保留環」。 造成循環引用的緣由:是對象之間互相經過強指針指向對方(或者說互相強持有對方)。 在開發中,咱們不但願出現循環引用,由於會形成內存泄漏。 解決方案:有一方使用弱引用(weak reference),解開循環引用,讓多個對象均可以釋放。 PS:關於如何檢驗項目中有無內存泄漏:參考這篇博客

2、以ARC簡化引用計數

,在ARC環境下,禁止🚫調用:retainreleaseautoreleasedealloc方法。

  • 使用ARC時必須遵循的方法命名規則: 若方法名以allocnewcopymutableCopy開頭,則規定返回的對象歸調用者。

  • 變量的內存管理語義:

對比一下MRC和ARC在代碼上的區別

MRC環境下:

- (void)setObject:(id)object {

    [_object release];
    _object = [object retain];
}
複製代碼

這樣會出現一種邊界狀況,若是新值和舊值是同一個對象,那麼會先釋放掉,object就變成懸掛指針。

ARC環境下:

- (void)setObject:(id)object {

    _object = object;
}
複製代碼

ARC會用一種更安全的方式解決邊界問題:先保留新值,再釋放舊值,最後更新實例變量。

同時,ARC能夠經過修飾符來改變局部變量和實例變量的語義:

修飾符 語義
__strong 默認,強持有,保留此值。
__weak 不保留此值,安全。對象釋放後,指針置nil。
__unsafe_unretained 不保留此值,不安全。對象釋放後,指針依然指向原地址(即不置nil)。
__autoreleasing 此值在方法返回時自動釋放。
  • ARC如何清理實例變量:

MRC中,開發者須要在dealloc中動插入必要的清理代碼(cleanup code)。 而ARC會借用Objective-C++的一項特性來完成清理任務,回收OC++對象時,會調用C++的析構函數:底層走.cxx_destruct方法。而當釋放OC對象時,ARC在.cxx_destruct底層方法中添加所須要的清理代碼(這個方法底層的某個時機會調用dealloc方法)。 不過若是有非OC的對象,仍是要重寫dealloc方法。好比CoreFoundation中的對象或是malloc()分配在堆中的內存依然須要清理。這時要適時調用CFRetain/CFRelease

- (void)dealloc {

   CFRelease(_coreFoundationObject);
   free(_heapAllocatedMemoryBlob);
}
複製代碼

3、dealloc方法中只釋放引用並解除監聽

調用dealloc方法時,對象已經處於回收狀態了。這時不能調用其餘方法,尤爲是異步執行某些任務又要回調的方法。若是異步執行完回調的時候對象已經摧毀,會直接crash。

dealloc方法裏要作些釋放相關的事情,好比:

  • 釋放指向其餘對象的引用。
  • 取消訂閱KVO。
  • 取消NSNotificationCenter通知。

舉個例子:

  • 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 {

    //......

    // 添加響應通知
    [[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];

    // 或者使用一個語句所有移除
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
複製代碼

4、編寫「 異常安全代碼 」時留意內存管理問題

異常只應在發生嚴重錯誤後拋出。
用的很差會形成內存泄漏:在try塊中,若是先保留了某個對象,而後在釋放它以前又拋出了異常,那麼除非catch塊能解決問題,不然對象所佔內存就會泄漏。

緣由:C++的析構函數由Objective-C的異常處理例程來運行。因爲拋出異常會縮短生命期,因此發生異常時必須析構,否則就內存泄漏,而這時若是文件句柄(file handle)等系統資源沒有正確清理,就會發生內存泄漏。

  • 捕獲異常時,必定要將try塊內所創立的對象清理乾淨。
  • ARC下,編譯器默認不生成安全處理異常所需的清理代碼。如要開啓,請手動打開:-fobjc-arc-exceptions標誌。但很影響性能。因此建議最好仍是不要用。但有種狀況是可使用的:Objective-C++模式。

PS:在運行期系統,C++Objective-C的異常互相兼容。也就是說其中任一語言拋出的異常,能用另外一語言所編的**「異常處理程序」**捕獲。而在編寫Objective-C++代碼時,C++處理異常所用的代碼與ARC實現的附加代碼相似,編譯器自動打開-fobjc-arc-exceptions標誌,其性能損失不大。

最後,仍是建議:

  1. 異常只用於處理嚴重的錯誤(fatal error,致命錯誤)
  2. 對於一些不那麼嚴重的錯誤(nonfatal error,非致命錯誤),有兩種解決方案:
    • 讓對象返回nil或者0(例如:初始化的參數不合法,方法返回nil或0)
    • 使用NSError

5、以弱引用避免循環引用(避免內存泄漏)

這條比較簡單,內容主旨就是標題:以弱引用避免循環引用(Retain Cycle)

  • 爲了不因循環引用而形成內存泄漏。這時,某些引用須要設置爲弱引用(weak)。
  • 使用弱引用weak,ARC下,對象釋放時,指針會置nil

6、以 「自動釋放池塊」 下降內存峯值

  • 默認狀況下:自動釋放池須要等待線程執行下一次事件循環時才清空,一般for循環會不斷建立新對象加入自動釋放池裏,循環結束才釋放。所以,可能會佔用大量內存。
  • 手動加入自動釋放池塊(@autoreleasepool):每次for循環都會直接釋放內存,從而下降了內存的峯值。

尤爲,在遍歷處理一些大數組或者大字典的時候,可使用自動釋放池來下降內存峯值,例如:

NSArray *qiShare = /*一個很大的數組*/
NSMutableArray *qiShareMembersArray = [NSMutableArray new];
for (NSStirng *name in qiShare) {
    @autoreleasepool {
        QiShareMember *member = [QiShareMember alloc] initWithName:name];
        [qiShareMembersArray addObject:member];
    }
}
複製代碼

PS:自動釋放池的原理:排布在「棧」中,對象執行autorelease消息後,系統將其放入最頂端的池裏(進棧),而清空自動釋放池就是把對象銷燬(出棧)。而調用出棧的時機:就是當前線程執行下一次事件循環時。

7、用 「殭屍對象」 調試內存管理問題

如上圖,勾選這裏能夠開啓殭屍對象設置。開啓以後,系統在回收對象時,不將其真正的回收,而是把它的isa指針指向特殊的殭屍類(zombie class),變成殭屍對象。殭屍類可以響應全部的選擇子,響應方式爲:打印一條包含消息內容以及其接收者的消息,而後終止應用程序。

殭屍對象簡單原理:在Objective-C的運行期程序庫、Foundation框架以及CoreFoundation框架的底層加入了實現代碼。在系統即將回收對象時,經過一個環境變量NSZombieEnabled識別是殭屍對象——不完全回收,isa指針指向殭屍類而且響應全部選擇子。

8、不要使用retainCount

在蘋果引入ARC以後retainCount已經正式廢棄,任什麼時候候都無法調用這個retainCount方法來查看引用計數了,由於這個值實際上已經沒有準確性了(並且在ARC環境下也調用不了)。可是在MRC下仍是能夠正常使用的。

最後,特別緻謝:《Effective Objective-C 2.0》第五章。

關注咱們的途徑有:
QiShare(簡書)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公衆號)

推薦文章:
iOS與JS交互之WKWebView-WKUIDelegate協議
若是360推出辣椒水,各位女士會買嗎?
從撒狗糧帶你瞭解WoT鏈接場景

相關文章
相關標籤/搜索