3

我想使用dispatch_async在後臺線程上放置一些複雜的計算,但我在塊中使用的對象似乎被過度釋放。我使用的是ARC,所以我認爲我不需要太在意保留和釋放,但是我錯過了一些重要的東西,或者在我的情況下放過ARC。ARC似乎過度釋放在循環中創建和分派的塊中引用的對象

問題僅當

  • 我請dispatch_async創建一個塊在for循環中
  • 出現我在塊的外部創建的塊表示一個對象
  • 環路確實至少兩次迭代(因此至少創建了兩個塊並添加到隊列中)
  • 使用RELEASE構建配置(因此它可能與某些優化有關)

這似乎並不重要

  • 無論是串行或並行隊列
  • 什麼樣的對象是使用

這個問題是不是塊在發佈配置爲發佈(如iOS 5 blocks crash only with Release Build),但塊中引用的對象被過度發佈。

我創建使用NSURL對象的一個​​小例子:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification 
{ 
    NSURL *theURL = [NSURL URLWithString:@"/Users/"]; 
    dispatch_queue_t myQueue = dispatch_queue_create("several.blocks.queue", DISPATCH_QUEUE_SERIAL); 

    dispatch_async(myQueue, ^(){ 
     NSURL *newURL = [theURL URLByAppendingPathComponent:@"test"]; 
     NSLog(@"Successfully created new url: %@ in initial block", newURL); 
    }); 

    for (int i = 0; i < 2; i++) 
    { 
     dispatch_async(myQueue, ^(){ 
      NSURL *newURL = [theURL URLByAppendingPathComponent:@"test"]; 
      NSLog(@"Successfully created new url: %@ in loop block %d", newURL, i); 
     }); 
    } 
} 

這是不是在for循環而不問題將工作的第一個塊。如果循環只有一次迭代,第二秒也是如此。在給出的例子中,它會執行兩次迭代,並且如果使用RELEASE配置運行,將會崩潰。在計劃啓用NSZombie輸出這樣的:

2013-01-07 23:33:33.331 BlocksAndARC[17185:1803] Successfully created new url: /Users/test in initial block 
2013-01-07 23:33:33.333 BlocksAndARC[17185:1803] Successfully created new url: /Users/test in loop block 0 
2013-01-07 23:33:33.333 BlocksAndARC[17185:1803] *** -[CFURL URLByAppendingPathComponent:]: message sent to deallocated instance 0x101c32790 

與調試器在for循環塊中的通話URLByAppendingPathComponent停止。

在使用併發隊列失敗的通話實際上是在調用堆棧release電話與_Block_release:

2013-01-07 23:36:13.291 BlocksAndARC[17230:5f03] *** -[CFURL release]: message sent to deallocated instance 0x10190dd30 
(lldb) bt 
* thread #6: tid = 0x3503, 0x00007fff885914ce CoreFoundation`___forwarding___ + 158, stop reason = EXC_BREAKPOINT (code=EXC_I386_BPT, subcode=0x0) 
    frame #0: 0x00007fff885914ce CoreFoundation`___forwarding___ + 158 
    frame #1: 0x00007fff885913b8 CoreFoundation`_CF_forwarding_prep_0 + 232 
    frame #2: 0x00007fff808166a3 libsystem_blocks.dylib`_Block_release + 202 
    frame #3: 0x00007fff89f330b6 libdispatch.dylib`_dispatch_client_callout + 8 
    frame #4: 0x00007fff89f38317 libdispatch.dylib`_dispatch_async_f_redirect_invoke + 117 
    frame #5: 0x00007fff89f330b6 libdispatch.dylib`_dispatch_client_callout + 8 
    frame #6: 0x00007fff89f341fa libdispatch.dylib`_dispatch_worker_thread2 + 304 
    frame #7: 0x00007fff852f0cab libsystem_c.dylib`_pthread_wqthread + 404 
    frame #8: 0x00007fff852db171 libsystem_c.dylib`start_wqthread + 13 

但這可能只是因爲稍有不同的時間。

我認爲這兩個錯誤都表明由theURL引用的NSURL對象被過度發佈。但爲什麼呢?我錯過了什麼,或者是ARC和塊組合中的錯誤?

我會希望發生的是,無論是之前dispatch_async呼叫或在dispatch_async實施(反正:裏面的for循環,每進行一次dispatch_async -call)的塊中引用的每個變量可以被保留,這是在(但在)塊的末尾發佈。

什麼實際上似乎發生的是,變量是retain編一次的代碼dispatch_asyncrelease發生時調用所以每當它執行塊的結束,這導致更多的release電話比retain電話在一個循環中。

