Почему @autoreleasepool все еще нужен для ARC?

191

По большей части с ARC (Автоматический подсчет ссылок) нам вообще не нужно думать об управлении памятью с объектами Objective-C. Больше не разрешено создавать NSAutoreleasePools, однако есть новый синтаксис:

@autoreleasepool {
    
}

Мой вопрос: зачем мне это нужно, когда я не должен вручную выпускать / автоматически выпускать?


РЕДАКТИРОВАТЬ: Подводя итог, что я получил от всех ответов и кратких комментариев:

Новый синтаксис:

@autoreleasepool { … } это новый синтаксис для

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

[pool drain];

Важнее:

  • ARC использует autoreleaseтак же, как и release.
  • Для этого нужен пул автоматических выпусков.
  • ARC не создает пул автоматических релизов для вас. Тем не мение:
    • В главном потоке каждого приложения Cocoa уже есть пул авто-релизов.
  • Есть два случая, когда вы можете использовать @autoreleasepool:
    1. Когда вы находитесь во вторичном потоке и нет пула автоматического выпуска, вы должны создать свой собственный для предотвращения утечек, таких как myRunLoop(…) { @autoreleasepool { … } return success; }.
    2. Когда вы хотите создать более локальный пул, как показал @mattjgalloway в своем ответе.
Mk12
источник
1
Есть и третий случай: когда вы разрабатываете что-то, что не связано с UIKit или NSFoundation. Что-то, что использует инструменты командной строки или около того
Garnik

Ответы:

215

ARC не избавляется от удержаний, выпусков и авто-выпусков, он просто добавляет нужные вам. Таким образом, все еще есть вызовы для сохранения, есть вызовы для освобождения, все еще есть вызовы для автоматического выпуска и все еще существуют пулы автоматического выпуска.

Одна из других изменений , которые они сделали с новым компилятором Clang 3.0 и ARC является то , что они заменили NSAutoReleasePoolс @autoreleasepoolдирективой компилятора. NSAutoReleasePoolв любом случае, он всегда был чем-то вроде особого «объекта», и они сделали это так, чтобы синтаксис его использования не путался с объектом, так что он, как правило, немного проще.

В общем, вам нужно, @autoreleasepoolпотому что есть еще пулы автоматических выпусков, о которых нужно беспокоиться. Вам просто не нужно беспокоиться о добавлении в autoreleaseзвонки.

Пример использования пула авто релизов:

- (void)useALoadOfNumbers {
    for (int j = 0; j < 10000; ++j) {
        @autoreleasepool {
            for (int i = 0; i < 10000; ++i) {
                NSNumber *number = [NSNumber numberWithInt:(i+j)];
                NSLog(@"number = %p", number);
            }
        }
    }
}

Конечно, очень надуманный пример, но если бы у вас не было @autoreleasepoolвнутреннего внешнего forцикла, вы бы выпустили 100000000 объектов позже, а не 10000 каждый раз вокруг внешнего- forцикла.

Обновление: также посмотрите этот ответ - https://stackoverflow.com/a/7950636/1068248 - почему @autoreleasepoolнет ничего общего с ARC.

Обновление: я взглянул на внутреннюю часть того, что здесь происходит, и написал это в своем блоге . Если вы посмотрите туда, то увидите, что именно делает ARC и как новый стиль @autoreleasepoolи как он вводит область видимости используется компилятором для вывода информации о том, что требуется, релизы и авто-релизы требуются.

mattjgalloway
источник
11
Это не избавляет от остатков. Это добавляет их для вас. Подсчет ссылок все еще идет, он просто автоматический. Отсюда автоматический подсчет ссылок :-D.
mattjgalloway
6
Так почему же это не добавляет @autoreleasepoolмне? Если я не контролирую то, что автоматически высвобождается или освобождается (ARC делает это для меня), как я должен знать, когда настраивать пул автоматического высвобождения?
mk12
5
Но у вас есть контроль над тем, куда пойдут ваши пулы автоматического выпуска. По умолчанию оно обернуто вокруг всего вашего приложения, но вы можете захотеть большего.
mattjgalloway
5
Хороший вопрос. Вы просто должны «знать». Подумайте о том, чтобы добавить его как родственное, почему на языке GC можно добавить подсказку сборщику мусора, чтобы продолжить и запустить цикл сбора. Может быть, вы знаете, что есть тонна объектов, готовых к очистке, у вас есть цикл, который выделяет кучу временных объектов, так что вы «знаете» (или Instruments может сказать вам :), что добавление пула релизов вокруг цикла будет отличная идея.
Грэм Перкс
6
Пример зацикливания прекрасно работает без автоматического освобождения: каждый объект освобождается, когда переменная выходит из области видимости. Выполнение кода без автоматического выпуска занимает постоянный объем памяти и показывает повторное использование указателей, а установка точки останова на dealloc объекта показывает, что он вызывается один раз каждый раз в цикле, когда вызывается objc_storeStrong. Может быть, OSX делает что-то глупое, но автозапуск в iOS совершенно не нужен.
Гленн Мейнард
16

