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 неправильно вставляет релиз в конце внутренней части цикла.