3

我正在尝试使用 dispatch_async 在后台线程上进行一些复杂的计算,但是我在块中使用的对象似乎被过度释放了。我正在使用 ARC,所以我认为我不必太在意保留和释放,但在我的情况下,要么我错过了一些重要的东西,要么 ARC 过度释放了对象。

仅在以下情况下才会出现问题

  • 我调用 dispatch_async 在 for 循环中创建一个块
  • 我在块外创建的块中引用了一个对象
  • 循环至少进行两次迭代(因此至少创建了两个块并将其添加到队列中)
  • 使用了 RELEASE 构建配置(因此它可能与一些优化有关)

这似乎无关紧要

  • 无论是串行队列还是并发队列
  • 使用什么样的对象

这个问题不是关于在 RELEASE 配置中释放的块(如在iOS 5 块中仅使用 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

URLByAppendingPathComponent调试器在 for 循环块中的调用处停止。

当使用并发队列时,失败的调用实际上是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

但这可能只是由于时间略有不同。

我认为这两个错误都表明引用的 NSURL 对象theURL被过度释放。但这是为什么呢?我错过了什么,还是 ARC 和块组合中的错误?

我期望发生的是,在dispatch_async调用之前或在执行dispatch_async(无论如何:在for循环内,每次dispatch_async调用一次)块内引用的每个变量都被保留并在结束时释放(但是在)块。

实际上似乎发生的是,变量在代码中retain出现一次,但在块的末尾被调用,所以每当它被执行时,这会导致比循环中的调用更多的调用。dispatch_asyncreleasereleaseretain

但也许我忽略了一些东西。有更好的解释吗?我是否以某种方式滥用了块或 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);
        });
    }
}

那么为什么它在一个案例中起作用,而在另一个案例中却不起作用呢?我看不出有什么区别。

4

2 回答 2

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);
        });
    }
}

将其复制到堆栈会强制块每次重新捕获它并强制执行您预期的内存语义。

于 2013-01-07T23:48:37.983 回答
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在循环内部的末尾错误地插入了一个释放。

于 2013-01-08T01:42:28.143 回答