天天看點

iOS Objective-C記憶體管理

大概是因為 Objective-C 是 C的超集,是以Objective-C 也使用alloc來申請記憶體,不同的是C調用free來直接釋放記憶體,而Objective-C 不直接調用dealloc來釋放記憶體。整個

Objective-C 都使用對象的引用,而每個對象都有一個引用計數器。當計數器為0時,系統調用dealloc來釋放記憶體。Objective-C 業提供了autorelease 屬性,進而可以讓系統自

動釋放對象所占有的記憶體。程式中可以設定一個自動釋放池,系統使用這個池來跟蹤對象。

使用如下語句可以建立一個自動釋放池:

NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

在我們的程式入口處,xcode也幫我們建了一個自動釋放池,而且把整個程式循環加了進去。

int main(int argc, char *argv[])

{

    @autoreleasepool {

        // 這個說實話我也沒搞明白,不過我知道它建立了一個自動釋放池,然後把我們的程式放到了這個池子裡

        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));

    }

}

// 先别管這個的具體意思,你隻要知道xcode幫我們建了一個自動釋放池,然後把整個程式放到了自動釋放池裡面。

在這個 自動釋放池建立之後,基礎架構就自動将數組、字元串等對放到這個池中。當下面語句被執行時,池中的對象就被自動釋放:

[pool drain];

總之,一個對象被辨別為自動釋放對象,那麼就會被加到自動釋放池中。除了使用drain來釋放池中的對象外,在自動釋放池本身被釋放時,池中的所有對象也會被釋放。

自動釋放池語句如下:

[pool release];

之前是沒有ARC(Automatic Reference Counting)功能的,現在有了,我想ARC 主要是為從Java轉過來的程式員而設計的吧,因為這個差不多相當于Java裡面的自動垃圾收集器吧

。從C/C++轉過來的程式員大多數還是願意手動去釋放的,因為他們不大相信自動釋放的效率,還有就是他們喜歡掌控一切的感覺,比如我。如果你要開啟,在建立工程的時候請勾選上這

個選項:

如果你使用手動管理記憶體,請往下看:

一、申請記憶體(alloc)

當使用 alloc 建立了一個對象時,需要在用完這個對象後釋放(release)它,你不需要自己去釋放一個被設定為自動釋放(autorelease)的對象,如果你真的這麼幹了,對不起,程式

會崩潰。

// str1 是autorelease的,會自動釋放

NSString *str1 = [NSString string];

// str2需要手動釋放

NSString *str2 = [[NSString alloc]init];

// str2 使用完後

[str2 release];

為便于了解,你可以了解成autorelease型的對象會在該釋放他的時候自動釋放。

二、釋放記憶體(dealloc)

當一個對象從記憶體上删除之前,系統就會調用dealloc方法,這是釋放對象成員變量的最好時機。比如:

- (void)dealloc

{

    [_window release];

    [super dealloc];

}

上例中,先調用release釋放了成員變量_window所占用的記憶體。相對而言,用标準的release比autorelease更快一點。最後一行[ super dealloc ]; 非常重要,必須調用這個方

法讓父類清楚它自己,否則會造成記憶體洩漏。

在ARC模式下,dealloc 不會被調用到,取而代之的是,需要實作finalize方法 。

三、引用計數器(retainCout)

整個Objective-C都使用對象引用,而每個對象有一個引用計數器。當時用alooc(或copy)方法建立一個對象時。其計數器的值為 1 。當計數器為 0 時,系統自動調用 dealloc方

法來釋放記憶體中的對象。比如:

STU *stu = [[STU alloc]init]; // 計數器為1

[stu retain];  // 計數器為2

[stu release]; // 計數器為1

[stu release]; // 計數器為0,系統自動調用dealloc方法

// 釋放之後,如果調用該對象的任何一個方法,程式就會異常終止

[stu goHiking]; // 崩潰

為了防止上述的異常崩潰出現,可以在最後一個release之後加一句:

stu = nil;

上述語句将該對象置為nil。在這之後,任何調用該member的方法都傳回nil,而不是異常終止。再來看下面的例子:

int main(int argc, char *argv[])

