來源于 Ry’s Objective-C Tutorial - RyPress
一個學習Objective-C基礎知識的網站.
個人覺得很棒,所以決定抽時間把章節翻譯一下.
本人的英語水平有限,有讓大家誤解或者迷惑的地方還請指正.
原文地址:http://rypress.com/tutorials/objective-c/memory-management
僅供學習,如有轉摘,請注明出處.
如我們在屬性章節討論的, 任何一種內存管理系統的目的都是通過控制其所有對象的生命周期來減少內存占用. iOS和OS X應用程序通過對象擁有關系來完成(管理對象生命周期), (這種關系)確保對象應該存在的時間, 而不是或多或少.
這種對象擁有關系方案通過引用計數系統來實現的, 引用計數即是跟蹤并記錄每個對象的擁有者個數. 當你需要一個對象時, 便要增加該對象的引用數量, 并且當你不在使用該對象, 便減少它的引用計數. 只要一個對象的引用計數大于0, 便能保證對象存活, 一旦(引用的)數量達到0, 系統便會被允許銷毀該對象.
在過去, 開發人員手動控制一個對象的引用數量, 通過調用在[NSObject protocol]中定義的特定內存管理方法. (這種管理方式)被稱作手動保持與釋放(MRR). 還好, Xcode4.2版本引入了自動引用計數(ARC), 意味著可以為你自動插入這些(自動內存管理的)方法. 現在的程序應該只使用ARC, 因為它更可靠而且讓你專注于程序的功能, 而不是它的內存管理.
本模塊解釋了MRR中的引用計數核心概念, 然后討論了ARC的一些實際中需要考慮的因素.
Manual Retain Release
在手動內存管理環境下, 程序中每個對象的擁有關系的得到和放棄都要你負責. 通過調用以下特定的內存相關方法來完成.
方法 | 行為 |
---|---|
alloc | 新建一個對象并得到(對它的)擁有關系 |
retain | 對已存在的對象得到(對它的)擁有關系 |
copy | 復制一個對象并得到(對它的)擁有關系 |
release | 放棄一個對象的擁有關系, 并立即銷毀 |
autorelease | 放棄一個對象的擁有關系, 但是延遲銷毀 |
手動控制對象擁有關系看起來是一件很可怕的任務, 但實際上很容易. 你所需要做的就是得到你需要的對象的擁有關系并記得用完以后放棄(擁有關系)就行了. 從實際情況來看, 上述意味著, 對于相同的對象, 你必須通過對應的release和autorelease來平衡該對象的每個alloc, retain以及copy操作.
一旦你忘記平衡這些操作, 兩個事情中的一個就會發生. 如果你忘記釋放一個對象, 那么它占用的內存永遠不會被釋放, 從而導致內存泄露. 稍小的泄露不會對程序造成明顯的影響, 但是如果吃了足夠的內存, 那程序最終會崩潰. 另一方面, 如果嘗試多次釋放一個對象, 會引起被稱作指針懸掛的問題. 一旦你嘗試訪問這些懸掛指針, 便是在請求非法的內存地址, 那么程序也最有可能崩潰.
該部分的剩余內容將解釋怎樣適當使用上述方法來避免內存泄露和懸掛指針.
Enabling MRR - 激活MRR
在我們開始嘗試手動內存管理之前, 我們需要關閉自動引用計數(系統). 在項目導航欄中點擊項目圖標, 確保選中了Build Setting選項, 然后在搜索欄中輸入automatic reference counting. OC的自動引用計數編譯選項就會出現. 將選項YES調成NO.
記住, 我們僅為了學習的目的才這么干 - 在新項目中, 永遠別使用MRR.
The alloc Method
在這個教程中, 我們已經用過alloc方法去創建對象了. 但是, 它不僅僅是為對象分配空間, 它同時也設置對象的引用數為1. 這完全可以理解, 因為, 如果我們不想讓一個對象存在, 哪怕是一會, 我們就不會去創建該對象的.
// main.m
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *inventory = [[NSMutableArray alloc] init];
[inventory addObject:@"Honda Civic"];
NSLog(@"%@", inventory);
}
return 0;
}
上面的代碼看著應該很熟悉, 我們做的就是實例化一個可變數組, 給它添加一個值, (隨后)展示它的內容. 從內存管理的角度來看, 我們擁有了這個inventory對象, 就意味著什么時候釋放它也成為了我們的責任.
但是, 由于我們并沒有釋放它, 所以程序目前有一處內存泄露. 可以通過靜態內存分析工具運行項目來檢查這個問題(內存泄露). 在菜單欄中, 選擇導航 -> Product -> Analyze, 或者使用Shift + Cmd + B鍵盤快捷鍵(啟動). (該功能)將會查找到代碼中可預料的問題, 如下面main.m中展示的那樣.
這是一個小對象, 所以這個泄露并不是災難性的. 然而, 如果它反復發生.(比如, 在一個長循環或者每次用戶點擊一個按鈕(都會發生)), 那么這個程序最終內存溢出并崩潰.
The release Method
release方法通過減少一個對象的的引用數量來放棄相應的擁有關系. 因此, 我們可以通過在mian.m中的NSLog()調用之后添加下面這一樣(代碼)來避免內存泄露:
[inventory release];
現在, 我們的alloc已通過release(達到)平衡了, 靜態分析器不再輸出任何問題. 一般來說, 在方法中創建的對象, 都會在該方法的的結尾處放棄對該對象的擁有關系, 就像我們這里做的事情.
太早的釋放一個對象會產生懸掛指針(的問題). 比如, 試著將上面的代碼移到調用NSLog()方法之前. 因為release會立即釋放真實的內存, 所以在NSLog()中的inventory變量指向一個非法地址, 當你再嘗試運行, 程序會報EXC_BAD_ACCESS錯誤并崩潰:
關鍵就在于, 在用完該對象之前不要放棄對它的擁有關系.
The retain Method
retain方法獲取一個已存在對象的擁有關系. 像是在告訴操作系統, "嗨, 我也需要那個對象, 不要干掉它". 當其他對象需要保證(自身)屬性引用的是一個合法實例(時), 這是一個必備的能力.
舉個例子, 我們使用retain來得到對inventory數組的強引用. 新建一個CarStore類, 然后修改它的頭文件如下:
// CarStore.h
#import <Foundation/Foundation.h>
@interface CarStore : NSObject
- (NSMutableArray *)inventory;
- (void)setInventory:(NSMutableArray *)newInventory;
@end
手動聲明inventory屬性的訪問器. 第一次迭代中的CarStore.m中提供了getter, setter(使用一個實例變量來記錄對象)的簡易實現:
// CarStore.m
#import "CarStore.h"
@implementation CarStore {
NSMutableArray *_inventory;
}
- (NSMutableArray *)inventory {
return _inventory;
}
- (void)setInventory:(NSMutableArray *)newInventory {
_inventory = newInventory;
}
@end
回到main.m, 將inventory變量分配給CarStore's的inventory屬性:
// main.m
#import <Foundation/Foundation.h>
#import "CarStore.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *inventory = [[NSMutableArray alloc] init];
[inventory addObject:@"Honda Civic"];
CarStore *superstore = [[CarStore alloc] init];
[superstore setInventory:inventory];
[inventory release];
// Do some other stuff...
// Try to access the property later on (error!)
NSLog(@"%@", [superstore inventory]);
}
return 0;
}
因為該對象(inventory)已經在main.m中提早被釋放了, superStore對象只有一個對該數組的弱引用, 所以最后一行代碼中的inventory屬性已是一個懸掛指針. 為了將其調整成強引用, CarStore需要在它的setInventory: 訪問器中得到一個對該數組的擁有關系:
// CarStore.m
- (void)setInventory:(NSMutableArray *)newInventory {
_inventory = [newInventory retain];
}
這樣就能保證inventory對象在superStore在使用它的過程中不被釋放. 注意, retain方法返回的是對象本身, 所以允許我們將retain和賦值寫成一行.
很遺憾, 這樣的代碼會產生其他的問題: 即, retain調用沒有release來平衡(對應), 所以有內存泄露(的問題). 一旦我們給setInventory:傳遞其他值, 我們無法得到舊值, 意味著我們永遠都不能釋放它了. 為了改正(這個問題), setInventory:需要對舊值調用release:
// CarStore.m
- (void)setInventory:(NSMutableArray *)newInventory {
if (_inventory == newInventory) {
return;
}
NSMutableArray *oldValue = _inventory;
_inventory = [newInventory retain];
[oldValue release];
}
// 或者這么寫
- (void)setInventory:(NSMultableArray*)newInventory {
if (_inventory != newInventory) {
[_inventory release];
_inventory = [newInventory retain];
}
}
這就是屬性的retain以及strong特性本質做的事情. 很顯然, 使用@property比我們自己創建這些訪問器方便很多.
上面的圖解中, 各自的位置都形象的展示了我們在main.m中創建的inventory對象的內存管理調用. 如你所見, 所有的alloc和retain(調用)都有release來平衡, 從而保證真實的內存最終會被釋放.
The copy Method
相對于retain, 另一個可選方案就是copy, (copy)會創建一個全新的對象實例, 并且增加引用計數, 并且能讓原始對象不受影響[此處是相對而言]. 因此, 如果不想引用一個可變的inventory數組, 你可以copy它. 將setInventory: 調整如下:
// CarStore.m
- (void)setInventory:(NSMutableArray *)newInventory {
if (_inventory == newInventory) {
return;
}
NSMutableArray *oldValue = _inventory;
_inventory = [newInventory copy];
[oldValue release];
}
也許你會想起在copy Attribute中, [this has the added perk of freezing mutable collections at the time of assignment][還沒想好咋表達]. 一些類提供了多個copy方法(跟(一些類)提供多個init方法類似), 并且假設任何以copy開頭方法都有相同的行為(這種想法)是安全的.
The autorelease method
與release類似, autorelease方法也是放棄對一個對象的擁有關系, 但不同的是, 它不會立即銷毀該對象, 而是在程序中延緩真正的內存釋放. 這樣就允許你在應該釋放該對象的時候釋放它, 但卻能仍舊被其他(對象)使用.
舉例來說, 利用一個工廠方法來創建并返回一個CarStore對象:
// CarStore.h
+ (CarStore *)carStore;
技術上講, 釋放這個對象是carStore方法的責任, 因為沒有一種方式告知調用者擁有這個返回的對象. 因此, 它的實現應該返回一個autorelease對象, 像這樣:
// CarStore.m
+ (CarStore *)carStore {
CarStore *newStore = [[CarStore alloc] init];
return [newStore autorelease];
}
這種方式在創建carStore對象會會立即放棄對該對象的擁有關系, 但是將該對象保存在內存中足夠長時間以便與調用者交互. 準確來講, 該對象會等到在最近的一個@autorelease{}塊結尾處, 調用常規的release方法[不顯式調用]后再釋放. 這就是為什么main()函數總是被@autorelease{}塊包圍著 - 因為這樣能保證所有的autoreleased對象在程序執行完成后都能被釋放.
所有這些內置的工廠方法, 像NSString的stringWithFormt: 和 stringWithContentOfFile: 與我們的carStore方法用完全相同的方式工作. 在ARC之前, 這是一個便利的約定, 因為它能讓你在不用擔心何時釋放對象的情況下使用對象.
如果現在將main()中的superStore構造器從alloc/init調整成下面的方式, 那么你就不用在函數結尾(顯式)釋放它了.
// main.m
CarStore *superstore = [CarStore carStore];
實際上, 你現在已不能釋放superStore實例了, 因為你不再擁有它 - 而是carStore工廠方法擁有它. 避免顯式地釋放autoreleased對象很重要(否則, 將會產生懸掛指針, 導致程序崩潰).
The dealloc Method
一個對象的dealloc方法與它的init方法剛好相反. 當一個對象被釋放之前, 該方法會被合適地調用, 以便給你清理內部對象的機會. 該方法是被runtime自動調用地 - 永遠別嘗試主動調用.
在MRR環境下, 在dealloc方法中最常做的事情就是釋放存儲在實例變量中的對象. 想象一下, 當一個(CarStore)實例被重新分配時, 當前的CareStore會發生什么.: 它的被setter方法retain的_inventory實例變量永遠沒機會被釋放了. 這是另一種形式的內存泄露. 我們需要做的就是在CarStore.m中增加一個(準確說是重寫)自定義的dealloc來修復這個問題:
// CarStore.m
- (void)dealloc {
[_inventory release];
[super dealloc];
}
注意, 每次都需要調用父類的dealloc來保證父類中的所有實例變量也都被正確地釋放了. 作為一個普遍的規則, 為了盡可能的保持自定義的dealloc的簡介, 不應該在dealloc中處理可以在其他地方處理的邏輯(問題).
MRR總結
概括來講, 上述就是手動內存管理. 關鍵就是使用release或者autorelease來平衡每個alloc, retain和copy, 否則, 在你的程序中, 會遇到諸如懸掛指針, 或者某時刻的內存泄露的問題.
請記住, 該部分僅使用了MRR來理解iOS和OS X內存管理的的內部工作原理. 在現實中, 上述的大部分代碼都是廢棄的, 不過你可能會在比較老的文檔中遇到. 像這種明確地得到和放棄(對某個對象的)擁有關系的方式已經完全被ARC取代了, 理解這點很重要.
Automatic Reference Counting
現在可以把你頭腦中的所有關于手動內存管理的內容都忘記了. 自動引用計數跟MRR的效果一樣, 但是它能自動為你在(代碼中)合適的位置插入(上述的)內存管理方法. 這對OC開發人員來說非常有益, 因為它能讓開發人員將注意力放在自身程序的功能而不再操心它(內存管理)怎么實現.
相比較內存管理的人為錯誤, ARC幾乎完美的解決了這個問題, 所以唯一不使用ARC的原因就是你還在與歷史遺留代碼打交道(但是, ARC, 在大部分情況下, 是向下兼容MRR程序的). 該章剩余部分講解MRR與ARC之間的重大變化.
Enabling ARC
首先, 在項目中Building Settings 中恢復ARC. 將自動引用計數的選項改為YES. 再強調一下, 這是是所有Xcode模板的默認值, 并且這也是你開發所有程序應該使用的.
No More Memory Methods
ARC通過分析你的代碼以得知每個對象存在的理想生命周期來工作, 隨后自動(在合適的位置)插入必要的retain以及release調用. 該算法需要對對象擁有關系的完全控制, 這意味著在你的程序中, 你不能手動調用retain, release或者autorelease.
在ARC程序中, 唯一涉及內存相關的方法就是alloc和copy. 你可以將這些理解為普通的構造器, 并忽略整個對象擁有關系這些事情.
New Property Attributes - 新的屬性特性
ARC介紹了@property新的特性. 你應該用strong特性來代替retain, 用weak替代assign. 這些特性都已在屬性章節討論了.
The dealloc Method in ARC
ARC中的dealloc也有點不同. 我們不用再像在[dealloc Method](#The dealloc Method)中那樣去釋放實例變量 - ARC幫我們做了. 另外, 父類的dealloc方法也是自動調用的, 也不再需要我們做了.
大多數情況下, 我們無需自定義dealloc方法. 其中一個例外的情況就是, 你在使用較低級的(相對高級語言來說, 比如C相對于OC)內存分配函數, 比如malloc. 這種情況下, 仍需要在dealloc中調用free()來避免內存泄露.
總結
大多數情況下, ARC會讓你完全忘記內存管理. (使用ARC)就是不再關注內存管理, 而專注于高級功能上. 唯一你需要操心的就是循環引用的問題, 這個已在屬性章節論述了.
如果你想知道更多關于ARC的細節, 請參閱Transitioning to ARC Release Notes.
好了, 截止到目前, 所有OC中你應該知道的基本上都學完了. 唯一我們還未涉及的是C和Foundation框架中提供的基本數據類型. 下一章節會介紹所有的標準類型, 從numbers(數值)到strings(字符串), arrays(數組), dictionaries(字典), 甚至dates(日期)等.
寫于15年11月13號, 完成于15年12月03號