有這么一道iOS面試題 以下代碼有沒有什么問題?如果有?如何解決?
for (int i = 0; i < largeNumber; i++) { NSString *str = [NSString stringWithFormat:@"hello -%04d", i]; str = [str stringByAppendingString:@" - world"]; }
局部釋放池和RunLoop釋放池的概念:
主線程的RunLoop是默認開啟的(視圖用[[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]]來停止它,也是做不到的), 每一次消息循環開始的時候會先創建自動釋放池,這次循環結束前,會釋放自動釋放池,然后RunLoop等待下次事件源。 在這個過程中,由RunLoop創建的釋放池類似於一個全局的釋放池。但是開發者可以任何執行的地方創建釋放池,也就是局部的釋放池,這時的釋放池類似於代碼塊 當釋放池結束的時候會自動釋放。因此一般情況下,局部的自動釋放池很快就被釋放了,而RunLoop釋放池會等一次消息循環結束的時候釋放。
什么樣的對象會交給釋放池管理:
返回當前類的實例的類方法創建出來的對象,都是autorelease的,會交給所在的釋放池進行管理。 例如創建一個Person類,使用[[self alloc]init]方法創建的對象的管理不會交給它所在的釋放池,而是根據引用計數來控制釋放的時機, 如果使用[[[self alloc]init] autorelease]創建的對象,會交給所在的釋放池管理,控制其釋放的時機。
- (void)test{ @autoreleasepool { Person *p = [[Person alloc] init]; p = nil; NSLog(@"---"); } NSLog(@"autorelease結束"); }
執行結果:
Person---dealloc --- autorelease結束
- (void)test1{ @autoreleasepool { Person *p = [Person person]; // 內部是[[[self alloc] init] autorelease] p = nil; NSLog(@"---"); } NSLog(@"autorelease結束"); }
執行的結果為:
--- Person---dealloc autorelease結束
因此自動釋放池被銷毀或耗盡時會向池中所有使用autorelease創建的對象發送release 消息,釋放所有autorelease的對象,而不是所有的對象。
回到面試的問題:
當我們使用for循環創建很多個使用autorelease方式創建的NSString對象的時候,將所有的對象的釋放權都交給了RunLoop 的釋放池,而RunLoop的釋放池會等待這個事件處理之后才會釋放,因此就會使對象無法及時釋放,堆積在內存造成內存泄露,可以在Debug Navigation 中觀察到內存激增。為了驗證確實是因為autorelease這種創建方式引起的內存泄露,我做了如下的測試:
int largeNumber = 100000000; - (void)correctSolution1{ for (int i = 0; i < largeNumber; i++) { NSString *str = [[NSString alloc] initWithFormat:@"hello -%04d", i]; str = [str stringByAppendingString:@" - world"]; } } // stringWithFormat:本質上是調用了alloc + initWithFormat: + autorelease // 我在本例中將stringWithFormat:方法換成了alloc + initWithFormat: // 這樣做問題就解決了:內存幾乎沒有變化。反向驗證了內存飆升確實是autorelease創建方式造成的。
但是在編寫代碼的時候我們仍然習慣用類的快速創建方法,而不是alloc+init。也就是說,為了方便寫程序,又使用了底層實現是alloc+init+autorelease的快速創建對象的方法(如 stringWithFormat:)。因此解決的方案就是添加局部的釋放池,以及時釋放內存,如果將局部釋放池添加到循環外:
- (void)wrongSolution{ @autoreleasepool { for (int i = 0; i < largeNumber; i++) { NSString *str = [NSString stringWithFormat:@"hello -%04d", i]; str = [str stringByAppendingString:@" - world"]; } } }
這樣顯然是沒有效果的,釋放池需要等循環執行之后再釋放內存,這和使用RunLoop創建的釋放池沒有什么區別。 較好的方案就是每次循環的時候添加一個釋放池:
- (void)correctSolution2{ for (int i = 0; i < largeNumber; i++) { @autoreleasepool { NSString *str = [NSString stringWithFormat:@"hello -%04d", i]; str = [str stringByAppendingString:@" - world"]; } } }
這樣每一次循環的結束時都會釋放一次內存,因而這個循環全部執行完成時也幾乎不消耗內存。
總結是:
做多線程開發時,需要在線程調度方法中手動添加自動釋放池,尤其是當執行循環的時候,如果循環內部有使用類的快速創建方法創建的對象, 一定要將循環體放到自動釋放池中。