Pull to refresh

Automatic Reference Counting: часть 1

Reading time 7 min
Views 32K
Original author: Mike Ash
Здравствуйте, коллеги.

Давно читаю блоги и статьи зарубежных разработчиков под iOS. И на днях попалась любопытная, довольно подробная статья об Automatic Reference Counting от разработчика по имени Mike Ash.
Статья довольно большая, потому перевод, сделанный мною, рискну разбить на несколько частей. Надеюсь, что уложусь в 2 части.


С тех пор, как Эппл анонсировал это нововведение, читатели призывали меня написать о механизме Automatic Reference Counting (ARC). Время пришло. Сегодня поговорим о новой системе управления памятью от Эппл: как это работает и как это заставить работать на себя.

Концепция

Статический анализатор Clang действительно полезный инструмент для поиска ошибок управления памятью в вашем коде. Если мы с вами похожи, то просматривая вывод анализатора, думаем: “Если уж ошибка выявлена анализатором, почему бы ему не исправить ее вместо меня?”

Вкратце — это и есть суть ARC. Правила управления памятью встроены в компилятор. Но вместо использования их для помощи разработчику в поиске ошибок, механизм ARC просто вставляет необходимые вызовы. И все.

Механизм ARC занимает положение где-то между сборщиком мусора и ручным управлением памятью. Как и сборщик мусора, ARC избавляет разработчика от необходимости написания вызовов retain/release/autorelease. Тем не менее, в отличие от сборщика мусора, ARC никак не реагирует на циклы retain. Два объекта со строгими ссылками друг на друга никогда не будут обработаны ARC, даже если никто более не ссылается на них. В то время, как ARC избавляет программиста от большинства задач управления памятью, разработчик по прежнему должен избегать или вручную уничтожать строгие циклические ссылки на объект.

Что касается специфики внедрения, тот тут еще одно ключевое различие между ARC и реализацией GC от Эппла: ARC не является его усовершенствованием. При использовании GC от Эппл либо все приложение работает в среде GC, либо нет. Это означает, что весь Objective-C код в приложении, включая используемые фреймворки Эппла и сторонние библиотеки, должны быть совместимы с GC для использования оного. Примечательно, что ARC мирно сосуществует с “не-ARC” кодом с ручным управлением памятью в одном и том же приложении. Это дает возможность конвертировать проекты по частям без больших проблем совместимости и надежности, с которыми столкнулся GC, когда он был внедрен.

Xcode

ARC доступен, начиная с Xcode 4.2 beta, и только при выборе компиляции с использованием Clang (a.k.a. «Apple LLVM compiler»). Для его использования достаточно отметить настройку с очевидным названием «Objective-C Automatic Reference Counting».
Если вы работаете уже с существующей базой кода, изменение этой настройки может повлечь за собой громадное число ошибок. ARC не только управляет памятью вместо вас, но и запрещает вам делать это собственноручно. Совершенно недопустимо, используя ARC, посылать сообщения retain/release/autorelease объектам. Исходя из того, что Cocoa весь нашпигован подобными сообщениями, неизбежно вы получите тонну ошибок.
К счастью, Xcode предлагает инструмент для преобразования существующего кода. Для этого достаточно выбрать Edit -> Refactor… -> Convert to Objective-C ARC... — и Xcode позволит вам пошагово преобразовать ваш код. Исключая очень узкие места, где вам необходимо будет указать, что делать, этот процесс в большой степени автоматический.

Базовая функциональность

Правила управления памятью в Cocoa достаточно просты. Вкратце:
1. Если вы посылаете alloc, new, copy, или retain объекту, вы должны компенсировать это, используя release или autorelease.
2. Если вы получили объект другим путем, не описанным в п.1, и вам необходимо, чтоб объект был “живым” достаточно долгое время, вы должны использовать retain или copy. Естественно, позже это должно быть компенсировано вами.

Они (правила) весьма способствуют автоматизации процесса. Если вы пишете:
  Foo *foo = [[Foo alloc] init];
   [foo something];
   return;


Компилятор видит несбалансированный alloc и преобразует в следующее:
  Foo *foo = [[Foo alloc] init];
   [foo something];
   [foo release];
   return;


В действительности же компилятор не вставляет код посылки сообщения release объекту. Вместо этого он вставляет вызов специальной runtime-функции:
  Foo *foo = [[Foo alloc] init];
   [foo something];
   objc_release(foo);
   return;

Тут можно применить некоторую оптимизацию. В большинстве случаев, когда -release не переопределен, objc_release может обойти механизм сообщений Objective-C, в результате чего можно получить небольшой прирост в скорости.

Подобная автоматизация поможет сделать код безопаснее. Многие разработчики на Cocoa трактуют выражение “достаточно долгое время” из п.2, подразумевая объекты, хранящихся в переменных экземпляра или подобных местах. Обычно мы не применяем retain и release к локальным временным объектам:
  Foo *foo = [self foo];
   [foo bar];
   [foo baz];
   [foo quux];


Тем не менее, следующая конструкция довольно опасна:
  Foo *foo = [self foo];
   [foo bar];
   [foo baz];
   [self setFoo: newFoo];
   [foo quux]; // crash


Это можно исправить, имея геттер для -foo, в котором посылается retain/autorelease до возвращения значения. Вполне рабочий вариант, но может произвести много временных объектов, активно использующих память. Однако ARC параноидно вставит экстра-вызовы тут:
  Foo *foo = objc_retainAutoreleasedReturnValue([self foo]);
   [foo bar];
   [foo baz];
   [self setFoo: newFoo];
   [foo quux]; // fine
   objc_release(foo);


Кроме того, даже если вы написали простой геттер, ARC сделает его как можно более безопасным:
  - (Foo *)foo
   {
       return objc_retainAutoreleaseReturnValue(_foo);
   }


