| 聲明 歡迎轉載,但是請尊重作者勞動成果,轉載請保留此框內聲明,謝謝。 文章出處:http://blog.csdn.net/iukey |
大概是因為 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 以節省資源。