objc-retain.com

Copy, Strong и Weak в Objective-C: управление владением объектами

Copy, Strong и Weak в Objective-C: управление владением объектами

В мобильной разработке для рынка недвижимости цена ошибки управления памятью — это не просто краш приложения. Это риэлтор, который потерял офлайн-доступ к карточке объекта перед сделкой, или датчик IoT в умном здании, чьи данные перестали поступать в аналитику из-за «повисшей» ссылки. Когда в Objective-C ломается память, ломается и бизнес-логика: внезапно очищается источник данных по жилому комплексу, теряется делегат экрана бронирования, возникает цикл удержания между контроллером каталога и его моделью. Разобраться в copy, strong и weak — значит понять, кто владеет объектом, кто отвечает за его жизнь и когда объект должен освобождаться. Это не теория, а ежедневная практика, особенно когда приложение работает с тысячами объектов недвижимости и должно быть стабильным при плохом интернете.

Что вообще означают copy, strong и weak

В ARC эти атрибуты описывают не «тип памяти», а семантику владения. strong удерживает объект — вы за него отвечаете, он будет жить, пока вы его держите. weak не удерживает — вы просто наблюдаете, и если объект исчезнет, ваша ссылка автоматически станет nil, без висячих указателей. copy создаёт копию значения при присваивании и тоже даёт собственность на результат, но уже на независимый экземпляр. У Apple weak-ссылки не влияют на жизненный цикл объекта: когда сильных ссылок не остаётся, объект освобождается, а weak-ссылки обнуляются.

Проще всего думать так:

  • strong — «я держу объект у себя, он мой»
  • weak — «я просто смотрю на него, но не мешаю ему исчезнуть»
  • copy — «я беру себе отдельную копию, чтобы исходник не мог поменять моё состояние»

В контексте недвижимости это легко представить: strong — это когда CRM-система владеет списком объектов, weak — когда карточка объекта ссылается на делегата навигации, а copy — когда адрес объекта фиксируется на момент получения из API, чтобы никакой фоновый поток не поменял его до показа клиенту.

Как работает strong

strong — базовый выбор для большинства объектов под ARC. Если свойство объявлено как strong, оно сохраняет объект в памяти, пока на него есть хотя бы одна сильная ссылка. Это эквивалентно идее владения: объект живёт, пока кто-то за него держится. В приложениях для девелоперов это означает, что кэшированный каталог квартир или данные по ипотечным ставкам будут доступны до тех пор, пока жив контроллер, который ими владеет.

На практике strong-ссылки формируют граф владения: если ViewController держит модель данных, а модель — массив объектов недвижимости, то вся цепочка остаётся в памяти, пока жив контроллер. Это именно то поведение, которое нужно для экранов с детальной информацией о ЖК, где пользователь может скроллить планировки и фотографии без подгрузок.

Когда использовать strong

strong подходит для:

  • моделей данных, которыми действительно владеет объект — например, массив объектов Property в менеджере каталога;
  • массивов, словарей, сетов — особенно когда они хранят результаты фильтрации по параметрам (цена, площадь, этаж);
  • зависимостей, которые должны жить столько же, сколько и владелец — например, сервис аналитики, собирающий данные с IoT-датчиков в здании;
  • вложенных объектов, которые не должны исчезать раньше родителя — скажем, планировка квартиры внутри карточки объекта.

Типичный пример

@interface PropertyListViewController : UIViewController
@property (nonatomic, strong) NSArray *properties;
@end

Если контроллер хранит список объектов, strong — нормальный вариант: пока жив контроллер, жив и список. Риэлтор может сворачивать приложение, переключаться между экранами — данные останутся на месте. Именно так работают офлайн-каталоги: массив держится в памяти, и при потере сети пользователь продолжает просматривать объекты.

Частая ошибка

Если поставить weak там, где нужна собственность, объект может исчезнуть сразу после присваивания. Это особенно заметно, когда объект создаётся локально и больше никто его не удерживает.

@interface SomeClass : NSObject
@property (nonatomic, weak) NSString *name;
@end

- (void)example {
    self.name = [NSString stringWithFormat:@"ЖК %@", @"Солнечный"];
    // name может стать nil сразу после выхода из метода
}

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

Как работает weak

