內存管理是程序在運行時分配內存、使用內存,并在程序完成時釋放內存的過程。在Objective-C中,也被看作是在眾多數據和代碼之間分配有限內存資源的所有權(Ownership)的一種方式。
內存管理關心的是清理或回收不用的內存,以便內存能夠再次利用。如果一個對象不再使用,就需要釋放對象占用的內存。Objective-C提供了兩種內存管理的方法:手動管理內存計數(MRR)和自動引用計數(ARC)。這兩種方法都采用了一種稱為“引用計數”的模型來實現,該模型由Foundation框架的NSObject類和運行時環境(Runtime Environment)共同提供。下面,我們就先來介紹下什么是引用計數。
1 引用計數
引用計數(Reference Count)是一個簡單而有效的管理對象生命周期的方式,一般概念是:當創建一個新的對象時,初始的引用計數為1。為保證對象的存在,每當創建一個引用到該對象時,通過給對象發送retain
消息,為引用計數加1;當不再需要對象時,通過給對象發送release
消息,為引用計數減1;當對象的引用計數為0時,系統就知道這個對象不再使用了,通過給對象發送dealloc
消息,銷毀對象并回收內存。一般在retain
方法之后,引用計數通常也被稱為保留計數(retain count)。
為了更好地闡述引用計數的機制,這里引用開關房間燈的例子來說明:
假設辦公室的照明設備只有一個,進入辦公室的人需要照明,離開辦公室的人不需要照明。因此,為保證辦公室僅有的照明設備得到很好的管理,只要辦公室還有人,就會需要照明,燈就得開著,而當辦公室沒人的時候,就需要關燈。為了判斷辦公室是否還有人,我們導入計數功能來計算“需要照明的人數”:
- 第一個進入辦公室的人,需要照明,此時,“需要照明的人數”為1,計數值從0變為1,需要開燈。
- 第二個進入辦公室的人,也需要照明,此時,“需要照明的人數”為2,計數值從1變為2。
- 之后,每一個進入辦公室的人都需要照明,此時“需要照明的人數”依次增加,計數值也依次加1。
- 當第一個人離開辦公室時,不再需要照明,此時“需要照明的人數”減1,計數值也減1。
- 之后,只要有人離開辦公室,就不再需要照明,此時“需要照明的人數”依次減少,計數值依次減1。
- 當最后一個人離開辦公室時,不再有人需要照明,“需要照明的人數”為0,計數值減至0,需要關燈。
在Objective-C中,對象就相當于辦公室的照明設備,而對象的使用環境就相當于進入辦公室上班的人。其對應關系可以用以下表格來表示:
進入辦公室上班的人對照明設備所做的動作 | 對象的使用環境對Objective-C對象所做的動作 |
---|---|
開燈 | 生成對象 |
需要照明 | 持有對象 |
不再需要照明 | 釋放對象 |
關燈 | 銷毀對象 |
2 手動管理內存 MRR
手動管理內存,即MRR(manual retain-release),是基于引用計數來實現的,通過自己跟蹤對象來明確管理內存。它與ARC之間的唯一區別是:在MRR中,對象的保留和釋放都是由我們手動處理,而在ARC中是自動處理的。
2.1 MRR內存管理的基本原則
為了方便理解,我們先通過一個例子看下MRR中的內存管理是如何工作的,之后會有總結。
首先打開Xcode,創建一個新的項目(File\New\Project...),在這里我們將項目名稱寫為MemoryManagementDemo。為了確保我們的代碼是在MRR環境下,在進行任何操作之前,我們需要先檢查當前項目是否啟用了ARC,如果是,將它關閉,如下圖所示:
然后打開storyboard,刪除當前視圖控制器,從右下角的對象庫中找到Navigation Controller,并拖動到畫布上。選中左邊的導航控制器,切換到屬性檢查器,勾選View Controller一欄下面的is Initial View Controller選項,將該控制器作為初始控制器,完成后,該控制器左側有個白色的箭頭,如下:
這時候如果你點擊運行,會在模擬器中看到一個空的表視圖:
接下來我們刪除ViewController.h文件和ViewController.m文件,并創建一個新的文件(File\New\File...),使它成為的UITableViewController的子類:
打開剛才創建的TableViewController.h文件,聲明一個數組類型的實例變量(這里我們使用下劃線_
作為實例變量名稱的起始字符,你可以把它看成是實例變量的一部分):
#import <UIKit/UIKit.h>
@interface TableViewController : UITableViewController
{
NSArray *_titles;
}
@end
然后切換到TableViewController.m文件,為了在表視圖中顯示內容,我們在viewDidLoad
方法中簡單地設置數組:
- (void)viewDidLoad
{
[super viewDidLoad];
_titles = [[NSArray alloc] initWithObjects:@"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9", @"10", nil];
}
在Objective-C中創建對象時,一般先向某個類發送alloc
消息來為新的對象分配內存存儲空間,然后調用init
方法初始化對象。如果init
方法沒有使用任何參數,可以用new
替代。即:
Class *object = [[Class alloc] init];
也可以寫成:
Class *object = [Class new];
在這里我們使用NSArray
類的initWithObjects:
方法創建數組對象,該方法返回一個新的對象,其中引用計數被設置為1,所以當我們不再使用對象時,需要減少引用計數,在OC中可以通過調用release
方法來實現。
但是應該在哪里調用release
呢?前面說過,當我們不再需要使用對象時,就可以調用該方法來釋放對象,因此我們將它寫在dealloc
方法中。
在viewDidLoad
下面創建一個dealloc
方法,并添加如下代碼:
- (void)dealloc
{
[_titles release];
_titles = nil;
[super dealloc];
}
我們不僅釋放了對象,還將對象設置為nil
,這樣做可以避免很多問題。比如當你嘗試在一個空的對象上調用一個方法,它什么都不做,但是如果你嘗試在一個被釋放掉的對象上調用方法,會引起程序的崩潰。還有,在dealloc
方法的末尾記得調用super
,這樣做是為了使任何繼承來的對象都能夠被釋放掉,同時,這也是使用手工內存管理的另一麻煩之處,在釋放完自身的一些對象之后,都需要調用[super dealloc]
。最后,需要注意的是,永遠不要直接對對象調用dealloc
方法。
接下來我們來完成tableView的dataSource部分:
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return _titles.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
NSString *string = [_titles objectAtIndex:indexPath.row];
NSString *title = [[NSString alloc] initWithFormat:@"Title: %@", string];
cell.textLabel.text = title;
[title release];
return cell;
}
前兩個方法分別返回tableView的section數量和row數量,第三個方法用來設置每一行顯示的內容,在這里我們顯示一個字符串。值的注意的是,使用initWithFormat:
方法創建的字符串引用計數將為1,結束時需要釋放,否則會造成內存泄漏。另外,在tableView:cellForRowAtIndexPath:
方法中,我們使用dequeueReusableCellWithIdentifier:forIndexPath:
方法創建一個可重用的表視圖單元對象,這里指定的重用標識符“cell”應該與storyboard中的TableViewCell的標識符一致。如果storyboard中的TableViewCell的標識符為空,需要添加:
最后,打開storyboard,將視圖控制器設置為TableViewController:
點擊運行,可以看到表視圖中的內容:
現在我們已經知道,在手動管理內存的情況下,調用alloc/init
創建對象時,引用計數為1,當結束使用對象時,需要調用release
方法使引用計數減為0。但在有些情況下,比如在一個方法中不再需要用到這個對象,但需要將其返回,這時候就引入了自動釋放的概念,通過給對象發送autorelease
消息用以標記該對象延遲釋放。
我們修改tableView:cellForRowAtIndexPath:
方法中的代碼:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
NSString *string = [_titles objectAtIndex:indexPath.row];
// 修改:
NSString *title = [[[NSString alloc] initWithFormat:@"Title: %@", string] autorelease];
cell.textLabel.text = title;
return cell;
}
上面的代碼刪除掉了[title release]
,并添加了autorelease
的調用,這樣在創建title
對象后,會將對象添加到由自動釋放池維護的對象列表中,我們可以繼續使用該對象來執行此方法的其余部分,直到自動釋放池被清理時釋放對象。
接下來我們繼續修改上面的代碼:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
NSString *string = [_titles objectAtIndex:indexPath.row];
// 修改:
NSString *title = [NSString stringWithFormat:@"Title: %@", string];
cell.textLabel.text = title;
return cell;
}
這次我們使用stringWithFormat:
方法替換了alloc/init/autorelease
,該方法也會返回一個新的NSString
對象,引用計數為1,同時該對象也會被自動添加到自動釋放池中。你可能對此會感到困惑,同樣都是創建對象,為什么有些會自動釋放有些需要調用autorelease
?這里有一個簡單的約定可供參考:
- 如果使用以
init
或copy
開頭的方法創建對象,返回的對象引用計數將為1,并且不會自動釋放。也就是說我們擁有該對象,使用完成后需要調用release
釋放,或者調用autorelease
延遲釋放。 - 如果使用任何其它方式開頭的方法創建對象,返回的對象引用計數也將為1,但會自動釋放,使用完成后不需要再調用
release
或autorelease
。需要注意的是,以這種方式創建的對象,即使我們現在可以使用它,但是并不擁有該對象。
現在我們又有一個新的問題:如果一個對象具有自動釋放,那以后想要再次使用該對象要怎么辦?如果我們嘗試將該對象存儲在某個地方稍后使用的話,可能會因為嘗試訪問已經釋放的內存而引起程序崩潰,因此我們調用retain
保留該對象,使對象的引用計數變為2,之后當autorelease
觸發時,引用計數減為1,并且對象不會被釋放。
下面打開TableViewController.h,我們繼續在文件中添加一個實例變量_lastTitleSelected
,當選中某一行時,該變量用于跟蹤上一次所選中行的對應文本內容:
@interface TableViewController : UITableViewController
{
NSArray *_titles;
NSString *_lastTitleSelected;
}
@end
然后在TableViewController.m文件底部添加tableView:didSelectRowAtIndexPath
方法及代碼:
#pragma mark - Table view delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// 創建消息
NSString *string = [_titles objectAtIndex:indexPath.row];
NSString *title = [NSString stringWithFormat:@"Title: %@", string];
NSString *message = [NSString stringWithFormat:@"Last title: %@. Current title: %@", _lastTitleSelected, title];
// 創建警報視圖以顯示彈出的消息
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Hint" message:message preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *action = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil];
[alert addAction:action];
[self presentViewController:alert animated:YES completion:nil];
// 釋放當前實例變量
[_lastTitleSelected release];
// 重新設置該實例變量
_lastTitleSelected = [title retain];
}
該方法首先創建了一個message
對象,用作警報視圖彈出的消息內容。然后創建一個警報控制器,當選中tableView某一行時會彈出一個警報框。最后的兩句代碼是我們強調的重點,由于警報框中的消息內容包含上次選中行的文本信息和這次選中行的文本信息,而每次選中的行可能會都不一樣,因此為了只保留最近一次選中行的信息,我們需要先釋放當前的實例變量_lastTitleSelected
,然后再重新設置該實例變量。最后一行代碼中,調用了retain
來保留title
對象,這是因為該對象在創建時具有自動釋放,為防止在后面的使用中對象被釋放掉,我們使用retain
來增加對象的引用計數以確保程序不會崩潰。如果想測試,我們可以在方法中添加NSLog
語句輸出title
對象的保留計數:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// 創建消息
...
NSString *title = [NSString stringWithFormat:@"Title: %@", string];
NSLog(@"retain count = %lu", (unsigned long)[title retainCount]);
...
// 重新設置該實例變量
_lastTitleSelected = [title retain];
NSLog(@"retain count = %lu", (unsigned long)[title retainCount]);
}
你將會看到下面的輸出結果:
retain count = 1
retain count = 2
最后,不要忘記在dealloc
方法中釋放實例變量_lastTitleSelected
:
- (void)dealloc
{
[_titles release];
_titles = nil;
[_lastTitleSelected release];
_lastTitleSelected = nil;
[super dealloc];
}
如果一切順利,點擊運行,你會看到下面的運行結果:
到這里,你對內存管理應該了解一些了,下面我們就來總結一下手動管理內存的規則:
以
alloc
、new
、copy
或mutableCopy
開頭的方法創建的對象,我們擁有該對象,使用完成后需要調用release
或autorelease
釋放。在
init
方法中為了獲取對象的所有權,或者在某些情況下避免對象被移除,可以使用retain
保留對象。在使用完對象后,需要使用release
進行釋放。對使用了
retain
、copy
、mutableCopy
、alloc
或new
方法的任何對象,以及具有retain
和copy
特性的屬性進行釋放,需要重寫dealloc
方法,使得在對象被釋放的時候能夠釋放這些實例變量。給對象發送
release
消息并不一定立即銷毀這個對象,只有當對象的引用計數減至0時,對象才會被銷毀,然后系統會發送dealloc
消息給這個對象用于釋放它的內存。如果在方法中不再需要用到某個對象,但需要將其返回,可以給該對象發送
autorelease
消息用以標記延遲釋放,對象的引用計數會在當前自動釋放池的末尾減1。當應用程序終止時,內存中的所有對象都會被釋放,不論它們是否在自動釋放池中。
當不再需要一個對象時,必須放棄所擁有的該對象的所有權。
不能放棄一個你所不擁有的對象的所有權。
2.2 自動釋放池(autorelease pool)
上面有說到自動釋放,在這里我們簡單介紹下自動釋放池。
自動釋放池創建的目的就是希望可以幫助追蹤需要延遲一些時間釋放的對象。
通過給對象發送autorelease
消息,就可以將一個對象添加到由自動釋放池維護的對象列表中:
[object autorelease];
程序中使用來自Foundation、UIKit、AppKit框架的類時,首先需要創建一個自動釋放池,這樣來自這些框架的類才會創建并返回自動釋放的對象,需要在程序中使用@autoreleasepool
指令(一般在項目中的main.m文件中就會看到該語句):
@autoreleasepool {
statements
}
當執行到autorelease塊的末尾時,系統就會對池中的每個對象發送release
消息,這將影響到所有發送過autorelease
消息并被添加到自動釋放池中的對象,當這些對象的引用計數減至0時,會發送出dealloc
消息,并且它們的內存將會被釋放。
需要注意的是,自動釋放池并不包含實際的對象,只是包含對象的引用,對象將在自動釋放池清理的時候被釋放。在ARC中,自動釋放池主要用于降低內存峰值,只是我們不再需要手動添加autorelease
的代碼了。
2.3 使用訪問器方法簡化內存管理
手動管理內存時,在代碼中使用retain
或release
來保留或釋放對象難免會出錯,為此,我們可以使用訪問器方法來減少內存管理中的問題。
通常所說的訪問器(accessor)方法指的是設值方法(setter)和取值方法(getter),又統稱為存取方法。設值方法即設置實例變量值的方法,主要目的是將方法參數設為對應的實例變量的值,一般不會返回任何值。取值方法即檢索實例變量值的方法,主要目的是獲取存儲在對象中的實例變量的值,并通過程序返回發送出去,所以取值方法必須返回實例變量的值作為return的參數。
下面我們繼續延用之前的例子,在TableViewController.h文件中為兩個實例變量添加設值方法和取值方法:
@interface TableViewController : UITableViewController
{
NSArray *_titles;
NSString *_lastTitleSelected;
}
- (void)setTitles:(NSArray *)titles;
- (NSArray *)titles;
- (void)setLastTitleSelected:(NSString *)lastTitleSelected;
- (NSString *)lastTitleSelected;
@end
然后在TableViewController.m文件的底部添加實現方法:
#pragma mark - Accessor method
- (void)setTitles:(NSArray *)titles
{
[titles retain];
[_titles release];
_titles = titles;
}
- (NSArray *)titles
{
return _titles;
}
- (void)setLastTitleSelected:(NSString *)lastTitleSelected
{
[lastTitleSelected retain];
[_lastTitleSelected release];
_lastTitleSelected = lastTitleSelected;
}
- (NSString *)lastTitleSelected
{
return _lastTitleSelected;
}
上面的getter方法容易理解,它們只返回了實例變量。而在setter方法中,我們首先使用retain
保留新傳入的參數變量以增加引用計數,然后使用release
釋放掉舊的實例變量以減少引用計數,最后將實例變量的值設置為傳入的參數變量。這樣,只要設置對象,就能保證在實例變量中存儲的對象有正確的引用計數。另外,使用這樣的順序設置實例變量,可以防止將實例變量設置為同一對象的情況。還有,如果你仔細觀察上面的設值方法,你就會明白我們在最開始命名實例變量的時候要使用下劃線的原因,最主要的是為了避免由實例變量名稱和參數變量名稱相同而引起的沖突。
接下來我們就使用這些存取方法來修改代碼中的實例變量。
首先我們使用設值方法來修改viewDidLoad
中的_titles
:
- (void)viewDidLoad
{
[super viewDidLoad];
[self setTitles:[[[NSArray alloc] initWithObjects:@"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9", @"10", nil] autorelease]];
}
觀察仔細的話,你會發現除了使用設值方法外,在末尾我們還調用了autorelease
。還記得在setter方法中我們使用retain
將傳入的參數變量引用計數加1嗎?這里我們使用了initWithObjects:
創建數組對象,會導致變量最終的引用計數為2,因此必須使用自動釋放來減少引用計數。當然,我們也可以使用arrayWithObjects:
方法創建數組對象,這樣就不需要再調用autorelease
了。
另外,為了方便,Objective-C語言允許我們使用點運算符.
代替方括號[]
來設置或獲取實例變量的值,即:
self.titles = xxx;
相當于:
[self setTitles:xxx];
點運算符
.
通常用于屬性,但不限于屬性。
下面我們使用點運算符.
再次修改上面的方法:
- (void)viewDidLoad
{
[super viewDidLoad];
self.titles = [[[NSArray alloc] initWithObjects:@"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9", @"10", nil] autorelease];
}
接下來在tableView:didSelectRowAtIndexPath
方法中修改_lastTitleSelected
:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
...
// 創建警報視圖以顯示彈出的消息
...
[self presentViewController:alert animated:YES completion:nil];
// 設置實例變量(刪除最后兩行代碼,用下面的代碼替代)
self.lastTitleSelected = title;
}
我們將實例變量的內存管理的代碼都寫在了setter
方法中,所以在上面的代碼中設置實例變量就簡單了很多。
另外需要說明的一點是,文檔中有提到:不要在初始化方法和dealloc方法中使用訪問器方法:
The only places you shouldn’t use accessor methods to set an instance variable are in initializer methods and
dealloc
.
如果要在init
方法中初始化一個對象,一般使用下面的形式:
- (instancetype)init
{
self = [super init];
if (self) {
instance = ...
}
return self;
}
在dealloc
方法中也要使用實例變量:
- (void)dealloc
{
[instance release];
[super dealloc];
}
之所以說不要在init
和dealloc
方法中使用訪問器方法,主要是由于面向對象的繼承、多態特性與訪問器方法可能造成的副作用聯合導致的。繼承和多態導致在父類的實現中調用訪問器方法時可能會調用到子類重寫的存取方法,而此時子類部分并未完全初始化或已經銷毀,導致混亂,從而出現一系列的邏輯問題甚至崩潰。但這種說法也不是絕對的,盡管存在風險但并不代表百分之百的崩潰或錯誤,如果你在程序中有這樣寫,并且明確地知道它不會產生任何問題,那么就可以使用它來簡化你的代碼。比如,在這里我們修改dealloc
中的代碼:
- (void)dealloc
{
self.titles = nil;
self.lastTitleSelected = nil;
[super dealloc];
}
上面刪除了之前手動調用release及將其設置為nil的兩行代碼,然后使用setter方法替代。現在我們將nil作為設值方法里的參數變量傳遞,即:
[nil retain];
[_titles release];
_titles = nil;
由于代碼中[nil retain]
不會執行任何操作,因此我們在dealloc
方法中使用self.titles = nil
不會產生任何問題。不過為了安全起見,在不清楚是否會產生問題的情況下,我們還是建議遵守文檔中的說明。
2.4 使用屬性
除了手動編寫存取方法,Objective-C還提供了屬性(property)方便我們快速地為實例變量創建訪問器方法,并可選地實現它們。
屬性的聲明一般在接口部分,以@property
關鍵字開頭,后面可以選擇性的定義屬性的特性,然后以屬性的類型和名稱(一般情況下我們使用與實例變量相同的名稱,但并不是必須的)結尾。下面是幾種有效的屬性聲明:
@property int age;
@property (copy) NSString *name;
@property (nonatomic, strong) NSArray *array;
需要注意的是,在聲明屬性時,屬性的名稱前面不要以
new
、alloc
、copy
或者init
這些詞語開頭。
聲明屬性的操作就相當于聲明setter和getter方法,以上面的age
為例:
@property int age;
就相當于:
- (void)setAge:(int)age;
- (int)age;
一般系統默認的設值方法名稱是以set
開頭(setPropertyName),默認的取值方法是以屬性名稱命名(propertyName)。如果想要更改成自定義的名稱可以使用下面的方法:
- 用setter = setterName來指定setter方法的名稱,如:
@property (setter = setterName) int age;
- 用getter = getterName來指定getter方法的名稱,如:
@property (getter = getterName) int age;
一般常用于BOOL
類型,getter方法通常以is
開頭,比如標識一個視圖是否隱藏的hidden
屬性,其getter方法應該稱為isHidden
。可以這樣聲明:
@property (nonatomic,getter = isHidden ) BOOL hidden;
接下來我們在類的實現部分使用@synthesize
告訴編譯器自動為屬性實現一個設值方法和取值方法,即:
@implementation Class
@synthesize age;
@end
其中@synthesize age
默認指定的實例變量名稱與屬性相同,即:
@synthesize age = age;
Xcode 4.4以后,編譯器引入了屬性自動合成(property autosynthesis),也就是說編譯器會為每一個@property
添加@synthesize
,我們不需要再顯式地使用@synthesize
指令了。但需要注意的是,自動合成默認生成的實例變量名稱以_
為前綴,加上屬性名,即:
@synthesize age = _age;
如果不喜歡默認的實例變量名稱,或者我們希望使用更有語義的名稱,就需要通過@synthesize
來指定等號后面的名稱作為我們希望的實例變量名:
@synthesize age = currentAge;
還需要說明的是,通常編譯器會自動合成一個實例變量和至少一個訪問器方法,如果我們為帶有readwrite
關鍵字的屬性同時手動實現了setter和getter方法,或為帶有readonly
關鍵字的屬性實現了getter方法,那么編譯器會假定我們正在對屬性的實現進行控制,并且不會自動合成實例變量。在這種情況下,我們就需要手動指定一個實例變量:
@synthesize property = _property;
另外需要注意的是,一般情況下使用@property
會在編譯期間自動合成存取方法,但有些存取方法是在運行時動態創建的,這時聲明和使用屬性時會由于缺少方法而在編譯期間發出警告,這時候我們可以使用@dynamic
語句來抑制警告:
@implementation Class
@dynamic age;
@end
下面我們就使用屬性替換我們代碼中的存取方法:
首先我們在TableViewController.h文件中刪除實例變量和存取方法的聲明,使用屬性來代替:
@interface TableViewController : UITableViewController
@property (nonatomic, retain) NSArray *titles;
@property (nonatomic, retain) NSString *lastTitleSelected;
@end
然后切換到TableViewController.m文件,刪除之前寫的setter和getter方法,并在實現部分的最頂部添加@synthesize
:
@implementation TableViewController
@synthesize titles = _titles;
@synthesize lastTitleSelected = _lastTitleSelected;
當然這里聲明的實例變量與默認的一致,因此上面的@synthesize
也可以省略不寫。
現在可以運行代碼試下,你會發現模擬器跟以前一樣正常顯示。但是顯然這種讓編譯器自動生成存取方法的做法比我們手動編寫存取方法要簡單很多,同時也更高效,并且在多核設備上可以使用多線程運行。
2.5 屬性的特性
屬性的特性是使用一些特殊的關鍵字來告訴編譯器如何合成相關的訪問器方法。在我們的代碼中,聲明屬性時使用了nonatomic
和retain
。在此之前我們講到屬性的聲明時,在示例中也使用到了copy
,但屬性的關鍵字遠不止這幾個, 一般分為線程相關、訪問器相關以及內存相關這三類。下面是詳細介紹。
2.5.1 線程相關:atomic、nonatomic
atomic(默認)
- 默認特性,如果沒有使用任何關鍵字指定屬性的特性,默認情況下Objective-C屬性的特性是
atomic
。 -
atomic
表示原子屬性,使用該屬性是為了告訴系統使用互斥(mutex)鎖定保護屬性的存取方法,如果當前進程進行到一半,其他線程來訪問當前線程,可以保證先執行完當前線程。也就是說,即使從不同的線程同時調用訪問器方法,也能夠保證有一個值總能被getter方法完全檢索到或者被setter方法完全設置。比如,在多線程的程序中,如果有多個線程指向相同的實例變量,一個線程可以讀取,另一個線程可以寫入。當它們在同一時間點擊時,讀取的線程將被保證能夠獲取到一個有效的值,可能是更改前的值,也可能是更改后的值。 - 原子屬性與對象的線程安全性是不同的,并且使用原子屬性并不能保證線程安全。如果有多個線程同時訪問同一個實例變量,其中一個線程調用
release
,就會造成程序崩潰。 - 使用
atomic
讓編譯器生成的互斥鎖定代碼會很耗費資源,使程序變慢影響效率,所以一般很少使用。 -
atomic
屬性合成的訪問器方法是私有的,因此不能與自己實現的訪問器方法相結合。如果我們嘗試為atomic
屬性提供自定義的setter或getter方法,會收到編譯器的警告:
或者
nonatomic
-
nonatomic
與atomic
相反,表示非原子屬性。使用該屬性是為了告訴系統不要使用互斥鎖定保護屬性的存取方法。當有多個線程同時訪問同一個屬性時將會導致無法預計的結果。在這里不能保證調用訪問器方法時會返回一個有效的值。如果嘗試在寫入的中間讀取時,我們可能會得到一個無效的值。 - 跟
atomic
比起來,nonatomic
效率要更高一些,如果要多次訪問一個屬性時,使用nonatomic
會更高效。 - 一般在單線程和明確知道只有一個線程訪問的情況下廣泛使用。
- 使用
nonatomic
可以將自動合成的setter或getter方法與自己手動實現的getter或setter方法相結合。因此,對于使用atomic
實現setter或getter時出現的警告,可以通過設置屬性的特性為nonatomic
來解除:
2.5.2 訪問器相關:readonly、readwrite
readonly
-
readonly
表示只讀屬性,對包括自身在內的所有類都是只讀的。 - 合成的訪問器只有getter方法沒有setter方法。
readwrite(默認)
- 默認特性,一般不需要顯式聲明。
-
readwrite
表示讀寫屬性,即屬性允許被自身或其他類讀寫,與readonly
相反。 - 合成的訪問器同時擁有setter方法和getter方法。
如果希望一個屬性只允許自身讀寫,而對其他類都是只讀的,可以在
.h
文件的接口部分中將屬性的特性聲明為readonly
,然后在.m
文件的私有接口部分再重新將屬性特性聲明為readwrite
即可。
2.5.3 內存相關:retain、assign、strong、weak、copy、unsafe_unretained
retain
-
retain
表示屬性的保留操作,用于獲取對象的所有權,會增加傳入對象的引用計數。 - 在屬性中使用
retain
關鍵字可以指示編譯器在設置實例變量之前保留傳入的變量,在默認的設值方法中會是:
if (_property != newValue) {
[_property release];
_property = [newValue retain];
}
- 主要在手動內存管理中使用,在ARC下,一般使用
strong
替代。
assign(默認)
- 默認特性。一般像
int
、float
、double
和NSInteger
、CGFloat
、BOOL
等值類型的屬性默認使用assign
。 -
assign
用于屬性的賦值操作,不存在所有權關系。在默認的設值方法中會是:
_property = newValue;
copy
-
copy
用于創建對象的副本,并且對該副本對象擁有所有權,而非原對象本身。 - 在屬性中使用
copy
關鍵字可以指示編譯器在設置實例變量之前創建傳入變量的副本,在默認的設值方法中會是:
if (_property != newValue) {
[_property release];
_property = [newValue copy];
}
-
copy
屬性對其創建的副本對象隱式強引用。同時也意味著該屬性將使用strong
,因為它必須保持其創建的副本對象。 - 為
copy
屬性設置的任何對象必須遵守NSCopying
協議。 - 如果需要直接設置
copy
屬性的實例變量,例如在初始化方法中,要記得設置原始對象的副本。
strong(默認)
- 默認特性。一般Objective-C對象的屬性默認是
strong
。 -
strong
是在引入ARC的時候引入的關鍵字,相當于MRR下的retain
。 - 使用
strong
聲明強引用,表示實例變量對傳入的對象擁有所有權,只要持有該屬性中對象的引用,該對象就不會被釋放。如果有兩個強引用的對象相互指向對方,就會造成強引用循環。 - 需要區分的是,在Objective-C中,對象屬性默認是
strong
,而對象變量默認是__strong
。
weak
-
weak
也是ARC下的屬性關鍵字,但它是從iOS 5引入,因此在iOS 5之前不可用。 - 使用
weak
聲明弱引用,與strong
相反,它表示實例變量對傳入的對象沒有所有權,并且在setter方法中也不會對傳入對象增加引用計數。當對象被釋放后,實例變量會自動設置為nil。 - 對于變量來說,我們可以使用
__weak
將變量聲明為弱指針變量,并且在實際應用中,我們也通過使用__weak
將強引用替換為弱引用,以此來解決強引用循環的問題:
NSObject * __weak weakVariable;
- 一般情況下,delegate和outlet用
weak
來聲明。
unsafe_unretained
-
unsafe_unretained
表示不安全的引用,是iOS 5之前替代weak
的關鍵字。 - 與
weak
不同的是,使用unsafe_unretained
聲明的屬性,當對象被釋放后,實例變量不會自動設置為nil。這意味著我們將留下一個懸掛指針,指向原先被釋放的對象所占用的內存,會導致程序崩潰,因此它被稱為是“不安全的”。
2.6 內存管理要避免的問題
內存管理不正確導致的主要問題有兩種:
釋放或重寫仍在使用的內存導致內存損壞:通常會導致應用程序崩潰,甚至導致用戶數據遭到改寫或損壞。
沒有釋放不再使用的內存導致內存泄漏:會導致應用程序對內存的使用不斷增加,從而導致系統性能下降或應用程序被終止。
對于上面第一種問題,我們可以使用NSZombieEnabled調試工具來查找過度釋放的對象。對于第二種問題,可以使用Instruments跟蹤引用計數事件并查找內存泄漏。
如果想在編譯時識別出代碼中的問題,可以使用Xcode中內置的靜態分析功能。這將使XCode運行我們的代碼,并查找可以自動檢測到的任何錯誤,以警告我們有任何潛在的問題。下面我們就對之前的項目使用該功能來檢測下是否存在問題:
打開項目,在頂部的菜單欄選擇Product\Analyze:
分析完成后,你會看到一個藍色的小方塊標記,點擊之后會在左側看到詳細信息:
上面的消息告訴我們檢測到內存泄漏,實例變量_window
需要被釋放,缺少dealloc
方法,所以我們在AppDelegate.m
的實現部分添加dealloc
方法,并對實例變量_window
調用release
方法:
- (void)dealloc
{
[_window release];
_window = nil;
[super dealloc];
}
再次點擊菜單欄中的Product\Analyze,你會看到之前的標記消失了,同時也沒有檢測到其他新問題。
3 自動引用計數 ARC
自動引用計數,即ARC(Automatic Reference Counting),是Xcode 4.2版本的一個新特性,使用了與手動管理相同的引用計數系統,不同的是,系統在編譯時會幫我們插入合適的內存管理方法,保留和釋放都是自動處理的,從而避免了手動引用計數的一些潛在陷阱。一般在新項目中被推薦使用。
3.1 ARC下內存管理的規則
ARC的規則很簡單,我們不需要再手動保留和釋放對象,需要做的只是管理指向對象的指針。只要有指針指向該對象,該對象將保留在內存中;當指針指向其他對象,或不存在時,該對象將被自動釋放。
下面我們列出在ARC下內存管理的規則:
- 不能顯式調用
dealloc
,或retain
、release
、retainCount
、autorelease
等,甚至也不能使用@selector(retain)
、@selector(release)
等。 - 如果需要管理除釋放實例變量之外的資源,則可以實現
dealloc
方法,并且自定義的dealloc
方法不需要調用[super dealloc]
。 - 訪問器方法的名稱不能以
new
開頭,這反過來也意味著不能聲明一個以new
開頭的屬性,除非我們指定了一個不同的getter:
//錯誤:
@property NSString * newTitle;
//正確:
@property(getter = theNewTitle)NSString * newTitle;
3.2 MRR轉化為ARC
在上面的demo中我們都是用MRR來進行內存管理的,現在我們需要把它轉化成ARC,最容易想到的方法是手動轉換,需要把所有調用到retain
、release
等方法的代碼都去掉,但這樣做會很麻煩。幸運的是,Xcode提供了一個ARC自動轉換工具,可以幫助我們方便地將源碼轉為ARC,更靈活的是,它不但可以將項目中的所有文件轉換為ARC,還可以選擇性地對指定的文件進行轉換,對于一些不想轉換的文件可以禁用ARC。
下面我們就使用這種自動轉換工具轉換我們的代碼:
首先,ARC是LLVM3.0編譯器的特性,因此我們先來確認下當前的編譯器是否符合。選中文件中的項目,在Build Settings搜索框中輸入“compiler”,然后在Build Options中查看第一項Compiler for C/C++/Objective-C對應的編譯器版本:
在轉換前我們先點擊Xcode菜單欄中的Product\Build以確保當前代碼沒有問題。
接下來從Xcode的菜單欄中選擇Edit\Convert\To Objective-C ARC...:
然后在彈出的窗口中點擊第一個圖標下的小三角可以展開所有文件,在這里,為了對比手動轉換,我們取消選中AppDelegate.m文件,只選中其他兩個默認勾選的文件進行轉換,然后點擊check:
繼續在彈出的窗口點擊Next:
你會看到正在生成轉化:
轉換完成后將會顯示所有文件的預覽。左側窗格顯示已更改的文件,右側窗格顯示原文件。這里顯示的是TableViewController.h文件,你會看到在屬性聲明中使用strong
替代了retain
。
接下來我們切換到TableViewController.m文件:
這里一共有兩處更改。首先是在viewDidLoad方法中,初始化titles
時刪除了autorelease
的調用。然后刪除了dealloc
方法及內容。
確認后,繼續點擊Save保存更改,就可以在我們的項目中看到之前預覽文件的更改,轉換完成。
再次編譯程序,點擊Product\Build,顯示編譯成功。
還有一點需要知道的是,在同一個項目中將ARC代碼與非ARC代碼相結合是可行的。下面我們打開AppDelegate.m文件,會看到該文件依然存在dealloc
方法,并且可以正常運行,這是因為我們取消勾選該文件時,轉換工具已經禁用這兩個源文件的ARC,我們可以在TARGETS的Build Phases中看到:
AppDelegate.m文件后面被加上了-fno-objc-arc
的編譯標記,表示該文件將不使用ARC規則進行編譯。相反地,如果想對特定的文件啟用ARC,可以為其添加-fobjc-arc
標記。在這里,我們雙擊AppDelegate.m文件后面的的標記,將其更改為-fobjc-arc
以對該文件啟用ARC:
再次點擊Product\Build編譯程序,會看到錯誤提示,下面我們就手動更改代碼來修復這些錯誤:
首先打開AppDelegate.m文件,看到錯誤主要發生在dealloc
方法中:
從錯誤信息中我們不難看出,主要是由于在ARC下調用release
和dealloc
導致的。我們刪除整個dealloc
方法,錯誤消失,再次點擊Product\Build編譯程序,編譯成功。此時,運行程序,會發現一切正常顯示。
至此,我們的轉換工作已經全部完成,項目中的所有文件都使用了ARC。如果對其中的代碼還有問題,可以下載MemoryManagementDemo查看。
4 Core Foundation 對象的內存管理
4.1 Core Foundation
Core Foundation是基于Objective-C的Foundation框架,但是以C語言實現。對于大多數應用程序,我們并不需要使用Core Foundation,一般從Objective-C中就可以完成任何我們想要的操作。然而,對于一些底層的API,比如Core Graphics和Core Text等,就需要我們對Core Foundation有所了解。
一般底層的Core Foundation對象大多以CreateWith
開頭的函數來創建,其內存管理只需要延續手工引用計數的辦法即可。對于引用計數的修改,需要使用CFRetain
和CFRelease
函數,其功能與Objective-C對象的retain
和release
方法類似。
// 創建一個CFStringRef對象
CFStringRef cfString = CFStringCreateWithCString(kCFAllocatorDefault, "string", kCFStringEncodingUTF8);
// 保留該對象,引用計數加1
CFRetain(cfString);
// 釋放該對象,引用計數減1
CFRelease(cfString);
4.2 免費橋接(toll-free bridged)
Core Foundation框架和Foundation框架中有許多數據類型可以互換使用,比如我們可以使用NSString
對象將其用作CFStringRef
,也可以使用CFStringRef
對象將其用作NSString
。這種可以互換使用的數據類型也被稱為免費橋接數據類型。這意味著我們可以使用相同的數據結構作為Core Foundation函數調用的參數或者Objective-C消息調用的接收者。然而,并不是所有的數據類型都是免費橋接的,詳細列表可以參考Toll-Free Bridged Types。
在免費橋接中,與內存管理相關的一個重要問題就是轉換過程中對象的所有權問題。比如在ARC下,我們需要將一個Core Foundation對象轉換成一個Objective-C對象,這時候就需要告訴編譯器如何管理對象的所有權。于是我們引入bridge相關的關鍵字來說明對象的所有權語義:
4.2.1 __bridge
使用__bridge
可以在Objective-C對象和Core Foundation對象之間相互轉換,此轉換只做類型轉換,不轉移對象的所有權。
- 使用
__bridge
將Objective-C對象轉換為Core Foundation對象,使用完成后由ARC負責釋放對象:
// 創建一個NSString對象
NSString *nsString = @"string";
// 將NSString對象轉換為CFStringRef對象
CFStringRef cfString = (__bridge CFStringRef)nsString;
- 使用
__bridge
將Core Foundation對象轉換為Objective-C對象,完成后需要調用CFRelease
釋放對象:
// 創建一個CFStringRef對象
CFStringRef cfString = CFStringCreateWithCString(kCFAllocatorDefault, "string", kCFStringEncodingUTF8);
// 將CFStringRef對象轉換為NSString對象
NSString *nsString = (__bridge NSString*)cfString;
// 釋放CFStringRef對象
CFRelease(cfString);
4.2.2 __bridge_retained
使用__bridge_retained
或CFBridgingRetain
將Objective-C對象轉換為Core Foundation對象,此轉換會將Objective-C對象的所有權轉移給Core Foundation對象,使用完成后需要調用CFRelease
釋放對象所有權。
// 創建一個NSString對象
NSString *nsString = @"string";
// 將NSString對象轉換為CFStringRef對象
CFStringRef cfString = (__bridge_retained CFStringRef)nsString;
// 釋放CFStringRef對象
CFRelease(cfString);
4.2.3 __bridge_transfer
使用__bridge_transfer
或CFBridgingRelease
將Core Foundation對象轉換為Objective-C對象,此轉換會將Core Foundation對象的所有權轉移給ARC,使用完成后由ARC負責釋放對象所有權。
// 創建一個CFStringRef對象
CFStringRef cfString = CFStringCreateWithCString(kCFAllocatorDefault, "string", kCFStringEncodingUTF8);
// 將CFStringRef對象轉換為NSString對象
NSString *nsString = (__bridge_transfer NSString*)cfString;
5 參考資料
Advanced Memory Management Programming Guide
Transitioning to ARC Release Notes
Memory Management Tutorial for iOS