Что нужно знать об ARC

Автоматический подсчет ссылок (Automatic Reference Counting, ARC) для языка Objective-C был представлен компанией Apple еще в 2011 году для iOS 4.0 и выше, Mac OS X 10.6 и выше, с использованием xCode 4.2 и выше. И, хотя всё больше библиотек, фреймворков и проектов сегодня используют ARC, до сих пор среди программистов встречается либо неприятие этой технологии, либо eё неполное понимание. Опытные программисты, привыкшие к retain/release, иногда считают, что они лучше и эффективней справяться с подсчетом ссылок, чем это за них сделает компилятор, а новички, начищающие сразу использовать ARC, полагают, что им не надо вообще думать об управлении памятью, и всё сделается магическим образом само.

ARC — не сборщик мусора


В отличае от сборщика мусора, ARC не занимается автоматическим освобождением памяти от отработанных объектов и не запускает никаких фоновых процессов. Всё что он делает — это при сборке приложения анализирует и расставляет retain/release в компилируемый код за программиста.
Вы не можете напрямую вызвать retain, release, retainCount, autorelease.
Можно переопределить dealloc, но вызывать [super dealloc] не надо, за вас это сделает ARC, а так же освободит при этом все property и instance variables, которые в этом нуждаются. Переопределять dealloc бывает необходимо для того, чтобы, например, отписаться от нотификаций, удалить объект из иных служб и менеджеров, на которые он подписан, инвалидировать таймеры, а так же чтобы освободить не Objective-C объекты.
Код без ARC:

- (void)dealloc
{
	[_someIvar release];
	[_anotherIvar release];
	[[NSNotificationCenter defaultCenter] removeObserver:self];
        [super dealloc];
} 

Код с ARC:

- (void)dealloc
{
	[[NSNotificationCenter defaultCenter] removeObserver:self];
} 

Сильные и слабые ссылки


Вместо атрибутов retain и assign для properties в ARC используются атрибуты strong и weak (__strong и __weak для локальных переменных и ivars). Это не совсем их аналоги, но об этом позже. Атрибут strong используется по умолчанию, так что указывать его явно не обязательно.

@property  AnyClass *strongReference; //(atomic, strong) по умолчанию
@property (nonatomic, weak) id delegate;

Properties, переменные которых ссылаются на объекты «вверх» по иерархии и делегаты должны быть слабыми, чтобы избежать циклических ссылок.

Поскольку локальные переменные по умолчанию сильные (__strong), возможны такие конструкции в коде:
NSDate *originalDate = self.lastModificationDate; 
self.lastModificationDate = [NSDate date];
NSLog(@"Last modification date changed from %@ to %@", originalDate, self.lastModificationDate);

Объект, сохраненный в переменной originalDate будет жив до тех пор, пока ARC не найдет последнюю строку, где используется эта переменная, а затем тут же ее освободит (подставит release).

Для создания слабой ссылки используется атрибут __weak:
NSDate * __weak originalDate = self.lastModificationDate;
self.lastModificationDate = [NSDate date];

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

Важная и полезная особенность ARC: сразу после деаллокации слабые ссылки обнуляются, то есть становятся равными nil.

А поскольку в Objective-C nil может принимать любые сообщения, проблемы с EXC_BAD_ACCESS уходят в прошлое. Это и есть отличие от атрибутов retain/assign. Подобное происходит и при объявлении объектов: они неявно инициализируются nil'ом. Полезным приёмом является кэширование слабых property в сильных локальных переменных, чтобы быть уверенным, что объект будет жив необходимое время:

- (void)someMethod {
    NSObject *cachedObject = self.weakProperty;
    [cachedObject doSomething];
    [cachedObject doSomethingElse];
}

По этой же причине при проверке слабых property на nil, например, делегатов, нужно их кэшировать:
id cachedObject = self.delegate;
if (cachedObject) {
	[self.delegate doSomeTask];
}
cachedObject = nil;

Присваивание кэшированному объекту nil убирает сильную ссылку, и если на него больше нет других сильных ссылок, объект будет деаллоцирован.

В редких случаях нужно, чтобы ссылки на объект после его уничтожения не обнулялись. Для этого существует атрибут unsafe_unretained:
@property (unsafe_unretained) NSObject *someProperty;	
NSObject *__unsafe_unretained someReference;


