iPhone/Mac Objective-C 內存管理教程和原理剖析
前言
初學 objectice-C 的朋友都有一個困惑,總覺得對 objective-C 的內存管理 機制琢磨不透,程序經常內存洩漏或莫名其妙的崩潰。我在這裡總結了自己對 o bjective-C 內存管理機制的研究成果和經驗,寫了這麼一個由淺入深的教程。 希望對大家有所幫助,也歡迎大家一起探討。
此文涉及的內存管理是針對於繼承於 NSObject 的 Class。
一 基本原理
Objective-C 的內存管理機制與.Net/Java 那種全自動的垃圾回收機制是不 同的,它本質上還是 C 語言中的手動管理方式,只不過稍微加了一些自動方法。
1 Objective-C 的對象生成於堆之上,生成之後,需要一個指針來指向它。
ClassA *obj1 = [[ClassA alloc] init];
2 Objective-C 的對象在使用完成之後不會自動銷毀,需要執行 dealloc 來 釋放空間(銷毀),否則內存洩露。
[obj1 dealloc];
這帶來了一個問題。下面代碼中 obj2 是否需要調用 dealloc? ClassA *obj1 = [[ClassA alloc] init];
ClassA *obj2 = obj1;
[obj1 hello]; //輸出 hello
[obj1 dealloc];
[obj2 hello]; //能夠執行這一行和下一行嗎?
[obj2 dealloc];
不能,因為 obj1 和 obj2 只是指針,它們指向同一個對象,[obj1 dealloc] 已經銷毀這個對象了,不能再調用[obj2 hello]和[obj2 dealloc]。obj2 實際 上是個無效指針。
如何避免無效指針?請看下一條。
3 Objective-C 采用了引用計數(ref count 或者 retain count)。對象的內 部保存一個數字,表示被引用的次數。例如,某個對象被兩個指針所指向(引用) 那麼它的 retain count 為 2。需要銷毀對象的時候,不直接調用 dealloc,而是 調用 release。release 會讓 retain count 減 1,只有 retain count 等於 0,系 統才會調用 dealloc 真正銷毀這個對象。
ClassA *obj1 = [[ClassA alloc] init]; //對象生成時,retain count =1
[obj1 release]; //release 使 retain count 減 1,retain count = 0,d ealloc 自動被調用,對象被銷毀
我們回頭看看剛剛那個無效指針的問題,把 dealloc 改成 release 解決了嗎? ClassA *obj1 = [[ClassA alloc] init]; //retain count = 1
ClassA *obj2 = obj1; //retain count = 1
[obj1 hello]; //輸出 hello
[obj1 release]; //retain count = 0,對象被銷毀 [obj2 hello];
[obj2 release];
[obj1 release]之後,obj2 依然是個無效指針。問題依然沒有解決。解決 方法見下一條。
4 Objective-C 指針賦值時,retain count 不會自動增加,需要手動 retai n。
ClassA *obj1 = [[ClassA alloc] init]; //retain count = 1 ClassA *obj2 = obj1; //retain count = 1
[obj2 retain]; //retain count = 2
[obj1 hello]; //輸出 hello
[obj1 release]; //retain count = 2 ? 1 = 1 [obj2 hello]; //輸出 hello
[obj2 release]; //retain count = 0,對象被銷毀
問題解決!注意,如果沒有調用[obj2 release],這個對象的 retain count 始終為 1,不會被銷毀,內存洩露。(1-4 可以參考附件中的示例程序 memman-no -pool.m)
這樣的確不會內存洩露,但似乎有點麻煩,有沒有簡單點的方法?見下一條。
5 Objective-C 中引入了 autorelease pool(自動釋放對象池),在遵守一些 規則的情況下,可以自動釋放對象。(autorelease pool 依然不是.Net/Java 那 種全自動的垃圾回收機制)
5.1 新生成的對象,只要調用 autorelease 就行了,無需再調用 release!
ClassA *obj1 = [[[ClassA alloc] init] autorelease]; //retain coun t = 1 但無需調用 release
5.2 對於存在指針賦值的情況,代碼與前面類似。
ClassA *obj1 = [[[ClassA alloc] init] autorelease]; //retain coun t= 1
ClassA *obj2 = obj1; //retain count = 1 [obj2 retain]; //retain count = 2
[obj1 hello]; //輸出 hello
//對於 obj1,無需調用(實際上不能調用)release [obj2 hello]; //輸出 hello
[obj2 release]; //retain count = 2-1 = 1
細心的讀者肯定能發現這個對象沒有被銷毀,何時銷毀呢?誰去銷毀它?(可 以參考附件中的示例程序 memman-with-pool.m)請看下一條。
6 autorelease pool 原理剖析。(其實很簡單的,一定要堅持看下去,否則 還是不能理解 Objective-C 的內存管理機制。)
6.1 autorelease pool 不是天生的,需要手動創立。只不過在新建一個 ip hone 項目時,xcode 會自動幫你寫好。autorelease pool 的真名是 NSAutorele asePool。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
6.2 NSAutoreleasePool 內部包含一個數組(NSMutableArray),用來保存聲 明為 autorelease 的所有對象。如果一個對象聲明為 autorelease,系統所做的 工作就是把這個對象加入到這個數組中去。
ClassA *obj1 = [[[ClassA alloc] init] autorelease]; //retain coun t = 1,把此對象加入 autorelease pool 中
6.3 NSAutoreleasePool 自身在銷毀的時候,會遍歷一遍這個數組,releas e 數組中的每個成員。如果此時數組中成員的 retain count 為 1,那麼 release 之後,retain count 為 0,對象正式被銷毀。如果此時數組中成員的 retain co unt 大於 1,那麼 release 之後,retain count 大於 0,此對象依然沒有被銷毀, 內存洩露。
6.4 默認只有一個 autorelease pool,通常類似於下面這個例子。 int main (int argc, const char *argv[])
{
NSAutoreleasePool *pool;
pool = [[NSAutoreleasePool alloc] init];
// do something
[pool release];
return (0);
} // main
所有標記為 autorelease 的對象都只有在這個 pool 銷毀時才被銷毀。如果 你有大量的對象標記為 autorelease,這顯然不能很好的利用內存,在 iphone 這種內存受限的程序中是很容易造成內存不足的。例如:
int main (int argc, const char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
int i, j;
for (i = 0; i < 100; i )
{
for (j = 0; j < 100000; j )
[NSString stringWithFormat:@"1234567890"];//產生的對象是 autorele ase 的。
}
[pool release];
return (0);
} // main
(可以參考附件中的示例程序 memman-many-objs-one-pool.m,運行時通過 監控工具可以發現使用的內存在急劇增加,直到 pool 銷毀時才被釋放)你需要考 慮下一條。
7 Objective-C 程序中可以嵌套創建多個 autorelease pool。在需要大量創 建局部變量的時候,可以創建內嵌的 autorelease pool 來及時釋放內存。(感謝 網友 hhyytt 和 neogui 的提醒,某些情況下,系統會自動創建 autorelease poo l, 請參見第四章)
int main (int argc, const char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
int i, j;
for (i = 0; i < 100; i )
{
NSAutoreleasePool *loopPool = [[NSAutoreleasePool alloc] init];
for (j = 0; j < 100000; j )
[NSString stringWithFormat:@"1234567890"];//產生的對象是 autorele ase 的。
[loopPool release];
}
[pool release];
return (0);
} // main
(可以參考附件中的示例程序 memman-many-objs-many-pools.m,占用內存 的變化極小)
二 口訣與范式
1 口訣。
1.1 誰創建,誰釋放(類似於“誰污染,誰治理”)。如果你通過 alloc、ne w 或 copy 來創建一個對象,那麼你必須調用 release 或 autorelease。換句話說, 不是你創建的,就不用你去釋放。
例如,你在一個函數中 alloc 生成了一個對象,且這個對象只在這個函數中 被使用,那麼你必須在這個函數中調用 release 或 autorelease。如果你在一個 class 的某個方法中 alloc 一個成員對象,且沒有調用 autorelease,那麼你需 要在這個類的 dealloc 方法中調用 release;如果調用了 autorelease,那麼在 d ealloc 方法中什麼都不需要做。
1.2 除了 alloc、new 或 copy 之外的方法創建的對象都被聲明了 autorelea se。
1.3 誰 retain,誰 release。只要你調用了 retain,無論這個對象是如何 生成的,你都要調用 release。有時候你的代碼中明明沒有 retain,可是系統會 在默認實現中加入 retain。不知道為什麼蘋果公司的文檔沒有強調這個非常重 要的一點,請參考范式 2.7 和第三章。
2 范式。
范式就是模板,就是依葫蘆畫瓢。由於不同人有不同的理解和習慣,我總結 的范式不一定適合所有人,但我能保證照著這樣做不會出問題。
2.1 創建一個對象。
ClassA *obj1 = [[ClassA alloc] init];
2.2 創建一個 autorelease 的對象。
ClassA *obj1 = [[[ClassA alloc] init] autorelease];
2.3 Release 一個對象後,立即把指針清空。(順便說一句,release 一個空 指針是合法的,但不會發生任何事情)
[obj1 release];
obj1 = nil;
2.4 指針賦值給另一個指針。 ClassA *obj2 = obj1;
[obj2 retain];
//do something
[obj2 release];
obj2 = nil;
2.5 在一個函數中創建並返回對象,需要把這個對象設置為 autorelease ClassA *Func1()
{
ClassA *obj = [[[ClassA alloc]init]autorelease];
return obj;
}
2.6 在子類的 dealloc 方法中調用基類的 dealloc 方法
-(void) dealloc
{
[super dealloc];
}
2.7 在一個 class 中創建和使用 property。
2.7.1 聲明一個成員變量。
ClassB *objB;
2.7.2 聲明 property,加上 retain 參數。
@property (retain) ClassB* objB;
2.7.3 定義 property。(property 的默認實現請看第三章)
@synthesize objB;
2.7.4 除了 dealloc 方法以外,始終用.操作符的方式來調用 property。 self.objB 或者 objA.objB
2.7.5 在 dealloc 方法中 release 這個成員變量。
[objB release];
示例代碼如下(詳細代碼請參考附件中的 memman-property.m,你需要特別 留意對象是在何時被銷毀的。):
@interface ClassA : NSObject
{
ClassB* objB;
}
@property (retain) ClassB* objB;
@end
@implementation ClassA
@synthesize objB;
-(void) dealloc
{
[objB release];
[super dealloc];
}
@end
2.7.6 給這個property賦值時,有手動release和autorelease兩種方式。 void funcNoAutorelease()
{
ClassB *objB1 = [[ClassB alloc]init];
ClassA *objA = [[ClassA alloc]init];
objA.objB = objB1;
[objB1 release];
[objA release];
}
void funcAutorelease()
{
ClassB *objB1 = [[[ClassB alloc]init] autorelease];
ClassA *objA = [[[ClassA alloc]init] autorelease];
objA.objB = objB1; }
三 @property (retain)和@synthesize 的默認實現
在這裡解釋一下@property (retain) ClassB* objB;和@synthesize objB; 背後到底發生了什麼(retain property 的默認實現)。property 實際上是 gette r 和 setter,針對有 retain 參數的 property,背後的實現如下(請參考附件中 的 memman-getter-setter.m,你會發現,結果和 memman-property.m 一樣):
@interface ClassA : NSObject
{
ClassB *objB;
}
-(ClassB *) getObjB;
-(void) setObjB:(ClassB *) value;
@end
@implementation ClassA
-(ClassB*) getObjB
{
return objB;
}
-(void) setObjB:(ClassB*) value
{
if (objB != value)
{
[objB release];
objB = [value retain];
}
}
在 setObjB 中,如果新設定的值和原值不同的話,必須要把原值對象 relea se 一次,這樣才能保證 retain count 是正確的。
由於我們在 class 內部 retain 了一次(雖然是默認實現的),所以我們要在 dealloc 方法中 release 這個成員變量。
-(void) dealloc
{
[objB release];
[super dealloc];
}
四 系統自動創建新的 autorelease pool
在生成新的 Run Loop 的時候,系統會自動創建新的 autorelease pool(非 常感謝網友 hhyytt 和 neogui 的提醒)。注意,此處不同於 xcode 在新建項目時 自動生成的代碼中加入的 autorelease pool,xcode 生成的代碼可以被刪除,但 系統自動創建的新的autorelease pool是無法刪除的(對於無Garbage Collect ion 的環境來說)。Objective-C 沒有給出實現代碼,官方文檔也沒有說明,但我 們可以通過小程序來證明。
在這個小程序中,我們先生成了一個 autorelease pool,然後生成一個 au torelease 的 ClassA 的實例,再在一個新的 run loop 中生成一個 autorelease 的 ClassB 的對象(注意,我們並沒有手動在新 run loop 中生成 autorelease po ol)。精簡的示例代碼如下,詳細代碼請見附件中的 memman-run-loop-with-poo l.m。
int main(int argc, char**argv)
{
NSLog(@"create an autorelasePool");
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSLog(@"create an instance of ClassA and autorelease ");
ClassA *obj1 = [[[ClassA alloc] init] autorelease];
NSDate *now = [[NSDate alloc] init];
NSTimer *timer = [[NSTimer alloc] initWithFireDate:now
interval:0.0
target:obj1
selector:@selector(createClassB)
userInfo:nil
repeats:NO];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addTimer:timer forMode:NSDefaultRunLoopMode];
[timer release];
[now release];
[runLoop run]; //在新 loop 中調用一函數,生成 ClassB 的 autorelease 實例
NSLog(@"releasing autorelasePool ");
[pool release];
NSLog(@"autorelasePool is released ");
return 0;
}
輸出如下:
create an autorelasePool
create an instance of ClassA and autorelease
create an instance of ClassB and autorelease
ClassB destroyed
releasing autorelasePool
ClassA destroyed
autorelasePool is released
注意在我們銷毀 autorelease pool 之前,ClassB 的 autorelease