Objective-C中的內存管理

內存管理是程序在運行時分配內存、使用內存,并在程序完成時釋放內存的過程。在Objective-C中,也被看作是在眾多數據和代碼之間分配有限內存資源的所有權(Ownership)的一種方式。

內存管理關心的是清理或回收不用的內存,以便內存能夠再次利用。如果一個對象不再使用,就需要釋放對象占用的內存。Objective-C提供了兩種內存管理的方法:手動管理內存計數(MRR)和自動引用計數(ARC)。這兩種方法都采用了一種稱為“引用計數”的模型來實現,該模型由Foundation框架的NSObject類和運行時環境(Runtime Environment)共同提供。下面,我們就先來介紹下什么是引用計數。

1 引用計數

引用計數(Reference Count)是一個簡單而有效的管理對象生命周期的方式,一般概念是:當創建一個新的對象時,初始的引用計數為1。為保證對象的存在,每當創建一個引用到該對象時,通過給對象發送retain消息,為引用計數加1;當不再需要對象時,通過給對象發送release消息,為引用計數減1;當對象的引用計數為0時,系統就知道這個對象不再使用了,通過給對象發送dealloc消息,銷毀對象并回收內存。一般在retain方法之后,引用計數通常也被稱為保留計數(retain count)。

referenceCount.png

為了更好地闡述引用計數的機制,這里引用開關房間燈的例子來說明:

假設辦公室的照明設備只有一個,進入辦公室的人需要照明,離開辦公室的人不需要照明。因此,為保證辦公室僅有的照明設備得到很好的管理,只要辦公室還有人,就會需要照明,燈就得開著,而當辦公室沒人的時候,就需要關燈。為了判斷辦公室是否還有人,我們導入計數功能來計算“需要照明的人數”:

  • 第一個進入辦公室的人,需要照明,此時,“需要照明的人數”為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,如果是,將它關閉,如下圖所示:

closeARC.png

然后打開storyboard,刪除當前視圖控制器,從右下角的對象庫中找到Navigation Controller,并拖動到畫布上。選中左邊的導航控制器,切換到屬性檢查器,勾選View Controller一欄下面的is Initial View Controller選項,將該控制器作為初始控制器,完成后,該控制器左側有個白色的箭頭,如下:

addNavigationController.png

這時候如果你點擊運行,會在模擬器中看到一個空的表視圖:

emptyTableView.png

接下來我們刪除ViewController.h文件和ViewController.m文件,并創建一個新的文件(File\New\File...),使它成為的UITableViewController的子類:

createNewFile.png

打開剛才創建的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的標識符為空,需要添加:

addCellIdentifier.png

最后,打開storyboard,將視圖控制器設置為TableViewController

setController.png

點擊運行,可以看到表視圖中的內容:

tableView.png

現在我們已經知道,在手動管理內存的情況下,調用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?這里有一個簡單的約定可供參考:

  • 如果使用以initcopy開頭的方法創建對象,返回的對象引用計數將為1,并且不會自動釋放。也就是說我們擁有該對象,使用完成后需要調用release釋放,或者調用autorelease延遲釋放。
  • 如果使用任何其它方式開頭的方法創建對象,返回的對象引用計數也將為1,但會自動釋放,使用完成后不需要再調用releaseautorelease。需要注意的是,以這種方式創建的對象,即使我們現在可以使用它,但是并不擁有該對象。

現在我們又有一個新的問題:如果一個對象具有自動釋放,那以后想要再次使用該對象要怎么辦?如果我們嘗試將該對象存儲在某個地方稍后使用的話,可能會因為嘗試訪問已經釋放的內存而引起程序崩潰,因此我們調用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];
}

如果一切順利,點擊運行,你會看到下面的運行結果:

didSelectRow.png

到這里,你對內存管理應該了解一些了,下面我們就來總結一下手動管理內存的規則:

  • allocnewcopymutableCopy開頭的方法創建的對象,我們擁有該對象,使用完成后需要調用releaseautorelease釋放。

  • init方法中為了獲取對象的所有權,或者在某些情況下避免對象被移除,可以使用retain保留對象。在使用完對象后,需要使用release進行釋放。

  • 對使用了retaincopymutableCopyallocnew方法的任何對象,以及具有retaincopy特性的屬性進行釋放,需要重寫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 使用訪問器方法簡化內存管理