weak нужен для не владеющих связей. Такая ссылка не увеличивает счётчик удержания и не продлевает жизнь объекта. Если объект освобождается, weak-ссылка автоматически становится nil, что защищает от обращения к «мёртвому» адресу памяти. Это критически важно в навигации по экранам недвижимости: когда пользователь уходит с экрана фильтрации, его делегат не должен удерживать этот экран в памяти.

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

Где weak обязателен на практике

Чаще всего weak используют для:

  • делегатов — например, делегат экрана выбора квартиры, который сообщает родительскому контроллеру о выборе пользователя;
  • обратных ссылок «ребёнок → родитель» — скажем, карточка объекта может иметь слабую ссылку на контроллер каталога, чтобы запросить обновление данных;
  • outlet-свойств в интерфейсе, если объект уже удерживается иерархией view — стандартная практика в Interface Builder;
  • ссылок, которые не должны формировать цикл владения — например, между сервисом синхронизации и его делегатом.

Пример делегата

@interface ApartmentSelectionViewController : UIViewController
@property (nonatomic, weak) id<ApartmentSelectionDelegate> delegate;
@end

Это классический случай. Если бы делегат был strong, можно было бы легко получить цикл удержания: объект A хранит B, а B хранит A. В реальном сценарии риэлторского приложения это означало бы, что после закрытия экрана выбора квартиры память не освобождается, и через 20-30 таких переходов приложение падает по OOM.

Почему weak безопаснее, чем assign

В старом MRC часто использовали assign, но в ARC для объектов это небезопасно: после деаллокации указатель может остаться «висячим». weak решает эту проблему автоматически, обнуляя ссылку. Для разработчика, который работает с данными недвижимости, это значит, что даже если делегат экрана бронирования был освобождён из-за навигации, обращение к нему не вызовет краш — просто метод не будет вызван, и это можно корректно обработать.

Когда нужен copy

copy используют, когда важно защититься от внешних изменений объекта после присваивания. Особенно это касается изменяемых классов, например NSMutableString, NSMutableArray, NSMutableDictionary. Если передать изменяемый объект в свойство без copy, владелец свойства будет видеть дальнейшие изменения исходного экземпляра. Представьте: адрес объекта недвижимости пришёл из API как mutable-строка, и пока риэлтор просматривает карточку, фоновый поток меняет в ней номер дома. Без copy это изменение отразится в интерфейсе мгновенно и незаметно для пользователя — результат может быть катастрофическим для сделки.

copy создаёт копию значения на момент присваивания и тем самым фиксирует состояние. В мире PropTech это особенно важно для любых данных, которые должны быть иммутабельными после получения: кадастровые номера, условия договора, параметры ипотеки.

Самые частые сценарии для copy

  • строки — адреса, имена клиентов, описания объектов;
  • блоки — колбэки завершения загрузки, обработчики нажатий;
  • коллекции, если нужно защититься от мутабельных подмен — например, список фотографий объекта, который не должен измениться после получения;
  • свойства, которые должны быть неизменяемыми по смыслу — идентификаторы, URL-адреса, координаты.

Пример со строкой

@interface PropertyCard : NSObject
@property (nonatomic, copy) NSString *address;
@end

- (void)configureWithAddress:(NSString *)address {
    self.address = address; // создастся копия, если address — mutable
}

Такой вариант хорош, потому что в свойство можно передать и NSString, и NSMutableString, но внутри останется безопасная копия. Для риэлтора это означает, что адрес «ул. Ленина, 15» не превратится в «ул. Ленина, 15к2» из-за того, что где-то в коде модифицировали исходную строку.

Пример с блоком

@interface CatalogLoader : NSObject
@property (nonatomic, copy) void (^completionBlock)(NSArray *properties, NSError *error);
@end

Для блоков copy — стандартная практика. Блок может жить в стеке, и копирование переносит его в более безопасную область памяти. В загрузчике каталога недвижимости это гарантирует, что колбэк завершения не исчезнет до того, как сервер вернёт ответ, даже если блок был создан во временном контексте.

Сравнение copy, strong и weak

Атрибут Владеет объектом Обнуляется при деаллокации Создаёт копию Типичный сценарий
strong Да Нет Нет модели, массивы, зависимости
weak Нет Да Нет делегаты, обратные ссылки
copy Да Нет Да строки, блоки, неизменяемые значения