{

    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

    NSNumber* int1 = [NSNumber numberWithInt:222];

    NSNumber *int2;

    NSMutableArray *arr1 = [NSMutableArray array];

    NSLog(@"初始化以後:int1(retainCount):%lu int2(retainCount):%lu",(unsigned long)[int1 retainCount],(unsigned long)[int2 retainCount]);

    [arr1 addObject:int1];

    NSLog(@"将int1添加到數組以後:int1(retainCount):%lu int2(retainCount):%lu",(unsigned long)[int1 retainCount],(unsigned long)[int2 retainCount]);

    int2 = int1;

    NSLog(@"進行指派操作以後:int1(retainCount):%lu int2(retainCount):%lu",(unsigned long)[int1 retainCount],(unsigned long)[int2 retainCount]);

    [int1 retain];

    NSLog(@"int1 retain以後:int1(retainCount):%lu int2(retainCount):%lu",(unsigned long)[int1 retainCount],(unsigned long)[int2 retainCount]);

    [int1 release];

    NSLog(@"int1 release以後:int1(retainCount):%lu int2(retainCount):%lu",(unsigned long)[int1 retainCount],(unsigned long)[int2 retainCount]);

    [arr1 removeObject:0];

    NSLog(@"從數組中删除int1以後:int1(retainCount):%lu int2(retainCount):%lu",(unsigned long)[int1 retainCount],(unsigned long)[int2 retainCount]);

    [pool drain];

    return 0;

}

程式結果:

初始化以後:int1(retainCount):1 int2(retainCount):0

将int1添加到數組以後:int1(retainCount):2 int2(retainCount):0

進行指派操作以後:int1(retainCount):2 int2(retainCount):2

int1 retain以後:int1(retainCount):3 int2(retainCount):3

int1 release以後:int1(retainCount):2 int2(retainCount):2

從數組中删除int1以後:int1(retainCount):1 int2(retainCount):1

基礎架構所提供的一些方法也會增加對象的引用次數,比如,把對象添加到一個數組中的時候。

在上面的例子中,首先将NSNumber對象int1的值設定為整數222,我們定義了int2但是沒有初始化。這時候列印引用計數器,會發現int1的計數器為1,int2的計數器為0。也就是說,

隻有真正建立了一個對象,并且正常初始化了之後,才會增加引用計數器。

随後使用addObject:方法将int1添加到數組arr1中,列印計數器,不難發現int1的計數器變為2,int2的計數器還是0。也就是說當對象添加到任何類型的集合中時,都會使改對象的

有效引用增加。

接下來,将int1的值賦給int2,這時候的結果值得我們注意。int1與int2的引用計數器都是2了,這說明進行指派操作并未使int1的計數器增加,但是int2的計數器變為了2。這是因為

當把int1賦給int2的時候,并不是複制實際的對象,而是将該對象的記憶體地傳遞給了int2。也就是說這兩個指針指向的是同一個對象。

然後我們向int1發送retain消息來增加引用計數,int2的引用計數器當然也随之增加咯。

然後向int1發送release消息,int1與int2的引用計數當然同時減 1 咯。

最後,從arr1中移除 int1 ,這樣int1與int2的引用計數又同時減 1 。但是這時候對象并未真正釋放,因為引用計數器 1,并不是0。如果要釋放你還得 release一次。

Objective-C的記憶體管理系統基于引用計數。我們需要跟蹤引用,以及在運作期内是否真的釋放了記憶體。簡單來說就是每次調用了alloc或者retain之後都需要調用release。

在程式中,你隻在兩種情況下跟蹤一個對象:(1)本地變量(在一個方法裡面臨時使用,比如建立一個字元串)(2)成為一個類的成員變量隻要掌握了跟蹤者兩種情況,并作出相應記憶體釋放

,你就基本掌握了Objective-C的記憶體管理。

三、字元串處理

如果使用alloc或者copy建立一個對象(比如字元串),那麼在方法結束的時候需要release或者autorelease這個對象。假如我們是通過别的方式建立的,就可以不管。在下面的粒子

中,隻需要release那些使用alloc建立的對象:

NSNumber *num1 = [[NSNumber alloc] initWithFloat:1.80];

NSNumber *num2 = [NSNumber numberWithFloat:145.0];

// 我們隻需要釋放num1,不用釋放num2

[num1 release];

再看下面的例子:

int main(int argc, char *argv[])