但也許我忽略了一些東西。有更好的解釋嗎?我是否以某種方式誤用了塊或ARC?或者這是一個錯誤?

編輯:我嘗試了@Joshua Weinberg的建議,將引用變量複製到for循環中的本地變量。它工作在給定的樣本代碼,但不工作,當一個函數調用涉及:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification 
{ 
    NSObject *theObject = [[NSObject alloc] init]; 

    [self blocksInForLoopWithObject:theObject]; 
} 

-(void)blocksInForLoopWithObject:(NSObject *)theObject 
{ 
    dispatch_queue_t myQueue = dispatch_queue_create("several.blocks.queue", DISPATCH_QUEUE_SERIAL); 
    for (int i = 0; i < 2; i++) 
    { 
     NSObject *theSameObject = theObject; 
     dispatch_async(myQueue, ^(){ 
      NSString *description = [theSameObject description]; 
      NSLog(@"Successfully referenced object %@ in loop block %d", description, i); 
     }); 
    } 
} 

那麼,爲什麼它在工作的情況下,而不是在其他?我沒有看到區別。

+1

「我希望發生的是無論是在dispatch_async調用之前還是在dispatch_async調用之前(無論如何:在for循環中,對於每個dispatch_async調用一次),塊中引用的每個變量都將保留,它是在(但在)塊的末尾發佈。「更具體地說,應該發生的是'dispatch_async'的實現複製塊,並且當塊移動到堆時,它保留所引用的變量。然後,當塊被解除分配時(即,當分派完成時),它釋放其引用的變量。 – newacct

+0

我也注意到你正在泄漏'myQueue'隊列(它沒有被釋放),儘管泄漏永遠不會導致你所看到的問題。 – newacct

+1

我以前曾遇到過這個問題,但似乎它不再出現在最新的XCode(4.6)上,並帶有命令行工具的最新更新。我的clang版本似乎是「Apple LLVM 4.2版(clang-425.0.24)(基於LLVM 3.2svn)」 – user102008

回答

0

我只是能夠重現這一點,當我嘗試了。您的診斷似乎很有用,並且據我所知可能存在一些優化問題,如何將這些塊複製/保留其範圍出錯。看起來雷達值得。

只要你能做些什麼來解決這個問題。

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification 
{ 
    NSURL *theURL = [NSURL URLWithString:@"/Users/"]; 
    dispatch_queue_t myQueue = dispatch_queue_create("several.blocks.queue", DISPATCH_QUEUE_SERIAL); 

    dispatch_async(myQueue, ^(){ 
     NSURL *newURL = [theURL URLByAppendingPathComponent:@"test"]; 
     NSLog(@"Successfully created new url: %@ in initial block", newURL); 
    }); 

    for (int i = 0; i < 2; i++) 
    { 
     NSURL *localURL = theURL; 
     dispatch_async(myQueue, ^(){ 
      NSURL *newURL = [localURL URLByAppendingPathComponent:@"test"]; 
      NSLog(@"Successfully created new url: %@ in loop block %d", newURL, i); 
     }); 
    } 
} 

將它複製到堆棧會強制塊每次重新捕獲它並強制執行預期的內存語義。

+0

有趣的是,甚至不需要在塊內引用新的局部變量localURL。只需在for循環中添加賦值並仍然引用塊中舊的'theURL'即可防止過度釋放。我不知道爲什麼*那​​是*。 –

+0

我試着在原代碼中實現這個改變,但是它並沒有解決那裏的問題。所以我創建了另一個更接近我的問題代碼的例子,即使添加了局部變量也會崩潰(請參閱問題)。 –

0

爲了幫助人們試圖解決這個問題,我能夠用這種簡化的版本重現該問題在我的XCode 4.5,發佈配置:

- (id)test { 
    return [[NSObject alloc] init]; 
} 

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 

    id foo = [self test]; 
    for (int i = 0; i < 2; i++) 
    { 
    [^(){ 
     NSLog(@"%@", foo); 
    } copy]; 
    } 
    NSLog(@"%@", foo); 

    return YES; 
} 

從剖析它,它似乎ARC被錯誤地插入在循環的裏面的的末尾釋放。

+0

爲什麼「測試」方法需要使其崩潰?使用'id foo = [[NSObject alloc] init]'可以工作(如:不會崩潰)。 –

+0

@JoachimKurz:好吧,'test'返回一個非擁有的引用,而'alloc/init'返回一個擁有引用,編譯器可能會優化保留和釋放不同 – user102008