@autoreleasepoolничего не высвобождает Он создает пул автоматического выпуска, поэтому, когда будет достигнут конец блока, любые объекты, которые были автоматически освобождены ARC, когда блок был активен, будут отправлены сообщения об освобождении. Расширенное руководство по программированию управления памятью от Apple объясняет это так:

В конце блока пула автоматического выпуска объектам, получившим сообщение автоматического выпуска в блоке, отправляется сообщение об освобождении - объект получает сообщение об освобождении каждый раз, когда ему было отправлено сообщение автоматического выпуска в блоке.

outis
источник
1
Не обязательно. Объект получит releaseсообщение, но если счет сохранения> 1, объект НЕ будет освобожден.
Andybons
@andybons: обновлено; Спасибо. Это изменение от поведения до ARC?
августа
Это неверно Объекты, выпущенные ARC, будут отправлены сообщения о выпуске, как только они будут выпущены ARC, с пулом автоматического выпуска или без него.
Гленн Мейнард
7

Люди часто неправильно понимают ARC для какой-то сборки мусора или тому подобного. Правда в том, что через некоторое время люди в Apple (благодаря проектам llvm и clang) осознали, что администрирование памяти Objective-C (все и retainsи releasesт. Д.) Может быть полностью автоматизировано во время компиляции . Это просто путем чтения кода, даже до его запуска! :)

Для этого есть только одно условие: мы ДОЛЖНЫ следовать правилам , иначе компилятор не сможет автоматизировать процесс во время компиляции. Таким образом, чтобы гарантировать , что мы никогда не нарушать правила, мы не имеем права явно записи release, retainи т.д. Эти вызовы автоматически впрыскивается в наш код компилятором. Поэтому внутри у нас еще есть autoreleaseс, retain, releaseи т.д. Это просто нам не нужно писать их больше.

A ARC является автоматическим во время компиляции, что намного лучше, чем во время выполнения, как сборка мусора.

У нас все еще есть, @autoreleasepool{...}потому что наличие этого не нарушает ни одно из правил, мы можем свободно создавать / истощать наш пул в любое время, когда нам это нужно :).

nacho4d
источник
1
ARC - это GC для подсчета ссылок, а не GC типа Mark-and-Sweep, как в JavaScript и Java, но это определенно сборщик мусора. Это не отвечает на вопрос - «вы можете» не отвечает на вопрос «почему вы должны». Ты не должен.
Гленн Мейнард
3

Это потому, что вам все еще нужно предоставить компилятору подсказки о том, когда безопасным для автоматически выпущенных объектов является выход из области видимости.

DougW
источник
Можете ли вы привести пример, когда вам нужно будет это сделать?
mk12
Так, до ARC, например, у меня был CVDisplayLink, запущенный во вторичном потоке для моего приложения OpenGL, но я не создавал пул автоматического выпуска в его цикле выполнения, потому что я знал, что я ничего не делал автоматически (или использовал библиотеки, которые это делают). Означает ли это, что теперь мне нужно добавить, @autoreleasepoolпотому что я не знаю, может ли ARC решить что-то сделать автоматически?
mk12
@ Mk12 - Нет. У вас всегда будет пул автоматических выпусков, который сливается каждый раз вокруг цикла основного запуска. Вам нужно только добавить один, когда вы хотите убедиться, что объекты, которые были автоматически освобождены, будут слиты раньше, чем в противном случае - например, при следующем цикле выполнения.
mattjgalloway
2
@DougW - я посмотрел на то, что на самом деле делает компилятор, и написал об этом в блоге - iphone.galloway.me.uk/2012/02/a-look-under-arcs-hood- –-episode-3 /. Надеюсь, объясняет, что происходит как во время компиляции, так и во время выполнения.
Mattjgalloway
2

Цитируется по адресу https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html :