Эта таблица — не просто шпаргалка, а карта принятия решений. Когда я проектировал модель данных для приложения девелопера, то вешал её на стену: каждое свойство проходило проверку по этим трём колонкам. Особенно полезно для новичков в команде, которые приходят из веба и не привыкли думать о владении объектами.

Как выбрать правильный атрибут

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

Шаг 1. Ответьте: вы владеете объектом или просто ссылаетесь на него?

  • владеете — берите strong
  • не владеете — чаще всего weak

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

Шаг 2. Может ли объект быть изменён извне после присваивания?

  • да — нужен copy
  • нет — strong обычно достаточно

Классический пример: массив параметров фильтрации (цена от/до, площадь, этаж) приходит извне. Если вы сохраняете его как strong, и кто-то изменит исходный массив, ваш фильтр «поплывёт». С copy вы фиксируете состояние на момент получения.

Шаг 3. Есть ли риск цикла удержания?

  • да — один из участников связи должен быть weak
  • нет — strong допустим

Циклы часто возникают в навигации: контроллер каталога создаёт контроллер фильтров, а тот ссылается обратно на каталог как на делегат. Если обе ссылки strong — память не освободится никогда. Поэтому делегат всегда weak.

Типовые ошибки, которые встречаются в коде

За годы работы с Objective-C в контексте недвижимости я видел эти ошибки десятки раз — и в стартапах, и в крупных девелоперских компаниях. Вот самые частые.

1. Делегат объявлен как strong

Это один из самых частых источников retain cycle. Если делегат хранится сильно, объект и его делегат могут начать удерживать друг друга. В приложении для риэлтора это проявлялось так: после 15-20 открытий карточек объектов приложение начинало тормозить и падать — память забивалась контроллерами, которые не могли освободиться из-за циклических ссылок с делегатами.

2. NSString объявлен как strong вместо copy

Для обычной immutable-строки это часто незаметно, но проблема вылезает, когда на вход подают NSMutableString. После изменения исходника свойство тоже может измениться, если копии не было. В одном проекте адрес объекта недвижимости менялся прямо в интерфейсе риэлтора, потому что фоновая синхронизация модифицировала исходную mutable-строку. Переход на copy решил проблему мгновенно.

3. Weak там, где нужна собственность

Если объект должен жить вместе с владельцем, weak приводит к неожиданному обнулению. Например, кэш изображений планировок был объявлен как weak-свойство — и при каждом скролле картинки перезагружались заново, потому что массив успевал освободиться между обращениями. Симптомы: постоянные мигания и высокая нагрузка на сеть.

4. Copy для заведомо неизменяемого и тяжёлого объекта без необходимости

Не всё нужно копировать. Если объект большой и копия не даёт выигрыша в безопасности, это может быть лишней затратой памяти и времени. Например, массив из 5000 объектов недвижимости, полученный как NSArray (не NSMutableArray), не нужно копировать — strong здесь и безопасен, и эффективен. Избыточное копирование в таком случае увеличивает пиковое потребление памяти на 30-40%, что критично для старых iPad’ов, которыми до сих пор пользуются некоторые риэлторы.

Практические примеры из мобильных приложений для недвижимости

В приложениях для риэлторов и девелоперов эти правила проявляются очень быстро. Вот несколько реальных сценариев, с которыми я сталкивался.

Карточка объекта

@interface PropertyCard : NSObject
@property (nonatomic, strong) NSArray *photos;
@property (nonatomic, copy) NSString *address;
@property (nonatomic, strong) NSNumber *price;
@end
  • photos удерживается карточкой — это сильная ссылка, потому что фотографии должны быть доступны, пока карточка на экране;
  • address лучше копировать как строку — адрес приходит из API и не должен измениться, пока риэлтор показывает его клиенту;
  • price можно хранить strongly как значение-обёртку — это иммутабельный объект, копия здесь избыточна.

В реальном приложении одного девелопера мы добавили ещё и @property (nonatomic, copy) NSString *cadastralNumber; — кадастровый номер тоже копируется, чтобы избежать случайных изменений при фоновой синхронизации с сервером Росреестра.

Делегат экрана выбора квартиры

@interface ApartmentSelectionViewController : UIViewController
@property (nonatomic, weak) id<ApartmentSelectionDelegate> delegate;
@end