Autorelease


Объекты, созданные с помощью статических методов (например: [NSData data...], [NSArray array...] и т. д.) и литералов (@«string», @42, @[], @{} ) больше не авторелизные. Время жизни таких обьектов задается только сильными ссылками на них.
Существует атрибут __autoreleasing, который, согласно документации, рекомендуется использовать для двойных указателей (* id) в случае, если необходимо передать результат в параметр.

NSError *__autoreleasing error;
BOOL ok = [database save:&error];
if (!ok) {
	//обрабатываем ошибку
}

Tогда метод save должен иметь следующую сигнатуру:
- (BOOL)save:(NSError * __autoreleasing *) error;

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

NSError * error;
NSError *__autoreleasing tmp;
BOOL ok = [database save:&tmp];
error = tmp;


NSAutoreleasePool теперь недоступен для прямого использования. Вместо него предлагается использовать директиву @autoreleasepool {}. При входе в такой блок состояние пула сохраняется, при выходе восстанавливается, освобождая все объекты, созданные внутри. Рекомендуется использовать @autoreleasepool в циклах, если создается большое количество временных обьектов.

for (id object in hugeArray) {
	@autoreleasepool {
		//использование временных объектов
	}
} 


Блоки


Блоки по-прежднему необходимо копировать.

//property
@property (nonatomic, copy) SomeBlockType someBlock;
	
//локальная переменная
someBlockType someBlock = ^{NSLog(@"hi");}; 
[someArray addObject:[someBlock copy]];

О циклических ссылках компилятор предупредит:

warning: capturing 'self' strongly in this block is likely to lead to a retain cycle
[-Warc-retain-cycles,4]

SomeBlockType someBlock = ^{
    	[self someMethod];
};

Когда блок будет скопирован, он также захватит self, если в нем используются instance varisbles.

Чтобы избежать таких случаев, нужно создать слабую ссылку на self:
__weak SomeObjectClass *weakSelf = self;
SomeBlockType someBlock = ^{
    SomeObjectClass *strongSelf = weakSelf;
    if (strongSelf) {
        [strongSelf someMethod];
    }
};

Необходимо заботиться о любых объектах в блоках, объявленных снаружи, используя атрибут __weak.

Строим мосты


Как же быть с объектами CoreFondation? Для них ручной подсчет ссылок никто не отменял. Прямой каст теперь не работает, для этого есть несколько специальных ключевых слов.

id my_id;
CFStringRef my_cfref;
NSString   *a = (__bridge NSString*)my_cfref; 
CFStringRef b = (__bridge CFStringRef)my_id; 
NSString   *c = (__bridge_transfer NSString*)my_cfref; // -1 к количеству ссылок CFRef
CFStringRef d = (__bridge_retained CFStringRef)my_id;  // вернет CFRef +1


  • __bridge Оставит количество ссылок на старые объекты неизменным. CF-объект надо будет освободить вручную.
  • __bridge_transfer нужен для смены типа объекта с CF на Objective-C. ARC декрементирует счетчик ссылок CF, так что убедитесь, что он больше нуля.
  • __bridge_retained нужен для смены типа объекта с Objective-C на CF. Он вернет CF-объект c счетчиком ссылок +1. Не забудьте освободить объект вызовом CFRelease().


Заключение


Используйте ARC. Это проще, безопаснее и сэкономит вам время и нервы.

Ссылки

