有時候我們需要延遲一個對象的引用計數減一操作,比如:
[cpp]
+ (NSArray *)array
{
return [[NSArray alloc] init] autorelease];
}
由於方法名並不以alloc, new, copy, mutableCopy開頭,並且方法內部使用了alloc,需要對因此產生的引用計數負責。
不過如果直接調用release,將會返回野指針,所以我們需要autorelease機制來延遲釋放。
我們需要先創建一個autorelease pool,才能有效地實現autorelease機制,否則會導致內存洩露。
當向一個對象obj發送autorelease消息時,會發生如下過程:
獲取當前autorelease pool,即離事件發生處最近的(inner-most)自動釋放池,NSAutoreleasePool實例。
NSAutoreleasePool實例將obj添加到內部維護的數組中,記錄起來。
當發送drain消息給自動釋放池時,它會遍歷內部維護的數組,向每個元素發送一個release消息。
除了我們顯式創建的NSAutoreleasePool實例,系統會默認為我們在主線程創建一個:
[cpp]
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
從可見代碼來看,這個自動釋放池建立在main函數中,對於我們編碼過程中創建的自動釋放對象有什麼幫助呢?
等到結束UIApplicationMain才釋放,跟不釋放基本沒差別,因為程序運行即將結束。
所以,如果事實真相如此,那麼是不合理的。
蘋果官方文檔有這麼一段話:
The Application Kit creates an autorelease pool on the main threadat the beginning of every cycle of the event loop, anddrains it at the end, thereby releasing any autoreleased objects generated while processing an event. If you use the Application Kit, you therefore typically don’t have to create your own pools. If your application creates a lot of temporary autoreleased objects within the event loop, however, it may be beneficial to create “local” autorelease pools to help to minimize the peak memory footprint.
可見,在主線程中每個event loop開始的時候會創建一個autorelease pool,在循環結束時清空自動釋放池。
當然,如果在某個局部地方創建很多臨時對象,最好也顯式創建autorelease pool降低內存峰值。
用代碼來求證:
[cpp]
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
TestObject *testObject = [[[TestObject alloc] init] autorelease];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(@"Touches Ended\n");
[NSAutoreleasePool showPools];
}
通過在TestObject的release方法中輸出日志信息,可以看到testObject是在touchesEnded事件前被釋放的,即event loop結束前。
這是對主線程而言,那麼對其它線程呢?
下面又有一段說明:
Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects (see “Threads”). As new pools are created, they get added to the top of the stack. When pools are deallocated, they are removed from the stack. Autoreleased objects are placed into the top autorelease pool for the current thread. When a thread terminates, it automatically drains all of the autorelease pools associated with itself.
從上面這段話可以得知每個線程都維護著與自己對應的NSAutoreleasePool對象,將其放在線程棧的棧頂。當線程結束時,會清空自動釋放池。
同樣地可以用代碼來說明:
[cpp]
[NSThread detachNewThreadSelector:@selector(onNewThread:) toTarget:self withObject:nil];
- (void)onNewThread:(id)info
{
TestObject *testObject = [[[TestObjectalloc] init] autorelease];
}
通過輸出結果也可以得知線程結束時,testObject得到釋放。
不過,緊接著的一段說明說明了些例外情況:
If you are making Cocoa calls outside of the Application Kit’s main thread—for example if you create a Foundation-only applicationor if youdetach a thread—you need to create your own autorelease pool.
不過當我detach了多個線程出來,發現每個線程仍然有維護autorelease pool。是不是上面這段話我哪裡沒理解對?幸好在沒有autorelease pool存在時,我們向對象發送autorelease消息會有警告輸出。
最後,探討下NSRunLoop和autorelease的關系。
當我們分離了個線程,並且讓runloop跑起來,我們可以發現與主線程類似:每個runloop循環結束時會drain下autorelease pool。
通過觀察runloop狀態可以驗證:
[cpp]
#pragma mark - RunLoop Observer
- (void)onNewThread:(id)info
{
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
if (!runloop) {
return ;
}
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:5.0f target:self selector:@selector(onTimerFired:) userInfo:nil repeats:NO];
[runloop addTimer:timer forMode:NSRunLoopCommonModes];
CFRunLoopObserverContext context = {
0,
self,
NULL,
NULL,
NULL
};
CFRunLoopObserverRef observerRef = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runloopObserverCallback, &context);
CFRunLoopAddObserver([runloop getCFRunLoop], observerRef, kCFRunLoopCommonModes);
[runloop run];
CFRunLoopRemoveObserver([runloop getCFRunLoop], observerRef, kCFRunLoopCommonModes);
CFRelease(observerRef);
}
static void runloopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
CFRunLoopActivity currentActivity = activity;
switch (currentActivity) {
casekCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry \n");
break;
casekCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers \n");
break;
casekCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources \n");
break;
casekCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting \n");
break;
casekCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting \n");
break;
casekCFRunLoopExit:
NSLog(@"kCFRunLoopExit \n");
break;
default:
NSLog(@"Activity not recognized!\n");
break;
}
}
--------------------------------------------------
Jason Lee @ Hangzhou
2012.08.15