Экран не должен владеть своим делегатом, иначе при навигации можно создать цикл. Типичный сценарий: риэлтор выбирает квартиру на плане этажа, делегат получает уведомление и обновляет основную карточку объекта. Если делегат strong — контроллер этажа и карточка объекта будут удерживать друг друга вечно.

Блок завершения загрузки каталога

@interface CatalogLoader : NSObject
@property (nonatomic, copy) void (^completion)(NSArray *properties, NSError *error);
@end

Здесь copy — правильный и безопасный выбор. Блок может быть создан на стеке в методе контроллера, и без копирования он исчезнет до того, как асинхронный запрос к серверу каталога завершится. В одном проекте мы столкнулись с тем, что колбэк загрузки каталога просто не вызывался — проблема была именно в отсутствии copy у свойства блока.

Чек-лист перед тем, как ставить атрибут

  • Свойство должно удерживать объект до конца жизни владельца? Используйте strong.
  • Свойство представляет обратную ссылку или делегат? Используйте weak.
  • Важно зафиксировать значение на момент присваивания? Используйте copy.
  • Есть риск, что объект изменится снаружи? Смотрите в сторону copy.
  • Есть риск цикла удержания? Проверьте, где должна быть weak-ссылка.
  • Это блок? Обычно нужен copy.
  • Это делегат? Почти всегда weak.

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

Как проверять себя в отладке

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

  1. Создайте объект и присвойте его в свойство.
  2. Уберите все внешние сильные ссылки.
  3. Проверьте, живёт ли объект дальше.
  4. Если объект не должен жить — значит, лишняя сильная ссылка где-то осталась.
  5. Если объект исчезает слишком рано — вероятно, нужен strong, а не weak.
  6. Если внешний mutable-объект меняет внутреннее состояние свойства — вероятно, нужен copy.

В Xcode для этого удобно использовать инструменты вроде Allocations и Leaks, а также расставлять символические брейкпоинты на dealloc интересующих объектов. Для риэлторского приложения я часто проверяю, что после закрытия экрана каталога все связанные с ним объекты (модели, загрузчики, делегаты) корректно освобождаются — это гарантирует, что память не утекает при интенсивной навигации.

Важный нюанс про ARC

ARC автоматизирует вставку retain/release, но не решает архитектурные ошибки владения. Он не отменяет циклы, не угадывает правильную семантику и не делает все свойства безопасными по умолчанию. Apple отдельно отмечает, что weak-ссылки не удерживают объект, а циклы нужно разрывать вручную. В контексте PropTech это означает, что даже с ARC вы можете получить утечку памяти, если контроллер дашборда умного здания и его виджеты ссылаются друг на друга через strong-свойства. ARC — это инструмент, а не замена пониманию того, как работает владение объектами.

Коротко о главном

  • strong означает владение объектом.
  • weak означает неблокирующую ссылку без владения.
  • copy защищает от внешних изменений и создаёт копию значения.
  • Делегаты почти всегда weak.
  • Строки и блоки часто лучше объявлять как copy.
  • Модели, массивы и зависимости обычно хранят как strong.

FAQ

Чем copy отличается от strong на практике?

strong просто удерживает тот же объект, а copy создаёт отдельную копию перед удержанием. Если вы работаете с адресом объекта недвижимости, strong позволит исходной mutable-строке изменить ваш адрес, а copy — нет.

Почему делегат почти всегда weak?

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

Можно ли использовать strong для NSString?

Можно, но для строк обычно безопаснее и привычнее copy, особенно если на вход может прийти mutable-строка. В проектах, где адреса и описания объектов приходят из разных источников (API, кэш, пользовательский ввод), copy — это страховка от неожиданных изменений.

Почему weak становится nil?

Потому что ARC автоматически обнуляет слабые ссылки, когда объект деаллочируется. Это защищает от крашей при обращении к освобождённой памяти — особенно важно в многопоточных сценариях, например, когда фоновый поток синхронизации освобождает модель, а UI-поток пытается её отобразить.

Когда copy не нужен?

Когда объект не должен быть защищён от внешних изменений и нет смысла создавать отдельную копию. Например, для большого иммутабельного массива объектов недвижимости copy только потратит память и время без какой-либо практической пользы.