自動釋放池引用計數器在Xcode4.2及之後的版本中由於引入了ARC(Automatic Reference Counting)機制,程式編譯時間Xcode可以自動給你的代碼添加記憶體釋放代碼,如果編寫手動釋放代碼Xcode會報錯,因此在今天的內容中如果你使用的是Xcode4.2之後的版本(相信現在大部分朋友用的版本都比這個要高),必須手動關閉ARC,這樣才有助於你理解ObjC的記憶體回收機制。
ObjC中的記憶體管理機制跟C語言中指標的內容是同樣重要的,要開發一個程式並不難,但是優秀的程式則更測重於記憶體管理,它們往往佔用記憶體更少,運行更加流暢。雖然在新版Xcode引入了ARC,但是很多時候它並不能完全解決你的問題。在Xcode中關閉ARC:項目屬性—Build Settings--搜尋“garbage”找到Objective-C Automatic Reference Counting設定為No即可。
記憶體管理原理我們都知道在C#、Java中都有GC在自動管理記憶體,當我們執行個體化一個對象之後通常會有一個變數來引用這個對象(變數中儲存物件地址),當這個引用變數不再使用之後(也就是不再引用這個對象)此時GC就會自動回收這個對象,簡單的說就是:當一個對象沒有任何變數引用的時候就會被回收。
例如下面的C#程式碼片段
using System;namespace GC{ class Program { private static void Test() { object o=new object(); } static void Main(string[] args) { Test(); } }}
上面是一段C#代碼,在Test()方法中,通過new Object()建立了一個對象,o是一個對象的引用(儲存了對象的地址),它是一個局部變數,作用範圍就是Test()方法內部。
當執行完Test()方法之後o就會被釋放,此時由於沒有變數在引用new Object()這個對象,因此GC會自動回收這個對象所佔用的空間。
但是在ObjC中沒有記憶體回收機制,那麼ObjC中記憶體又是如何管理的呢?其實在ObjC中記憶體的管理是依賴對象引用計數器來進行的:在ObjC中每個對象內部都有一個與之對應的整數(retainCount),叫“引用計數器”,當一個對象在建立之後它的引用計數器為1,當調用這個對象的alloc、retain、new、copy方法之後引用計數器自動在原來的基礎上加1(ObjC中調用一個對象的方法就是給這個對象發送一個訊息),當調用這個對象的release方法之後它的引用計數器減1,如果一個對象的引用計數器為0,則系統會自動調用這個對象的dealloc方法來銷毀這個對象。
下面通過一個簡單的例子看一下引用計數器的知識:
Person.h
//// Person.h// MemoryManage//// Kenshin Cui on 14-2-15.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import @interface Person : NSObject#pragma mark - 屬性@property (nonatomic,copy) NSString *name;@property (nonatomic,assign) int age;@end
Person.m
//// Person.m// MemoryManage//// Kenshin Cui on 14-2-15.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import Person.h@implementation Person#pragma mark - 覆蓋方法#pragma mark 重寫dealloc方法,在這個方法中通常進行對象釋放操作-(void)dealloc{ NSLog(@Invoke Person's dealloc method.); [super dealloc];//注意最後一定要調用父類的dealloc方法(兩個目的:一是父類可能有其他引用對象需要釋放;二是:當前對象真正的釋放操作是在super的dealloc中完成的)}@end
main.m
//// main.m// MemoryManage//// Created by Kenshin Cui on 14-2-15.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import #import Person.hvoid Test1(){ Person *p=[[Person alloc]init]; //調用alloc,引用計數器+1 p.name=@Kenshin; p.age=28; NSLog(@retainCount=%lu,[p retainCount]); //結果:retainCount=1 [p release]; //結果:Invoke Person's dealloc method. //上面調用過release方法,p指向的對象就會被銷毀,但是此時變數p中還存放著Person對象的地址, //如果不設定p=nil,則p就是一個野指標,它指向的記憶體已經不屬於這個程式,因此是很危險的 p=nil; //如果不設定p=nil,此時如果再調用對象release會報錯,但是如果此時p已經是null 指標了, //則在ObjC中給null 指標發送訊息是不會報錯的 [p release];}void Test2(){ Person *p=[[Person alloc]init]; p.name=@Kenshin; p.age=28; NSLog(@retainCount=%lu,[p retainCount]); //結果:retainCount=1 [p retain];//引用計數器+1 NSLog(@retainCount=%lu,[p retainCount]); //結果:retainCount=2 [p release];//調用1次release引用計數器-1 NSLog(@retainCount=%lu,[p retainCount]); //結果:retainCount=1 [p release]; //結果:Invoke Person's dealloc method. p=nil;}int main(int argc, const char * argv[]) { @autoreleasepool { Test1(); } return 0;}
在上面的代碼中我們可以通過dealloc方法來查看是否一個對象已經被回收,如果沒有被回收則有可能造成記憶體泄露。如果一個對象被釋放之後,那麼最後引用它的變數我們手動設定為nil,否則可能造成野指標錯誤,而且需要注意在ObjC中給Null 物件發送訊息是不會引起錯誤的。
野指標錯誤形式在Xcode中通常表現為:Thread 1:EXC_BAD_ACCESS(code=EXC_I386_GPFLT)錯誤。因為你訪問了一塊已經不屬於你的記憶體。
記憶體釋放的原則手動管理記憶體有時候並不容易,因為對象的引用有時候是錯綜複雜的,對象之間可能互相交叉引用,此時需要遵循一個法則:誰建立,誰釋放。
假設現在有一個人員Person類,每個Person可能會購買一輛汽車Car,通常情況下購買汽車這個活動我們可能會單獨抽取到一個方法中,同時買車的過程中我們可能會多看幾輛來最終確定理想的車,現在我們的代碼如下:
Car.h
//// Car.h// MemoryManage//// Created by Kenshin Cui on 14-2-15.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import @interface Car : NSObject#pragma mark - 屬性#pragma mark 車牌號@property (nonatomic,copy) NSString *no;#pragma mark - 公用方法#pragma mark 運行方法-(void)run;@end
Car.m
//// Car.m// MemoryManage//// Created by Kenshin Cui on 14-2-15.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import Car.h@implementation Car#pragma mark - 公用方法#pragma mark 運行方法-(void)run{ NSLog(@Car(%@) run.,self.no);}#pragma mark - 覆蓋方法#pragma mark 重寫dealloc方法-(void)dealloc{ NSLog(@Invoke Car(%@) dealloc method.,self.no); [super dealloc];}@end
Person.h
//// Person.h// MemoryManage//// Created by Kenshin Cui on 14-2-15.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import @class Car;@interface Person : NSObject{ Car *_car;}#pragma mark - 屬性#pragma mark 姓名@property (nonatomic,copy) NSString *name;#pragma mark - 公用方法#pragma mark Car屬性的set方法-(void)setCar:(Car *)car;#pragma mark Car屬性的get方法-(Car *)car;@end
Person.m
//// Person.m// MemoryManage//// Created by Kenshin Cui on 14-2-15.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import Person.h#import Car.h@implementation Person#pragma mark - 公用方法#pragma mark Car屬性的set方法-(void)setCar:(Car *)car{ if (_car!=car) { //首先判斷要賦值的變數和當前成員變數是不是同一個變數 [_car release]; //釋放之前的對象 _car=[car retain];//賦值時重新retain }}#pragma mark Car屬性的get方法-(Car *)car{ return _car;}#pragma mark - 覆蓋方法#pragma mark 重寫dealloc方法-(void)dealloc{ NSLog(@Invoke Person(%@) dealloc method.,self.name); [_car release];//在此釋放對象,即使沒有賦值過由於null 指標也不會出錯 [super dealloc];}@end
main.m
//// main.m// MemoryManage//// Created by Kenshin Cui on 14-2-15.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import #import Person.h#import Car.hvoid getCar(Person *p){ Car *car1=[[Car alloc]init]; car1.no=@888888; p.car=car1; NSLog(@retainCount(p)=%lu,[p retainCount]); Car *car2=[[Car alloc]init]; car2.no=@666666; [car1 release]; car1=nil; [car2 release]; car2=nil;}int main(int argc, const char * argv[]) { @autoreleasepool { Person *p=[[Person alloc]init]; p.name=@Kenshin; getCar(p); [p.car run]; [p release]; p=nil; } return 0;}
程式運行結果:
從運行結果來看建立的三個對象p、car1、car2都被回收了,而且[p.car run]也能順利運行,已經達到了我們的需求。但是這裡需要重點解釋一下setCar方法的實現,setCar方法中為什麼沒有寫成如下形式:
-(void)setCar:(Car *)car{ _car=car;}
前面在我們說到屬性的定義時不是都採用的這種方式嗎?
根據前面說到的記憶體釋放原則,getCar方法完全符合,在這個方法中定義的兩個對象car1、car2也都是在這個方法中釋放的,包括main函數中的p對象也是在main函數中定義和釋放的。但是如果發現調用完getCar方法之後緊接著調用了汽車的run方法,當然這在程式設計和開發過程中應該是再普通不過的設計了。如果setCar寫成“_car=car”的形式,當調用完getCar方法後,人員的car屬性被釋放了,此時調用run方法是會報錯的(大家自己可以試試)。但是如下的方式卻不會有問題:
-(void)setCar:(Car *)car{ if (_car!=car) { //首先判斷要賦值的變數和當前成員變數是不是同一個變數 [_car release]; //釋放之前的對象 _car=[car retain];//賦值時重新retain }}
因為在這個方法中我們通過[car retain]保證每次屬性賦值的時候對象引用計數器+1,這樣一來調用過getCar方法可以保證人員的car屬性不會被釋放,其次為了保證上一次的賦值對象(car1)能夠正常釋放,我們在賦新值之前對原有的值進行release操作。最後在Person的dealloc方法中對_car進行一次release操作(因為setCar中做了一次retain操作)保證_car能正常回收。
屬性參數像上面這樣編寫setCar方法的情況是比較多的,那麼如何使用@property進行自動實現呢?答案就是使用屬性參數,例如上面car屬性的setter方法,可以通過@property定義如下:
@property (nonatomic,retain) Car *car;
你會發現此刻我們不必手動實現car的getter、setter方法程式仍然沒有記憶體泄露。其實大家也應該都已經看到前面Person的name屬性定義的時候我們同樣加上了(nonatomic,copy)參數,這些參數到底是什麼意思呢?
vcGVydHlQYXJhbWV0ZXI=" border="0" height="161" src="http://www.bkjia.com/uploads/allimg/150903/0425243003-2.png" title="propertyParameter" width="642" />
@property的參數分為三類,也就是說參數最多可以有三個,中間用逗號分隔,每類參數可以從上表三類參數中人選一個。如果不進行設定或者只設定其中一類參數,程式會使用三類中的各個預設參數,預設參數:(atomic,readwrite,assign)
一般情況下如果在多線程開發中一個屬性可能會被兩個及兩個以上的線程同時訪問,此時可以考慮atomic屬性,否則建議使用nonatomic,不加鎖,效率較高;readwirte方法會產生getter、setter兩個方法,如果使用readonly則只產生getter方法;關於set方法處理需要特別說明,假設我們定義一個屬性a,這裡列出三種方式的產生代碼:
assign,用於基礎資料型別 (Elementary Data Type)
-(void)setA:(int)a{ _a=a;}
retain,通常用於非字串對象
-(void)setA:(Car *)a{ if(_a!=a){ [_a release]; _a=[a retain]; }}
copy,通常用於字串對象
-(void)setA:(NSString *)a{ if(_a!=a){ [_a release]; _a=[a copy]; }}
自動釋放池在ObjC中也有一種記憶體自動釋放的機制叫做“自動引用計數”(或“自動釋放池”),與C#、Java不同的是,這隻是一種半自動的機制,有些操作還是需要我們手動設定的。自動記憶體釋放使用@autoreleasepool關鍵字聲明一個代碼塊,如果一個對象在初始化時調用了autorelase方法,那麼當代碼塊執行完之後,在塊中調用過autorelease方法的對象都會自動調用一次release方法。這樣一來就起到了自動釋放的作用,同時對象的銷毀過程也得到了延遲(統一調用release方法)。看下面的代碼:
Person.h
//// Person.h// MemoryManage//// Created by Kenshin Cui on 14-2-15.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import @interface Person : NSObject#pragma mark - 屬性#pragma mark 姓名@property (nonatomic,copy) NSString *name;#pragma mark - 公用方法#pragma mark 帶參數的建構函式-(Person *)initWithName:(NSString *)name;#pragma mark 取得一個對象(靜態方法)+(Person *)personWithName:(NSString *)name;@end
Person.m
//// Person.m// MemoryManage//// Created by Kenshin Cui on 14-2-15.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import Person.h@implementation Person#pragma mark - 公用方法#pragma mark 帶參數的建構函式-(Person *)initWithName:(NSString *)name{ if(self=[super init]){ self.name=name; } return self;}#pragma mark 取得一個對象(靜態方法)+(Person *)personWithName:(NSString *)name{ Person *p=[[[Person alloc]initWithName:name] autorelease];//注意這裡調用了autorelease return p;}#pragma mark - 覆蓋方法#pragma mark 重寫dealloc方法-(void)dealloc{ NSLog(@Invoke Person(%@) dealloc method.,self.name); [super dealloc];}@end
main.m
//// main.m// MemoryManage//// Created by Kenshin Cui on 14-2-15.// Copyright (c) 2014年 Kenshin Cui. All rights reserved.//#import #import Person.hint main(int argc, const char * argv[]) { @autoreleasepool { Person *person1=[[Person alloc]init]; [person1 autorelease];//調用了autorelease方法後面就不需要手動調用release方法了 person1.name=@Kenshin;//由於autorelease是延遲釋放,所以這裡仍然可以使用person1 Person *person2=[[[Person alloc]initWithName:@Kaoru] autorelease];//調用了autorelease方法 Person *person3=[Person personWithName:@rosa];//內部已經調用了autorelease,所以不需要手動釋放,這也符合記憶體管理原則,因為這裡並沒有alloc所以不需要release或者autorelease Person *person4=[Person personWithName:@jack]; [person4 retain]; } /*結果: Invoke Person(rosa) dealloc method. Invoke Person(Kaoru) dealloc method. Invoke Person(Kenshin) dealloc method. */ return 0;}
當上面@autoreleaespool代碼塊執行完之後,三個對象都得到了釋放,但是person4並沒有釋放,原因很簡單,由於我們手動retain了一次,當自動釋放池釋放後調用四個對的release方法,當調用完person4的release之後它的引用計數器為1,所有它並沒有釋放(這是一個反例,會造成記憶體泄露);autorelase方法將一個對象的記憶體釋放延遲到了自動釋放池銷毀的時候,因此上面person1,調用完autorelase之後它還存在,因此給name賦值不會有任何問題;在ObjC中通常如果一個靜態方法返回一個對象本身的話,在靜態方法中我們需要調用autorelease方法,因為按照記憶體釋放原則,在外部使用時不會進行alloc操作也就不需要再調用release或者autorelase,所以這個操作需要放到靜態方法內部完成。
對於自動記憶體釋放簡單總結一下:
- autorelease方法不會改變對象的引用計數器,只是將這個對象放到自動釋放池中;
- 自動釋放池實質是當自動釋放池銷毀後調用對象的release方法,不一定就能銷毀對象(例如如果一個對象的引用計數器>1則此時就無法銷毀);
- 由於自動釋放池最後統一銷毀對象,因此如果一個操作比較佔用記憶體(對象比較多或者對象佔用資源比較多),最好不要放到自動釋放池或者考慮放到多個自動釋放池;
- ObjC中類庫中的靜態方法一般都不需要手動釋放,內部已經調用了autorelease方法; 原文作者:崔江濤(KenshinCui) 原文出處: