STRONG UNOWNED WEAK — В чем разница?

Hadevs
7 min readMar 18, 2019

Перевод статьи Hector Matos «”WEAK, STRONG, UNOWNED, OH MY!” — A GUIDE TO REFERENCES IN SWIFT». Оригинал можно найти здесь.

Я часто беспокоюсь о наличии retain циклов в моем коде. Я считаю, что это общая проблема у многих разработчиков. Не знаю как вы, но я постоянно слышу подобные фразы: “Когда я должен использовать weak ссылки? И что вообще такое unowned?!” Проблема состоит в том, что мы используем strong, weak и unowned в нашем Swift коде, чтобы избежать retain циклов, но мы не до конца понимаем, где и какой спецификатор использовать. К счастью, я знаю, что это такое и когда это использовать! Я надеюсь, что это руководство поможет вам узнать, как работать с ними.

Давайте начнем

ARC

ARC — это механизм для автоматизированного управления памятью от Apple. Он расшифровывается как автоматический подсчет ссылок (Automatic Reference Counting). Механизм освобождает память для объектов (dealloc) только тогда, когда на них нет сильных (strong) ссылок.

STRONG

Стоит рассказать что такое strong ссылки. По сути, это обычная ссылка, но также она защищает от удаления те объекты, на которые ссылается, путем увеличения retain count на единицу. Ведь, как мы помним, что пока что-то имеет сильную ссылку на объект, он не будет уничтожен. Это важно запомнить на будущее, когда я буду объяснять что такое retain циклы и прочие вещи.

Сильные ссылки используются почти везде в Swift’e. На самом деле декларация свойства уже имеет сильную ссылку изначально. По сути, использовать подобные ссылки безопасно, когда иерархия отношений объектов линейна. Это значит, что когда родитель ссылается на дочерний объект с помощью сильных ссылок, мы не получим retain циклов, здесь все ок.

Здесь пример сильных ссылок в действии:

Здесь мы видим линейную иерархию на примере. У Kraken есть сильная ссылка к объекту класса Tentacle, а у него самого есть сильная ссылка к классу Sucker. Вся иерархия идет от родителя (Kraken) вниз до дочернего объекта (Sucker).

В похожем случае, в блоке анимаций, иерархия ссылка такая же:

Поскольку animateWithDuration является статическим методом в UIView, замыкание здесь является родителем, а self — дочерним объектом.

Но что будет, если дочерний объект захочет сделать ссылку на родительский? Здесь нам приходят на помощь weak и unowned ссылки.

WEAK и UNOWNED ссылки

WEAK

Слабая ссылка (weak reference) — это просто указатель на объект, который не защищает нас от уничтожения объекта путем ARC. Если сильные сслыки увеличивают количество ссылок на 1, то слабые — нет. К тому же слабые ссылки обнуляют указатель на ваш объект, когда он был успешно удален. Это гарантирует вам, что когда вы будете обращаться к слабой ссылки, вы получите либо правильный объект, либо nil.

В Swift’e все слабые ссылки — это неконстантные опционалы (Optionals). Потому что подобная ссылка может и будет изменяться на nil, когда на объект больше не будут указываться сильные ссылки.

Например, данный код не будет компилироваться:

Потому что tentacle это let константа. let константы по определению не могут быть изменены в рантайме. Поскольку объекты со слабыми ссылками могут быть нулевыми, если на них никто не ссылается с помощью сильных ссылок, компилятор Swift требует, чтобы все объекты с weakссылками были именно var (переменными).

Важными местами для использовани weak переменных являются те, у которых потенциально может произойти retain cycle. Это случается, когда два объекта имеют strong ссылки друг на друга. В таком случае ARC не будет генерировать соответствующий код сообщения об освобождении памяти для каждого экземпляра, так как они поддерживают друг друга.

Вот небольшое изображение Apple, которое хорошо иллюстрирует это:

Прекрасным примером этого являются API-интерфейсы Notification. Глянем на код:

На данный момент у нас есть retain цикл. Как вы можете видеть, closures (блоки замыкания) в свифте ведут себя точно так же, как и блоки в Objective-C. Если какая-либо переменная объявлена вне области действия замыкания, ссылка на эту переменную внутри области действия замыкания создает еще одну сильную ссылку на этот объект. Единственными исключениями из этого являются переменные, которые используют семантику значений, такую как Ints, Strings, Arrays и Dictionaries в Swift.

Здесь, NotificationCenter сохраняет замыкание, которое захватывает self сильно, когда вызывается функция eatHuman(). Хорошим тоном будет удалять наблюдателя (removeObserver). Проблема здесь в том, что мы не очищаем этот блок до deinit, но ARC никогда не вызовет deinit, потому что замыкание имеет сильную ссылку на экземпляр Kraken!

Подобые ошибки могут быть в таких местах, как NSTimer и NSThread.

Решение данной проблемы состоит в том, чтобы использовать weak ссылки в данных замыканиях. Это разрушает сильную ссылку цикла. В данном случае, график наших ссылок будет выглядеть так:

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

Чтобы использовать weak и unowned переменные в замыканиях, мы используем [] синтакс в теле этого замыкания. Пример:

Почему weak self здесь внутри квадратных скобок? Это выглядит странно! В свифте мы привыкли видеть подобные скобки у массивов. Здесь есть схожесть с ними, так как мы можем использовать несколько значений в замыканиях. Допустим:

Это уже больше похоже на массив, верно? Тепеь ты знаешь почему они в квадратных скобках. С этими знаниями мы можем исправить retain цикл в коде уведомлений, который мы видели ранее, добавив [weak self]:

Еще одно место, где мы должны использовать weak и unowned переменные — это там, где мы используем протоколы, которые выполняют роль делегатом. В свифте струтуры и enum’ы могут быть подчиняться протоколам, но они используют семантику значений. Если родительский класс использует делегирование с дочерним классом, например, так:

Тогда мы должны использовать weak переменные.

Вот почему:

Tentacle в данном случае хранит сильную ссылку на Kraken, под протоколом LossOfLimbDelegate в свойствеdelegate.

И в тоже время:

Kraken хранит сильную ссылку на Tentacle в свойстве tentacle.

Чтобы использовать weak перменную в данном сценарии, мы добавляем weak оператор в начало декларации нашего делегата:

Ты попробовал и у тебя не скомпилировалось? Да, проблема в том, что мы используем non-class протоколы, которые не могут быть помечены как weak.

В данном случае мы можем добавить в наш протокол :class.

Но когда мы не используем :class? Давайте спросим у Apple:

«Используйте протокол только для класса, когда поведение, определенное требованиями этого протокола, предполагает или требует, чтобы соответствующий тип имел ссылочную семантику, а не семантику значения».

По сути, если у вас есть ссылочная иерархия, точно такая же, как я показал выше, вы должны использовать :class. В ситуациях со структурами и перечислениями нет необходимости в :class, потому что структуры и перечисления используют Value Type, в то время как классы используют Reference Type.

UNOWNED

Поведение weak и unowned ссылок похоже, но не одинаково. Unowned ссылки, как и weak, не увеличивают количество retain ссылок на объект. Тем не менее в Swift unowned ссылка имеет преимущество — она не опциональна. Это облегчает управление объектами с unowned ссылкой. Данный принцип очень схож с force-unwrap у опционалов. Кроме того, unowned ссылки не обнуляются. Это означает, что когда объект освобождается, он не обнуляет указатель. Это означает, что использование unowned ссылок в некоторых случаях может привести к появлению висящих указателей. Для вас, стариков, которые помнят дни Objective-C, как и я, неподтвержденные ссылки отображаются на unsafe_unretained.

Это немного сбивает с толку. weak и unowned ссылки не увеличивают количество сохраняемых данных. Они оба могут быть использованы для избежания retain циклов. Так когда же мы их используем?! Согласно документации Apple:

«Используйте weak ссылку, если вы знаете, что в какой-то момент времени эта ссылка станет нулевой. И наоборот, используйте unowned ссылку, если вы знаете, что ссылка никогда не будет равна нулю.

Приходим к выводу: так же как и force-unwrap, если вы можете гарантировать, что ссылка не будет nil, то используйте unowned. Если нет, то используйте weak.

Здесь хороший пример того, как класс создает retain цикл, используя closure, который не будет nil, потому что он получает значение в init():

В этом случае цикл сохранения происходит от того, что замыкание захватывает self сильной ссылкой, в то время как self имеет сильную ссылку на замыкание через свойство замыкания. Чтобы сломать это, мы просто добавляем [unowned self] в назначение замыкания:

В этом случае мы можем предположить, что self никогда не будет nil, так как мы вызываем closure сразу после инициализации класса RetainCycle.

Если вы знаете, что ваша ссылка будет обнулена должным образом, и ваши две ссылки взаимно зависят друг от друга (один не может жить без другого), то вы должны отдавать предпочтение unowned, а не weak.

then you should prefer unowned over weak, since you aren’t going to want to have to deal with the overhead of your program trying to unnecessarily zero your reference pointers.

Идеальным местом для использования unowned ссылок будут lazy замыкания:

Нам нужен unowned self здесь чтобы избежать retain цикла. Kraken держит businessCardName closure все время, а сам closure держит Kraken'a. Они взаимозависимы, поэтому они всегда будут освобождены одновременно. Таким образом, он удовлетворяет правилам использования unowned!

ОДНАКО, это не должно быть перепутано с ленивыми переменными, которые НЕ ЯВЛЯЮТСЯ ЗАМЫКАНИЯМИ, такие как:

Unowned self не требуется, поскольку на самом деле ничто не сохраняет замыкание, вызываемое переменной lazy. Здесь нет замыкания, поэтому это просто ненужно. Вот скриншот, доказывающий это:

ИТОГ

Retain циклы — это отстой. Но при тщательном написании кода можно избежать утечек памяти за счет осторожного использования weak и unowned. Я надеюсь, что это руководство поможет вам в ваших начинаниях.

Ссылки на меня:

Телеграм канал с новостями Swift’a

Телеграм чат, где вам помогут

Мой сайт

--

--