Автозапуск блоков и потоков

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

Если ваше приложение или поток является долгоживущим и потенциально генерирует много автоматически выпущенных объектов, вы должны использовать блоки пула автоматического выпуска (как это делают AppKit и UIKit в основном потоке); в противном случае автоматически высвобождаемые объекты накапливаются, а объем памяти увеличивается. Если ваш отдельный поток не делает звонки Какао, вам не нужно использовать блок пула автоматического выпуска.

Примечание. Если вы создаете вторичные потоки, используя API потоков POSIX вместо NSThread, вы не можете использовать Какао, если Какао не находится в режиме многопоточности. Какао входит в режим многопоточности только после отсоединения своего первого объекта NSThread. Чтобы использовать Какао во вторичных потоках POSIX, ваше приложение должно сначала отсоединить хотя бы один объект NSThread, который может немедленно завершиться. Вы можете проверить, находится ли Cocoa в режиме многопоточности, с помощью метода класса NSThread isMultiThreaded.

...

В автоматическом подсчете ссылок, или ARC, система использует ту же систему подсчета ссылок, что и MRR, но она вставляет соответствующие вызовы метода управления памятью во время компиляции. Мы настоятельно рекомендуем использовать ARC для новых проектов. Если вы используете ARC, обычно нет необходимости понимать базовую реализацию, описанную в этом документе, хотя в некоторых ситуациях это может быть полезно. Для получения дополнительной информации о ARC см. Переход к примечаниям к выпуску ARC.

Raunak
источник
2

Пулы автоматического выпуска требуются для возврата вновь созданных объектов из метода. Например, рассмотрим этот кусок кода:

- (NSString *)messageOfTheDay {
    return [[NSString alloc] initWithFormat:@"Hello %@!", self.username];
}

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

Сам метод? Невозможно, он должен вернуть созданный объект, поэтому он не должен выпускать его до возвращения.

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

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

Вот почему существуют пуски авто-релиза, поэтому первый метод фактически станет

- (NSString *)messageOfTheDay {
    NSString * res = [[NSString alloc] initWithFormat:@"Hello %@!", self.username];
    return [res autorelease];
}

Вызов autoreleaseобъекта добавляет его в пул автоматического выпуска, но что это действительно означает, добавляя объект в пул автоматического выпуска? Что ж, это значит сказать вашей системе: « Я хочу, чтобы вы выпустили этот объект для меня, но через некоторое время, а не сейчас; у него есть счет сохранения, который должен быть уравновешен выпуском, иначе память утечет, но я не могу сделать это сам прямо сейчас, так как мне нужно, чтобы объект остался живым за пределами моей текущей области видимости, и мой вызывающий объект не сделает это и для меня, он не знает, что это необходимо сделать, поэтому добавьте его в свой пул, и как только вы очистите его бассейн, также очистить мой объект для меня. "

С ARC компилятор решает за вас, когда сохранить объект, когда освободить объект и когда добавить его в пул авто-выпуска, но он все еще требует наличия пулов авто-выпуска, чтобы иметь возможность возвращать вновь созданные объекты из методов без утечки памяти. Apple только что провела несколько оптимизаций в сгенерированном коде, которые иногда устраняют пулы автоматического выпуска во время выполнения. Эти оптимизации требуют, чтобы как вызывающий, так и вызываемый использовали ARC (помните, что смешивание ARC и non-ARC допустимо и также официально поддерживается), и если это действительно так, то это можно узнать только во время выполнения.

Рассмотрим этот код ARC:

// Callee
- (SomeObject *)getSomeObject {
    return [[SomeObject alloc] init];
}

// Caller
SomeObject * obj = [self getSomeObject];
[obj doStuff];

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

// Callee
- (SomeObject *)getSomeObject {
    return [[[SomeObject alloc] init] autorelease];
}

// Caller
SomeObject * obj = [[self getSomeObject] retain];
[obj doStuff];
[obj release];

(Обратите внимание, что удержание / отпускание в вызывающей стороне является просто защитным сохранением, оно не является строго обязательным, код был бы совершенно правильным без него)

Или он может вести себя как этот код, в случае, если оба обнаруживают использование ARC во время выполнения:

// Callee
- (SomeObject *)getSomeObject {
    return [[SomeObject alloc] init];
}

// Caller
SomeObject * obj = [self getSomeObject];
[obj doStuff];
[obj release];

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

Теперь к актуальному вопросу: зачем использовать @autoreleasepool?

