ARC, кажется, перевыпускает объекты, на которые ссылаются в блоках, которые создаются и отправляются в цикле

Я пытаюсь выполнить несколько сложных вычислений в фоновом потоке с помощью 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 (в любом случае: внутри цикла, один раз для каждого dispatch_async -call) каждая переменная, на которую есть ссылка внутри блока, сохраняется и освобождается в конце (но внутри) блока.

На самом деле кажется, что переменные retain Эд один раз для появления dispatch_async в коде, но release вызывается в конце блока, поэтому всякий раз, когда он выполняется, это приводит к более release звонки, чем retain звонки в цикле.

Но, может быть, я что-то упускаю. Есть ли лучшее объяснение? Я как-то неправильно использовал блоки или ARC или это ошибка?

РЕДАКТИРОВАТЬ: Я попробовал предложение @ Джошуа Вайнберга о копировании ссылочной переменной в локальную внутри цикла 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);
        });
    }
}

Так почему же это работает в одном случае, а не в другом? Я не вижу разницы.

2 ответа

Решение

Я просто смог воспроизвести это, когда попробовал. Ваш диагноз, кажется, точен, и, насколько я могу судить, это проблема с некоторой оптимизацией того, как блоки копируются / сохраняют свою область ошибочной. Кажется радар достойным.

Что касается того, что вы можете сделать, чтобы обойти это.

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

Копирование этого в стек вынуждает блок повторно захватывать его каждый раз и применяет вашу семантику памяти, которую вы намеревались.

Чтобы помочь людям, пытающимся устранить эту проблему, я смог воспроизвести проблему с этой упрощенной версией на своем 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 неправильно вставляет релиз в конце внутренней части цикла.

Другие вопросы по тегам