級別: ★★☆☆☆
標籤:「iOS」「記憶體管理」「Objective-C」
作者: MrLiuQ
審校: QiShare團隊
前言: 這幾篇文章是小編在鑽研《Effective Objective-C 2.0》的知識產出,其中包含作者和小編的觀點,以及小編整理的一些demo。希望能幫助大家以簡潔的文字快速領悟原作者的精華。 在這裡,QiShare團隊向原作者Matt Galloway表達誠摯的敬意。
文章目錄如下:
iOS 編寫高質量Objective-C程式碼(一)
iOS 編寫高質量Objective-C程式碼(二)
iOS 編寫高質量Objective-C程式碼(三)
iOS 編寫高質量Objective-C程式碼(四)
iOS 編寫高質量Objective-C程式碼(五)
iOS 編寫高質量Objective-C程式碼(六)
iOS 編寫高質量Objective-C程式碼(七)
iOS 編寫高質量Objective-C程式碼(八)
本篇的主題是iOS中的 “記憶體管理機制”。
說到iOS記憶體管理,逃不過iOS的兩種記憶體管理機制:MRC & ARC。
先簡單介紹一下:
MRC(manual reference counting): “手動引用計數” ,由開發者管理記憶體。 ARC(automatic reference counting):“自動引用計數”,從iOS 5
開始支援, 由編譯器幫忙管理記憶體。
蘋果引入ARC機制的原因猜測:
iOS 4
之前,所有iOS開發者必須要手動管理記憶體,即手動管理物件的記憶體分配和釋放。首先,不斷插入retain
、release
等記憶體管理語句,大大加大了工作量和程式碼量。其次,在面對一些多執行緒併發操作時,開發者手動管理記憶體並不簡單,還可能會帶來很多無法預知的問題。
所以,蘋果從iOS 5
開始引入ARC機制,由編譯器幫忙管理記憶體。在編譯期,編譯器會自動加上記憶體管理語句。這樣,開發者可以更加關注業務邏輯。
下面進入正題:編寫高質量Objective-C程式碼(五)——記憶體管理篇。
一、理解引用計數
- 引用計數工作原理:
這裡引入《Objective-C 高階程式設計 iOS與OSX多執行緒和記憶體管理》這本書的例子: 很經典的圖解:
解釋:
1.開燈:引申為:“ 建立物件 ”。
2.關燈:引申為:“ 銷燬物件 ”。
解釋:
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方法將保留新值,釋放舊值,然後更新例項變數。這三個語句的順序很重要。
如果先release
再retain
。那麼該物件可能已經被回收,此時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:關於如何檢驗專案中有無記憶體洩漏:參考這篇部落格。
二、以ARC簡化引用計數
,在ARC環境下,禁止?呼叫:retain
、release
、autorelease
、dealloc
方法。
-
使用ARC時必須遵循的方法命名規則: 若方法名以
alloc
、new
、copy
、mutableCopy
開頭,則規定返回的物件歸呼叫者。 -
變數的記憶體管理語義:
對比一下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);
}
複製程式碼
三、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];
}
複製程式碼
四、編寫“ 異常安全程式碼 ”時留意記憶體管理問題
異常只應在發生嚴重錯誤後丟擲。
用的不好會造成記憶體洩漏:在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
標誌,其效能損失不大。
最後,還是建議:
- 異常只用於處理嚴重的錯誤(fatal error,致命錯誤)
- 對於一些不那麼嚴重的錯誤(nonfatal error,非致命錯誤),有兩種解決方案:
- 讓物件返回
nil
或者0
(例如:初始化的引數不合法,方法返回nil或0) - 使用
NSError
- 讓物件返回
五、以弱引用避免迴圈引用(避免記憶體洩漏)
這條比較簡單,內容主旨就是標題:以弱引用避免迴圈引用(Retain Cycle)
- 為了避免因迴圈引用而造成記憶體洩漏。這時,某些引用需要設定為弱引用(
weak
)。 - 使用弱引用
weak
,ARC下,物件釋放時,指標會置nil
。
六、以 “自動釋放池塊” 降低記憶體峰值
- 預設情況下:自動釋放池需要等待執行緒執行下一次事件迴圈時才清空,通常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訊息後,系統將其放入最頂端的池裡(進棧),而清空自動釋放池就是把物件銷燬(出棧)。而呼叫出棧的時機:就是當前執行緒執行下一次事件迴圈時。
七、用 “殭屍物件” 除錯記憶體管理問題
如上圖,勾選這裡可以開啟殭屍物件設定。開啟之後,系統在回收物件時,不將其真正的回收,而是把它的isa指標
指向特殊的殭屍類(zombie class),變成殭屍物件。殭屍類能夠響應所有的選擇子,響應方式為:列印一條包含訊息內容以及其接收者的訊息,然後終止應用程式。
殭屍物件簡單原理:在Objective-C的執行期程式庫、Foundation框架以及CoreFoundation框架的底層加入了實現程式碼。在系統即將回收物件時,通過一個環境變數
NSZombieEnabled
識別是殭屍物件——不徹底回收,isa
指標指向殭屍類並且響應所有選擇子。
八、不要使用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連線場景