Раздел документации Clang об ARC
Статья о быстродействии ARC
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 31
  • НЛО прилетело и опубликовало эту надпись здесь
    • +6
      Я думаю, что самый большой вред ARC — это поколение новых программистов, которые сразу стали использовать ARC, и от того плохо понимают модель управления памятью Objective-C в целом.
      • –3
        У меня любимый вопрос на собеседовании — написать мутатор и аксессор для nonatomic NSObject * свойства вручную без использования synthesize и ARC. Пока из всех приходящих людей только один ответил так, чтобы можно было считать его ответ правильным.
        • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            Именно их и предлагается написать.
            • 0
              А какой ответ вы ждете от испытуемых, и на чем они чаще всего ошибаются?
              • 0
                Я прошу написать примерно следующее:
                -(void)setObject:(NSObject *)obj { id tmp = _obj; _obj = [obj retain]; [tmp release]; }

                А ошибаются в том, что не могут это написать, хотя это банальщина.

                Я даже согласен на двухстрочный грязный вариант без временной переменной, который способен отдать мертвый объект в соседнем потоке, потому что речь не про потокобезопасность, а просто про подсчет ссылок.
                • 0
                  Вообще-то, лучше так:
                  - (void) setObject:(NSObject *)obj
                  {
                    if(obj != _obj) {
                      [_obj autorelease];
                      _obj = [obj retain];
                    }
                  }
                  
            • 0
              Поясню предыдущий вопрос.

              Существует достаточно распространенное мнение, что в переопределенных сеттерах для сохранения KVO совместимости необходимо вручную вызывать методы willChangeValueForKey: и didChangeValueForKey:

              Однако данное мнение ошибочно, т.к. KVO при подмене класса объекта расширяет сеттеры и вызывает данные методы автоматически. (Если только вручную не отключить автоматическое уведомление, переопределив +automaticallyNotifiesObserversForKey:)

              Вот было любопытно, доходит ли разговор до KVO, или все ошибки ещё на этапе retain/release.
              • 0
                Нет, речь не о тонкостях — мы не сеньоров набираем. Речь просто про базовое понимание принципа подсчета ссылок.
    • 0
      Можно не добавлять модификатор __autoreleasing в сигнатуру -(BOOL)save:(NSError * __autoreleasing *) error, т.к. переменная-указатель на id имеет этот модификатор по умолчанию.
      • 0
        Для блока нужно вызывать copy только если он все еще размещен на стеке, т.е. если блок объявлен вне метода это делать особого смысла нет.
        • 0
          NSString *a = (__bridge NSString*)my_cfref; // пустой каст

          В этой строчке происходит увеличение количества ссылок, так как переменная имеет модификатор __strong.
          • 0
            Вы правы, надо было пояснить. my_cfref в таком случае останется +1, и ее надо отдельно освобождать.
          • +3
            Блоки по-прежднему необходимо копировать.
            //локальная переменная
            someBlockType someBlock = ^{NSLog(@"hi");}; 
            [someArray addObject:[someBlock copy]];
            

            Мне кажется, интересно разобрать этот момент подробнее.

            Во-первых, почему нужно копировать? Потому что по-умолчанию блоки создаются в стеке, а не в куче, проверим:
                int i = 0;
                NSLog(@"%@",[^{NSLog(@"i = %d", i);} class]);
            // __NSStackBlock__
            

            Блок класса __NSStackBlock__, т.е по выходу из области видимости указатель на блок станет невалидным, поэтому нам надо скопировать его в кучу, тогда в массиве будет храниться верный указатель:
                int i = 0;
                NSLog(@"%@",[[^{NSLog(@"i = %d", i);} copy] class]);
            //__NSMallocBlock__
            

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

            Но любопытно, если инициализировать блок через переменную, то копирование не обязательно, блок уже будет в куче.
                int i = 0;
                dispatch_block_t someBlock = ^{NSLog(@"i = %d", i);};
                NSLog(@"%@",[someBlock class]);
            // __NSMallocBlock__
            

            Почему так происходит? Потому что по-умолчанию для указателей на объекты (а блок — это объект) применяется модификатор __strong, т.е. код разворачивается в __strong dispatch_block_t someBlock = ...; затем при инициализации strong переменной объекту посылается retain, а в соответствии с документацией ARC такой retain для блоков разворачивается в Block_copy. Итого, блок уже в куче.

            Кроме того, примитивные блоки без доступа к окружающему scope никогда не лежат в стеке, а определяются как глобальные константы, операции copy и release на них никак не влияют.
                NSLog(@"%@",[^{NSLog(@"hi");} class]);
                NSLog(@"%@",[[^{NSLog(@"hi");} copy] class]);
            // __NSGlobalBlock__
            // __NSGlobalBlock__
            

            (Поэтому для предыдущих примеров мне понадобилось ввести переменную «i».)

            Но, безусловно, всё равно лучше придерживаться правила всегда копировать блоки при передачи их за область видимости, где они были определены. Просто, понимая внутренние процессы, запомнить это правило по-моему проще.
            • 0
              Вот спасибо, мудрый человек! Всегда знал, что блоки со стека надо копировать, но не мог понять почему и без копирования все работает!
            • +1
              Важная и полезная особенность ARC: сразу после деаллокации слабые и сильные ссылки обнуляются, то есть становятся равными nil.

              Сильные в эту фразу очевидно случайно попали, ибо если они не nil, то какая тогда деаллокация?

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

              О циклических ссылках компилятор предупредит

              На компилятор надейся, да сам не плошай. Компилятор видит далеко не всё, пример вчера приводил. Одна из частых ошибок — использование переменных экземпляра в блоке, в результате чего также ретейнится объект. ( _i эквивалентно self->_i )
              • 0
                Спасибо за замечание относительно сильных ссылок, поправил.
                • 0
                  можно допустить ошибку и указатель на объект занулится раньше времени, падения не произойдет, потому что посылка сообщения nil`у валидная операция и тут самая большая проблема. Приложение то будет работать, но может вести себя совсем не так как ожидается и вы будете смотреть и не понимать где же кроется проблема. По мне так лучше пусть оно честно упадет.
                  • 0
                    Всё верно. Если программная логика подразумеает, что объект существует, а он уже деаллоцировался, то рано или поздно произойдёт что-то нехорошое, тут надо кидать исключение и падать.

                    Но во-первых, если использовать assign указатель, то программа не обязательно сразу упадёт. Память по адресу указателя может быть всё ещё не перезаписана, или перезаписана объектом, у которого существуют те же методы, что и у предыдущего; в итоге сообщение может быть отправлено успешно, но другому объекту, что ещё хуже, чем недоставка нужного сообщения (представьте к примеру, что хотели удалить что-то не нужное, а в итоге стерли данные пользователя; — уж лучше б ничего не удаляли).

                    Во-вторых, нет никакой возможности проверить assign указатель на валидность (жив ли ещё объект или нет). А weak элементарно сравниваем с nil (через приведение к strong, но не суть), и по факту сравнения решаем, что делать дальше.

                    На практике при использовании weak всегда подразумевается возможность зануления.

                    Ещё, можно включить опцию компилятора, чтобы он предупреждал при отправке сообщений через weak указатели.
                • 0
                  Простите за глупый вопрос, вот тут:

                  __weak SomeObjectClass *weakSelf = self;
                  SomeBlockType someBlock = ^{
                  SomeObjectClass *strongSelf = weakSelf;
                  if (strongSelf) {
                  [strongSelf someMethod];
                  }
                  };

                  можно же обойтись без strongSelf, просто используя weakSelf?
                  • 0
                    Если ничего больше не будете добавлять в блок, — то да.

                    __typeof(self) __weak weakSelf = self;
                    SomeBlockType someBlock = ^{
                        [weakSelf someMethod];
                    };
                    
                    • 0
                      Только спустя все это время понял почему нужно локальную переменную делать))) Иначе self может пропасть в любой момент, даже после прохождения if. Мне стыдно)
                      • 0
                        А могли бы вы пояснить, что такое можно добавить в блок, что по-другому надо работать внутри с 'weakSelf'? Что-то «ретейнящее»?
                    • 0
                      К сожалению, не всегда есть возможность использовать ARC. Например, при написании скринсейвера для 10.7 — только GC-compatible, а он не совместим с ARC.
                      • 0
                        По поводу старичков которые говорят, что нужно писать без ARC, чтобы держать все под контролем, в одной книге написано следующее: Если не доверяют компилятору, пусть откроют код в ассемблированом виде и контролируют по полной)
                        • 0
                          Вырвано из контекста вероятно или книжка плохая. Когда явно известен факт, что доверять нельзя, это другое.
                        • 0
                          Используйте ARC. Это проще, безопаснее и сэкономит вам время и нервы.

                          Ха ха. Сегодня с другом разбирали пол дня его проблему. В итоге ARC создан чтобы не следить за памятью. Но в некоторых случаях он сбоит и нужно то закэшировать переменную, то ещё чего, чтобы он не вставил dealloc. ИМХО найти проблемное место, и то что оно возникает куда большая проблема, чем писать сразу явно, тогда не будет вопросов, или использовать другой язык. Он новичек, а я вообще на object c и swift не пишу, не знали про такие грабли, спасибо за статью, я думал это баг. В следующий раз будем знать куда копать, но это костыли какие то, подкручивать счётчики когда он сам не справляется, а это узнаешь постфактум, зачем он тогда нужен, если полагаться нельзя.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.