Для большинства разработчиков сегодня есть только одна причина использовать эту конструкцию в своем коде, а именно, уменьшить объем памяти, где это применимо. Например, рассмотрим этот цикл:

for (int i = 0; i < 1000000; i++) {
    // ... code ...
    TempObject * to = [TempObject tempObjectForData:...];
    // ... do something with to ...
}

Предположим, что каждый вызов tempObjectForDataможет создать новый, TempObjectкоторый возвращается авто-релиз. Цикл for создаст миллион таких временных объектов, которые все собраны в текущем автозапуске, и только после уничтожения этого пула все временные объекты также будут уничтожены. Пока это не произойдет, у вас есть миллион таких временных объектов в памяти.

Если вы вместо этого напишите код:

for (int i = 0; i < 1000000; i++) @autoreleasepool {
    // ... code ...
    TempObject * to = [TempObject tempObjectForData:...];
    // ... do something with to ...
}

Затем каждый раз при запуске цикла for создается новый пул, который уничтожается в конце каждой итерации цикла. Таким образом, в любой момент времени в памяти висит не более одного временного объекта, несмотря на то, что цикл выполняется миллион раз.

В прошлом вам часто приходилось самостоятельно управлять автозапусками при управлении потоками (например, с использованием NSThread), поскольку только основной поток автоматически имеет пул автозапуска для приложения Cocoa / UIKit. Однако сегодня это в значительной степени наследие, поскольку сегодня вы, вероятно, не будете использовать потоки для начала. Вы бы использовали GCD DispatchQueueили NSOperationQueues, и оба они управляют пулом автоматического выпуска верхнего уровня для вас, созданным перед запуском блока / задачи и уничтоженным, как только закончите с ним.

Mecki
источник
-4

Похоже, в этой теме много путаницы (и, по крайней мере, 80 человек, которые, вероятно, сейчас смущены этим и думают, что им нужно посыпать @autoreleasepool вокруг своего кода).

Если проект (включая его зависимости) использует исключительно ARC, тогда @autoreleasepool не нужно использовать и ничего полезного не будет делать. ARC будет обрабатывать освобождение объектов в нужное время. Например:

@interface Testing: NSObject
+ (void) test;
@end

@implementation Testing
- (void) dealloc { NSLog(@"dealloc"); }

+ (void) test
{
    while(true) NSLog(@"p = %p", [Testing new]);
}
@end

дисплеи:

p = 0x17696f80
dealloc
p = 0x17570a90
dealloc

Каждый объект тестирования освобождается, как только значение выходит из области видимости, не дожидаясь выхода из пула автоматического выпуска. (То же самое происходит с примером NSNumber; это просто позволяет нам наблюдать за разлочкой.) ARC не использует autorelease.

Причина, по которой @autoreleasepool по-прежнему разрешена, заключается в смешанных проектах ARC и не-ARC, которые еще не полностью перешли на ARC.

Если вы обращаетесь к не-ARC-коду, он может вернуть объект с автоматическим освобождением. В этом случае вышеприведенный цикл будет протекать, поскольку текущий пул автоматического выпуска никогда не будет завершен. Вот где вы хотели бы поместить @autoreleasepool вокруг блока кода.

Но если вы полностью перешли на ARC, забудьте об автозапуске.

Гленн Мейнард
источник
4
Этот ответ неверен и также противоречит документации ARC. ваше свидетельство анекдотично, потому что вы используете метод выделения, который компилятор решает не делать авто-выпуском. Вы можете легко увидеть, что это не работает, если вы создадите новый статический инициализатор для своего пользовательского класса. Создать этот инициализатор и использовать его в цикле: + (Testing *) testing { return [Testing new] }. Тогда вы поймете, что Deloc не будет вызван до позже. Это исправлено, если вы оберните цикл внутри @autoreleasepoolблока.
Дима
@Dima Пробовал на iOS10, dealloc вызывали сразу после печати адреса объекта. + (Testing *) testing { return [Testing new];} + (void) test { while(true) NSLog(@"p = %p", [self testing]);}
KudoCC
@KudoCC - Я тоже, и я видел то же поведение, что и вы. Но когда я бросил [UIImage imageWithData]в уравнение, то внезапно я начал видеть традиционное autoreleaseповедение, требующее @autoreleasepoolподдержания пика памяти на некотором разумном уровне.
Роб
@Rob Я не могу помочь себе добавить ссылку .
KudoCC