手動管理內存時,在代碼中使用retainrelease來保留或釋放對象難免會出錯,為此,我們可以使用訪問器方法來減少內存管理中的問題。

通常所說的訪問器(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];
}

之所以說不要在initdealloc方法中使用訪問器方法,主要是由于面向對象的繼承、多態特性與訪問器方法可能造成的副作用聯合導致的。繼承和多態導致在父類的實現中調用訪問器方法時可能會調用到子類重寫的存取方法,而此時子類部分并未完全初始化或已經銷毀,導致混亂,從而出現一系列的邏輯問題甚至崩潰。但這種說法也不是絕對的,盡管存在風險但并不代表百分之百的崩潰或錯誤,如果你在程序中有這樣寫,并且明確地知道它不會產生任何問題,那么就可以使用它來簡化你的代碼。比如,在這里我們修改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;

需要注意的是,在聲明屬性時,屬性的名稱前面不要以newalloccopy或者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 屬性的特性

屬性的特性是使用一些特殊的關鍵字來告訴編譯器如何合成相關的訪問器方法。在我們的代碼中,聲明屬性時使用了nonatomicretain。在此之前我們講到屬性的聲明時,在示例中也使用到了copy,但屬性的關鍵字遠不止這幾個, 一般分為線程相關、訪問器相關以及內存相關這三類。下面是詳細介紹。

2.5.1 線程相關:atomic、nonatomic

atomic(默認)

  • 默認特性,如果沒有使用任何關鍵字指定屬性的特性,默認情況下Objective-C屬性的特性是atomic
  • atomic表示原子屬性,使用該屬性是為了告訴系統使用互斥(mutex)鎖定保護屬性的存取方法,如果當前進程進行到一半,其他線程來訪問當前線程,可以保證先執行完當前線程。也就是說,即使從不同的線程同時調用訪問器方法,也能夠保證有一個值總能被getter方法完全檢索到或者被setter方法完全設置。比如,在多線程的程序中,如果有多個線程指向相同的實例變量,一個線程可以讀取,另一個線程可以寫入。當它們在同一時間點擊時,讀取的線程將被保證能夠獲取到一個有效的值,可能是更改前的值,也可能是更改后的值。
  • 原子屬性與對象的線程安全性是不同的,并且使用原子屬性并不能保證線程安全。如果有多個線程同時訪問同一個實例變量,其中一個線程調用release,就會造成程序崩潰。
  • 使用atomic讓編譯器生成的互斥鎖定代碼會很耗費資源,使程序變慢影響效率,所以一般很少使用。
  • atomic屬性合成的訪問器方法是私有的,因此不能與自己實現的訪問器方法相結合。如果我們嘗試為atomic屬性提供自定義的setter或getter方法,會收到編譯器的警告:
atomicSetterWarning.png

或者

atomicGetterWarning.png

nonatomic

  • nonatomicatomic相反,表示非原子屬性。使用該屬性是為了告訴系統不要使用互斥鎖定保護屬性的存取方法。當有多個線程同時訪問同一個屬性時將會導致無法預計的結果。在這里不能保證調用訪問器方法時會返回一個有效的值。如果嘗試在寫入的中間讀取時,我們可能會得到一個無效的值。
  • atomic比起來,nonatomic效率要更高一些,如果要多次訪問一個屬性時,使用nonatomic會更高效。
  • 一般在單線程和明確知道只有一個線程訪問的情況下廣泛使用。
  • 使用nonatomic可以將自動合成的setter或getter方法與自己手動實現的getter或setter方法相結合。因此,對于使用atomic實現setter或getter時出現的警告,可以通過設置屬性的特性為nonatomic來解除:
nonatomicFixSetterWarning.png
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(默認)

  • 默認特性。一般像intfloatdoubleNSIntegerCGFloatBOOL等值類型的屬性默認使用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

useAnalyze.png

分析完成后,你會看到一個藍色的小方塊標記,點擊之后會在左側看到詳細信息:

momoryLeak.png

上面的消息告訴我們檢測到內存泄漏,實例變量_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,或retainreleaseretainCountautorelease等,甚至也不能使用@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,最容易想到的方法是手動轉換,需要把所有調用到retainrelease等方法的代碼都去掉,但這樣做會很麻煩。幸運的是,Xcode提供了一個ARC自動轉換工具,可以幫助我們方便地將源碼轉為ARC,更靈活的是,它不但可以將項目中的所有文件轉換為ARC,還可以選擇性地對指定的文件進行轉換,對于一些不想轉換的文件可以禁用ARC。