{

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    NSString* str1 = @"LiuWei";

    NSString* str2 = [NSString stringWithString:@"IUKEY"];

    NSMutableString *str3 = [NSMutableString stringWithString:@"MasterLiuWei"];

    NSMutableArray *arr1 = [NSMutableArray array];

    // 初始化以後 各個retainCount:str1:fffffffffffffff str2:fffffffffffffff str3:1

    [arr1 addObject:str1];

    [arr1 addObject:str2];

    [arr1 addObject:str3];

    // 将字元串添加到數組以後,各個retainCount:str1:fffffffffffffff str2:fffffffffffffff str3:2

    [str1 retain];

    [str2 retain];

    [str3 retain];

    // 執行retain以後,各個retainCount:str1:fffffffffffffff str2:fffffffffffffff str3:3

    [str3 release];

    // 執行release以後,各個retainCount:str1:fffffffffffffff str2:fffffffffffffff str3:2

    [pool drain];

    return 0;

}

你可能會對結果産生疑問,為什麼是fffffffffffffff 呢?在程式中,NSString 的對象 str1 指派為靜态的字元串@"welcome"。字元串常量的記憶體配置設定方式和其他的對象不同,這

種方式沒有引用計數機制,是以永遠不能釋放這些對象。當向 str1 發送消息 retainCount 的時候,它傳回fffffffffffffff ,即最大的無符号整數。注意:這同樣适用于用字元

串常量初始化那些不可變的字元串對象。

值得注意的是下面的語句:

NSMutableString *str3 = [NSMutableString stringWithString:@"MasterLiuWei"];

這條語句使用字元串常量來初始化一個可變字元串對象,通過 stringWithString: 方法進行。由于可變字元串的至可能在程式運作過程中發生變化。但是字元串常量是無法改變的,所

以系統拷貝這個字元串常量到 str3 上來完成初始化。是以可變字元串對象擁有了引用計數。

當我們使用stringWithString:  方法建立不可變字元串對象時,這個對象其實被添加到了自動釋放池。通過array 方法所建立的對象arr1 對象也被添加到了自動釋放池。

程式中的代碼塊将這些字元串添加到數組中,冰箱這給謝對象發送retain 消息。從程式的結果可以看出,這些操作改變了他們的引用技術。

在釋放自動釋放池之前,str3已經被釋放了一次。是以 str3 的引用計數變為2。随後,自動釋放池的釋放使得這些對想的引用技術減為 0 ,這使得他們被釋放。當釋放自動釋放池的時

候,池中的每一個對象都會收到一條 release 消息(收到的條數等于當時發送autorelease消息的條數)。由于在使用 stringWithString: 方法所建立的字元串對象 str3 時,

系統将他添加到自動釋放池,也就是說調用了 autorelease 方法,是以,當自動釋放池被釋放(drain)時,str3會收到一條  release消息,這将導緻他的引用計數減為1。當自動釋

放池中的數組被釋放的時候,數組中的每個元素也将被釋放,是以數組中的每個元素都将收到一條release'消息,這将使str3的引用計數變為0 。系統随之将調用str3的dealloc方法将str3釋放。

注意:如果我們過度釋放,程式将異常終止。

四、類成員變量

在大多數情況下,一個成員變量的setter方法應該僅僅 aytorelease/release 舊的對象,然後 retain/copy 新的對象。我們隻需要在 dealloc的時候調用release就好了。所

以真正需要管理的就是方法内部的局部變量的引用。比如,設定字元串(使用copy):

- (void)setName:(NSString *)newName

{

    if (name != newName)

    {

        [name release];

        name = [newName copy]; // name 的計數器為1

    }

}

下面是使用retain管理成員對象的另一個例子:

- (void)setScore:(NSNumber *)inputScore

{

    [score autorelease];

    score = [inputScore retain];

}

- (void)dealloc

{

    [score release];

    [super dealloc];

}

下面是使用本地對象去設定一個成員變量的例子:

NSNumber *value1 = [[NSNumber alloc] initWithFloat:180.0];

[self setScore:value1];

NSNumber *value2 = [NSNumber numberWithFloat:72.5];

[self setScore:value2];

[value1 release];

下面我來分析一下幾種成員變量的處理方法。

首先定義一個類,它隻包含NSString類型的name屬性,并手動書寫了這個屬性的存取方法。

#import <Foundation/Foundation.h>

@interface Student : NSObject{

    NSString *name;

}

- (void)setName:(NSString *)s;

- (NSString *)name;

@end

在下面的方法實作檔案中,我們直接把setName: 方法中的形參 s 的值賦給 name 屬性,并沒有做其他的操作。

#import "Student.h"

@implementation Student

- (void)setName:(NSString *)s

{

    name = s;

}

- (NSString *)name

{

    return name;

}

@end

下面是測試代碼:

NSMutableString *str1 = [NSMutableString stringWithString:@"LiuWei"];

Student* stu = [[Student alloc] init];

NSLog(@"str1的引用計數器:%x",[str1 retainCount]);

[stu setName:str1];

NSLog(@"str1的引用計數器:%x",[str1 retainCount]);

[stu release];

測試結果:

str1 的引用計數器:1

str1 的引用計數器:1

程式首先為Student類建立了對象stu ,然後調用這個對象的setName 方法,将name的值設定為  str1的值(str1 和name 這兩個指針指向的是同一塊記憶體空間)。這種做法有一個

問題,在設定完畢以後,如果不需要str1對象并将其釋放,那麼str1的 引用計數器變為0。毫無疑問,name的引用計數器也将變為0 ,存儲在那麼中的值則變為無效的。

由于str1是用 NSMutableString stringWithString: 方法建立的,該對象也被自動添加到自動釋放池之中(雖然沒有顯式将對象添加到 自動釋放池中)。是以,在自動釋放池釋放

的時候,str1 也會被釋放,這時,任何str1所指向的對象的通路都将是無效的。

下面我們來看第二種寫法。接口檔案未改動,是以參看上面的接口代碼。在實作檔案中我們執行了 [ name retain] 方法,這樣會使 name 的引用計數器增加 1 ,進而在str1 釋放

的時候,name 還能夠正常使用。

#import "Student.h"

@implementation Student

- (void)setName:(NSString *)s

{

    name = s;

    [name retain];

}

- (NSString *)name

{

    return name;

}

@end

再來測試:

NSMutableString *str1 = [NSMutableString stringWithString:@"LiuWei"];

Student* stu = [[Student alloc] init];

NSLog(@"str1的引用計數器:%x",[str1 retainCount]);

[stu setName:str1];

NSLog(@"str1的引用計數器:%x",[str1 retainCount]);

[stu release];

NSLog(@"str1的引用計數器:%x",[str1 retainCount]);

測試結果:

str1 的引用計數器:1

str1 的引用計數器:2

str1 的引用計數器:1

根據結果,在調用 setName: 之後,str1的引用計數器變為2 ,上一個程式中所産生的問題得以解決。接着就在程式中執行了 [str1 release] 方法,使得 str1 的引用計數器減

少為 1,這時候對name的引用依然有效。

由于程式使用了alloc方法建立 Student 類的對象,就需要負責将這個對象釋放掉。我們可以手動釋放,也可以調用 它的autorelease 方法,将其添加到自動釋放池中。在這之前我

們都能正常使用這個對象。

但是這樣的寫法還是存在一些問題,setName: 方法保持了作為參數傳入的字元串對象,但是這個對象什麼時候釋放呢?當多次 更改name 值的時候,原來的那些值會怎樣?應該釋放他

們所占用的記憶體麼?

下面來看最終的處理方法:

//  Student.h

#import <Foundation/Foundation.h>

@interface Student : NSObject

{

    NSString *name;

}

- (void)setName:(NSString *)s;

- (NSString *)name;

- (void)dealloc;

@end

//  Student.m

#import "Student.h"

@implementation Student

- (void)setName:(NSString *)s

{

    [name autorelease];

    name = [s retain];

}

- (NSString *)name

{

    return name;

}

- (void)dealloc

{

    [name release];

    [super dealloc];

}

@end

測試代碼:

NSMutableString *str1 = [NSMutableString stringWithString:@"LiuWei"];

Student* stu = [[Student alloc] init];

NSLog(@"str1的引用計數器:%x",[str1 retainCount]);

[stu setName:str1];

NSLog(@"str1的引用計數器:%x",[str1 retainCount]);

測試結果:

str1 的引用計數器:1

str1 的引用計數器:2

通過上面的方法,不管name中目前存儲的值是什麼,我們在 setName: 方法中首先将其手動添加到自動釋放池中,這也可以讓其以後自動釋放 name 。當程式中多次調用 setName:

方法時,這項操作将更加重要:每次存儲新的值的時候,變量就得值将标記為自動釋放,保持新的值到name中。

當系統釋放一個對象時,系統自動調用dealloc 方法。對于保持(retain)的對象,或者使用alloc配置設定的對象,或者在方法中複制的對象,可以在dealloc方法中去釋放它們。首先釋

放name變量,因為他是保持對象:然後調用父類的dealloc方法來釋放Student 的對象。如果需要外部的字元串完全獨立于設定方法的參數,可以在設定方法中聲稱字元串的全新副本,

即使用copy選項。

五、自動釋放(autorelease)池

在代碼中建立了一個字元串,如果需要傳回這個字元串的話,那麼需要使用 autorelease,而不是release。

比如:

- (NSString *)getStr

{

    NSString *str;

    str = [[NSString alloc]initWithFormat:@"Result return "];

    //[ str release];  // 不能釋放,否則調用者無法獲得傳回值

    [str autorelease];// 正确

    return str;

}

通過使用autorelease,該對象就被放入自動釋放池,系統自動跟蹤每個對象的使用情況,并在

釋放自動釋放池時, 釋放池中的所有對象

注意:autorelease 不是垃圾搜集(Garbage Collection)功能。在iPhone 作業系統上的Objective-C沒有垃圾搜集功能。另外,你可以建立多個自動釋放池。

int main(int argc, char *argv[])

{

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    Student*stu = [[Student alloc] init];//1

    NSLog(@"%x",[stu retainCount]);

    [pool drain];

    NSLog(@"%x",[stu retainCount]);//1

    pool = [[NSAutoreleasePool alloc] init];

    [stu autorelease];

    NSLog(@"%x",[stu retainCount]);//1

    [stu retain];

    NSLog(@"%x",[stu retainCount]);//2

    [pool drain];

    NSLog(@"%x",[stu retainCount]);//1

    [stu release];

    return 0;

}

六、記憶體洩漏

為了防止記憶體洩漏,并確定最有效地使用記憶體,應用程式應該隻在需要時才裝載資料。,比如,使用者點選了某個按鈕才顯示相關資料。我們在前面講了autorelease ,但是如果你的代碼

總是 autorelease,就沒有記憶體洩漏了嗎?問題在于autorelease 池釋放的時機。每當執行應用程式時,系統自動建立 autorelease 池。系統并不是立即釋放autorelease池中

的對象,而是在一個run loop之後才釋放,一般是微妙級别。在 autorelease 池中的對象本可以立即釋放,但是系統很有可能過一段時間才釋放他們,是以 使用release 可以更有

效地釋放記憶體。是以,我們應該盡量自己管理記憶體,不要太依賴 autorelease 。另外,一些系統對象使用 autorelease,如NSString。你可以 ziji管理記憶體的類,如

NSMutableString,你可以建立自己的池。

下面是幾個記憶體管理的基本原則:

1>如果使用 alloc(或者copy)方法建立一個對象,或者使用retain 保留一個對象,那麼 ,都要自己釋放對象。

2>在大多數情況下,申請記憶體的語句數量和釋放記憶體的語句數量應該相等

3>盡量少使用記憶體,用完立即釋放

七、copy、nonatomic

對于字元串類型的屬性變量,我們經常使用下面類似的語句:

@property(nonatomic,copy)NSString *name;

這個語句就等價于:

- (void)setName:(NSString *)s

{

    if (s != name)

    {

        [name release];

        name = [s copy];

    }

}

那麼為什麼要使用copy呢?如果直接使用下面的語句,結果将如何呢?

- (void)setName:(NSString *)s

{

    name = s;

}

結果是name 和 s 都指向同一個對象。當在調用 setName: 方法之後,如果 s 的值被修改,這顯然不是我們想要的結果。是以,使用copy 來拷貝 s 的值到name 上,其完成的功能

就是調用一個alloc 方法來建立一個新的字元串對象 (initWithStringLs).

在多線程中,兩個或多個線程可能在同一時間執行同一代碼。為了防止這種現象發生,開發人員可以使用互斥鎖。nonatomic 的意思是不需要使用互斥鎖, atomic 是使用互斥鎖。缺

省 是atomic 。 如果你的程式并沒有多線程,可以設定為 nonatomic 以節省資源。

文章出處:http://blog.csdn.net/iukey