Однако и это не решает проблему чрезмерного количества временных объектов полностью! Мы, как и прежде, применяем сочетание retain/autorelease в геттере и комбинацию retain/release в вызывающем коде. Это, несомненно, малоэффективно!

Но не беспокойтесь без меры. Как было сказано ранее, ARC (в целях оптимизации) использует специальные вызовы вместо обычной посылки сообщений. Вдобавок к ускорению retain и release, эти вызовы позволяют устранить некоторые операции в целом.

Когда objc_retainAutoreleaseReturnValue работает, он мониторит стэк и запоминает обратный адрес вызывающего объекта. Это позволяет ему точно узнать, что произойдет после его завершения. При включенной оптимизации компиляции, вызов objc_retainAutoreleaseReturnValue может быть поводом для оптимизации хвостовой рекурсии (tail-call optimization).

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

Заметьте: эта оптимизация полностью совместима с кодом, не использующим механизм ARC.
В событии, геттер которого не использует ARC, указанный выше флаг не устанавливается и вызывающий объект может использовать полную комбинацию retain/release сообщений.
В событии, геттер которого использует ARC, а вызывающий объект — нет, геттер видит, что он не возвращается к коду, сразу же вызывающего специальные специальные runtime-функции, и потому может использовать полную комбинацию retain/autorelease сообщений.
Некоторая производительность теряется, зато сохраняется полная корректность кода.

Вдобавок ко всему, механизм ARC автоматически создает или заполняет метод -dealloc для всех классов для освобождения всех переменных его экземпляра. При этом все еще есть возможность ручного написания -dealloc (и это необходимо для классов, управляющих внешними ресурсами), но в дальнейшем нет необходимости (или возможности) вручную освобождать переменные экземпляра класса. ARC даже вставит [super dealloc] вызов в конце, избавив вас от заботы об этом.

Проиллюстрируем сказанное.
Ранее вы могли иметь следующую конструкцию:
  - (void)dealloc
   {
       [ivar1 release];
       [ivar2 release];
       free(buffer);

       [super dealloc];
   }


Но вам достаточно написать:
  - (void)dealloc
   {
       free(buffer);
   }


В случае, если ваш -dealloc метод просто освобождал переменные экземпляра, данная конструкция (с использованием ARC) сделает то же самое вместо вас.

Циклы и слабые ссылки

ARC все еще обязывает разработчика вручную обрабатывать циклические ссылки, и лучший способ решить этот вопрос — использовать слабые ссылки.

ARC использует обнуление слабых ссылок. Такие ссылки не только не сохраняют “живым” объект, на который они ссылаются, но также автоматически становятся nil-ами, когда объект, на который они ссылаются, уничтожается. Обнуление слабых ссылок позволяет устранить проблему зависших указателей и связанных с ними падениями и непредсказуемым поведением приложения.
Для создания обнуляемой слабой ссылки необходимо просто добавить префикс __weak при объявлении.
Например, вот создание переменной экземпляра такого типа:
  @interface Foo : NSObject
   {
       __weak Bar *_weakBar;
   }


Таким же образом создаются и локальные переменные:
  __weak Foo *_weakFoo = [object foo];


Вы можете в дальнейшем использовать их как обычные переменные, но их значение автоматически станет nil, когда возникнет надобность:
  [_weakBar doSomethingIfStillAlive];


Однако держите в уме, что __weak переменная может стать nil практически в любой момент времени.
Управление памятью, по сути, многопоточный процесс и слабо связанный объект может быть уничтожен в одном потоке, в то время, как другой поток к нему обращается.
Проиллюстрируем.
Следующий код неправилен:
  if(_weakBar)
       [self mustNotBeNil: _weakBar];


Вместо этого, используйте локальную строгую ссылку и затем проверьте:
  Bar *bar = _weakBar;
   if(bar)
       [self mustNotBeNil: bar];


Это гарантированно сделает объект живым (и переменную не-nil ) на все время его использования, потому что в данном случае bar — это строгая ссылка.

Реализация в ARC обнуления слабых ссылок нуждается в тесной координации между системой учета ссылок в Objective-C и системой обнуления слабых ссылок. Это означает, что класс, который переопределяет retain и release, не может быть объектом обнуляемой слабой ссылки. Хоть это и необычно, некоторые Cocoa классы страдают от этого (напр., NSWindow).

К счастью, если вы допустили подобную ошибку, программа даст вам знать об это незамедлительно, выдав сообщение при падении, на манер этого:
  objc[2478]: cannot form weak reference to instance (0x10360f000) of class NSWindow


Если вы действительно нуждаетесь в слабой ссылке на подобные классы, можете использовать __unsafe_unretained вместо __weak. Это создаст слабую ссылку, которая НЕ будет обнулена. Но вы должны быть уверены, что не используете этот указатель (предпочтительно, чтобы вы обнулили его вручную) ПОСЛЕ того, как объект, на который он указывает, был уничтожен.
Будьте осторожны: используя необнуляемые слабые ссылки, вы играете с огнем.

Несмотря на возможность использования ARC для написания приложений под Mac OS X 10.6 и iOS 4, обнуляемые слабые ссылки недоступны в этих ОС (прим. переводчика: Совершенно не понял смысл этого пассажа в контексте разбора ARC. Возможно, автор допустил опечатку, вместо zeroing weak references должно быть non-zeroing weak references).
Все слабые ссылки должны быть обработаны с __unsafe_unretained.

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



Тут я закончу первую часть перевода, поскольку текст и так получился немаленьким. Старался как можно лучше перевести на русский язык, что не особо просто, если 99% читаемых материалов — на английском.
Как говорится, stay tuned.
Tags:
Hubs:
+28
Comments 15
Comments Comments 15

Articles