下面我們就使用這種自動轉換工具轉換我們的代碼:

首先,ARC是LLVM3.0編譯器的特性,因此我們先來確認下當前的編譯器是否符合。選中文件中的項目,在Build Settings搜索框中輸入“compiler”,然后在Build Options中查看第一項Compiler for C/C++/Objective-C對應的編譯器版本:

checkCompiler.png

在轉換前我們先點擊Xcode菜單欄中的Product\Build以確保當前代碼沒有問題。

接下來從Xcode的菜單欄中選擇Edit\Convert\To Objective-C ARC...

convertToARC.png

然后在彈出的窗口中點擊第一個圖標下的小三角可以展開所有文件,在這里,為了對比手動轉換,我們取消選中AppDelegate.m文件,只選中其他兩個默認勾選的文件進行轉換,然后點擊check

selectFiles.png

繼續在彈出的窗口點擊Next

clickNext.png

你會看到正在生成轉化:

generatingPreview.png

轉換完成后將會顯示所有文件的預覽。左側窗格顯示已更改的文件,右側窗格顯示原文件。這里顯示的是TableViewController.h文件,你會看到在屬性聲明中使用strong替代了retain

previewTableViewController.h.png

接下來我們切換到TableViewController.m文件:

previewTableViewController.m.png

這里一共有兩處更改。首先是在viewDidLoad方法中,初始化titles時刪除了autorelease的調用。然后刪除了dealloc方法及內容。

確認后,繼續點擊Save保存更改,就可以在我們的項目中看到之前預覽文件的更改,轉換完成。

再次編譯程序,點擊Product\Build,顯示編譯成功。

還有一點需要知道的是,在同一個項目中將ARC代碼與非ARC代碼相結合是可行的。下面我們打開AppDelegate.m文件,會看到該文件依然存在dealloc方法,并且可以正常運行,這是因為我們取消勾選該文件時,轉換工具已經禁用這兩個源文件的ARC,我們可以在TARGETSBuild Phases中看到:

checkCompilerFlags.png

AppDelegate.m文件后面被加上了-fno-objc-arc的編譯標記,表示該文件將不使用ARC規則進行編譯。相反地,如果想對特定的文件啟用ARC,可以為其添加-fobjc-arc標記。在這里,我們雙擊AppDelegate.m文件后面的的標記,將其更改為-fobjc-arc以對該文件啟用ARC:

changeCompilerFlags.png

再次點擊Product\Build編譯程序,會看到錯誤提示,下面我們就手動更改代碼來修復這些錯誤:

首先打開AppDelegate.m文件,看到錯誤主要發生在dealloc方法中:

errorsMessage.png

從錯誤信息中我們不難看出,主要是由于在ARC下調用releasedealloc導致的。我們刪除整個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開頭的函數來創建,其內存管理只需要延續手工引用計數的辦法即可。對于引用計數的修改,需要使用CFRetainCFRelease函數,其功能與Objective-C對象的retainrelease方法類似。

    // 創建一個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_retainedCFBridgingRetain將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_transferCFBridgingRelease將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

Encapsulating Data

Transitioning to ARC Release Notes

Obj-C Memory Management

Memory Management Tutorial for iOS

Properties Tutorial for iOS

Beginning ARC in iOS 5 Tutorial

iOS核心技術之:內存管理之二手動內存管理

理解 iOS 的內存管理

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容

  • iOS開發中, 之前一直使用swift, 因此對于Objective-C的內存管理機制長期處于混亂的一知半解狀態....
    icetime17閱讀 853評論 1 8
  • 29.理解引用計數 Objective-C語言使用引用計數來管理內存,也就是說,每個對象都有個可以遞增或遞減的計數...
    Code_Ninja閱讀 1,512評論 1 3
  • 1.1 什么是自動引用計數 概念:在 LLVM 編譯器中設置 ARC(Automaitc Reference Co...
    __silhouette閱讀 5,212評論 1 17
  • 內存管理 簡述OC中內存管理機制。與retain配對使用的方法是dealloc還是release,為什么?需要與a...
    丶逐漸閱讀 1,978評論 1 16
  • 秋日 推開門 濃烈的桂花香 大片溫暖的陽光 頭頂上藍藍的天空 操場上打球的小伙兒 留下怔怔的我 披散著頭發 氣若游...
    星梓馨閱讀 223評論 0 1