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