Статьи

Objective-C лаконично: управление памятью

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

Рисунок 19 Подсчет ссылок на объект

Подсчет ссылок на объект

В отличие от C #, Objective-C не использует сборку мусора. Вместо этого он использует среду подсчета ссылок, которая отслеживает, сколько мест использует объект. Пока существует хотя бы одна ссылка на объект, среда выполнения Objective C гарантирует, что объект будет находиться в памяти. Однако, если больше нет ссылок на объект, среде выполнения разрешается освобождать объект и использовать память для чего-то другого. Если вы попытаетесь получить доступ к объекту после того, как он был освобожден, ваша программа, скорее всего, потерпит крах.

В Objective-C есть два взаимоисключающих способа управления ссылками на объекты:

  1. Вручную отправьте методы для увеличения / уменьшения количества ссылок на объект.
  2. Пусть Xcode 4.2 (и позже) новая схема автоматического подсчета ссылок (ARC) сделает всю работу за вас.

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


Чтобы поэкспериментировать с любым кодом из этого раздела, вам нужно отключить автоматический подсчет ссылок. Вы можете сделать это, щелкнув проект HelloObjectiveC на панели навигации Xcode:

Рисунок 20 Проект HelloObjectiveC на панели навигации

Проект HelloObjectiveC в панели навигации

Откроется окно, в котором можно настроить параметры сборки для проекта. Мы обсудим настройки сборки во второй половине этой серии. На данный момент все, что нам нужно найти, это флаг ARC. В поле поиска в правом верхнем углу введите автоматический подсчет ссылок , и вы должны увидеть следующую настройку:

Рисунок 21 Отключение автоматического подсчета ссылок

Отключение автоматического подсчета ссылок

Нажмите стрелки рядом с « Yes и измените его на « No чтобы отключить ARC для этого проекта. Это позволит вам использовать методы управления памятью, описанные в следующих параграфах.

Ручное управление памятью (также называемое ручным сохранением-выпуском или MMR) вращается вокруг концепции объекта «владение». Когда вы создаете объект, вы говорите, что владеете объектом — это ваша обязанность освободить объект, когда вы закончите с ним. Это имеет смысл, так как вы не хотите, чтобы какой-то другой объект появлялся и отпускал объект, пока вы его используете.

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

  • +(id)alloc — выделяет память для нового экземпляра и требует владения этим экземпляром. Это увеличивает количество ссылок объекта на один. Возвращает указатель на выделенный объект.
  • -(id)retain — заявить право собственности на существующий объект. У объекта может быть более одного владельца. Это также увеличивает счетчик ссылок объекта. Возвращает указатель на существующий объект.
  • -(void)release — Отказ от права собственности на объект. Это уменьшает количество ссылок объекта.
  • -(id)autorelease-(id)autorelease владения объектом в конце текущего блока пула autorelease. Это уменьшает количество ссылок объекта, но позволяет вам продолжать использовать объект, откладывая фактическое освобождение до более позднего момента времени. Возвращает указатель на существующий объект.

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

Все ваши взаимодействия с объектами, независимо от того, используете ли вы их в методе экземпляра, в методе getter / setter или в отдельной функции, должны следовать шаблону request / use / free, как показано в следующем примере:

Включенный пример кода: ручная память

01
02
03
04
05
06
07
08
09
10
11
12
13
14
int main(int argc, const char * argv[]) {
 
    // Claim the object.
    Person *frank = [[Person alloc] init];
 
    // Use the object.
    frank.name = @»Frank»;
    NSLog(@»%@», frank.name);
 
    // Free the object.
    [frank release];
 
    return 0;
}

Вызов [Person alloc] устанавливает счетчик ссылок Фрэнка в единицу, а [frank release] уменьшает его до нуля, позволяя среде исполнения избавиться от него. Обратите внимание, что попытка вызвать другой [frank release] приведет к сбою, так как переменная frank больше не существует в памяти.

При использовании объектов в качестве локальной переменной в функции (например, в предыдущем примере) управление памятью довольно простое: просто вызовите release в конце функции. Тем не менее, все может стать сложнее при назначении свойств внутри методов установки. Например, рассмотрим следующий интерфейс для нового класса с именем Ship :

Включенный пример кода: ручная память — слабая ссылка

1
2
3
4
5
6
7
8
9
// Ship.h
#import «Person.h»
 
@interface Ship : NSObject
 
— (Person *)captain;
— (void)setCaptain:(Person *)theCaptain;
 
@end

Это очень простой класс с определенными вручную методами доступа для свойства captain . С точки зрения управления памятью, существует несколько способов реализации сеттера. Сначала возьмем самый простой случай, когда новое значение просто присваивается переменной экземпляра:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
// Ship.m
#import «Ship.h»
 
@implementation Ship {
    Person *_captain;
}
 
— (Person *)captain {
    return _captain;
}
 
— (void)setCaptain:(Person *)theCaptain {
    _captain = theCaptain;
}
 
@end

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#import <Foundation/Foundation.h>
#import «Person.h»
#import «Ship.h»
 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
 
        Person *frank = [[Person alloc] init];
        Ship *discoveryOne = [[Ship alloc] init];
 
        frank.name = @»Frank»;
        [discoveryOne setCaptain:frank];
        NSLog(@»%@», [discoveryOne captain].name);
 
        [frank release];
 
        // [discoveryOne captain] is now invalid.
        NSLog(@»%@», [discoveryOne captain]. name);
 
        [discoveryOne release];
    }
    return 0;
}

При вызове [frank release] счетчик ссылок Фрэнка уменьшается до нуля, что означает, что среде выполнения разрешено его освобождать. Это означает, что [discoveryOne captain] теперь указывает на недопустимый адрес памяти, хотя discoveryOne никогда не освобождает его.

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

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

Рисунок 22 Слабая ссылка на значение капитана

Слабая ссылка на стоимость капитана

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

Включенный пример кода: ручная память — надежная ссылка

1
2
3
4
— (void)setCaptain:(Person *)theCaptain {
    [_captain autorelease];
    _captain = [theCaptain retain];
}

С сильной ссылкой, не имеет значения, что другие объекты делают с объектом theCaptain , так как retain гарантирует, что он будет оставаться там до тех пор, пока это требуется экземпляру Ship . Конечно, вам нужно сбалансировать retain вызова, освободив старое значение — если вы этого не сделаете, ваша программа будет терять память, когда кто-либо назначит новое значение свойству captain .

Рисунок 23 Сильная ссылка на значение капитана

Сильная ссылка на значение капитана

Метод autorelease работает так же, как и release , за исключением того, что счетчик ссылок объекта не уменьшается сразу. Вместо этого среда выполнения ожидает до конца текущего блока @autoreleasepool чтобы вызвать нормальный release объекта. Вот почему шаблон main.m всегда заключен в @autoreleasepool — он гарантирует, что все объекты, поставленные в очередь с autorelease , фактически освобождаются в конце программы:

01
02
03
04
05
06
07
08
09
10
11
int main(int argc, const char * argv[]) {
 
    @autoreleasepool {
 
        // Insert code to create and autorelease objects here.
        NSLog(@»Hello, World!»);
 
        // Any autoreleased objects are *actually* released here.
    }
    return 0;
}

Идея автоматического выпуска заключается в том, чтобы дать владельцу объекта возможность отказаться от владения, фактически не разрушая объект. Это необходимый инструмент в ситуациях, когда вам нужно вернуть новый объект из фабричного метода. Например, рассмотрим следующий метод класса, определенный в Ship.m :

1
2
3
4
5
+ (Ship *)shipWithCaptain:(Person *)theCaptian {
    Ship *theShip = [[Ship alloc] init];
    [theShip setCaptain:theCaptian];
    return theShip;
}

Этот метод создает, настраивает и возвращает новый экземпляр Ship . Но есть серьезная проблема с этой реализацией: она приводит к утечке памяти. Метод никогда не отказывается от владения объектом, и вызывающие shipWithCaptain не знают, что им нужно освободить возвращенный объект (и не должны). В результате объект theShip никогда не будет освобожден из памяти. Это как раз та ситуация, для autorelease был разработан autorelease . Правильная реализация показана здесь:

1
2
3
4
5
+ (Ship *)shipWithCaptain:(Person *)theCaptian {
    Ship *theShip = [[Ship alloc] init];
    [theShip setCaptain:theCaptian];
    return [theShip autorelease];
}

Использование autorelease вместо немедленного release позволяет вызывающей стороне использовать возвращенный объект, в то же время отказываясь от владения им в нужном месте. Если вы помните из главы «Типы данных», мы создали все наши структуры данных Foundation, используя фабричные методы уровня класса. Например:

1
NSSet *crew = [NSSet setWithObjects:@»Dave», @»Heywood», @»Frank», @»HAL», nil];

Метод setWithObjects работает точно так же, как метод shipWithCaptain описанный в предыдущем примере. Он возвращает автоматически выпущенный объект, чтобы вызывающий мог использовать объект, не беспокоясь об управлении памятью. Обратите внимание, что существуют эквивалентные методы экземпляра для инициализации объектов Foundation. Например, объект crew в последнем примере может быть создан вручную следующим образом:

1
2
3
4
5
6
7
// Create and claim the set.
NSSet *crew = [[NSSet alloc] initWithObjects:@»Dave», @»Heywood», @»Frank», @»HAL», nil];
 
// Use the set…
 
// Release the set.
[crew release];

Однако использование методов класса, таких как setWithObjects , arrayWithCapacity и т. Д., Обычно предпочтительнее, чем alloc / init .

Работа с памятью за свойствами объекта может быть утомительной, повторяющейся задачей. Чтобы упростить процесс, Objective-C включает в себя несколько атрибутов свойств для автоматизации вызовов управления памятью в функциях доступа. Атрибуты, описанные в следующем списке, определяют поведение установщика в средах ручного подсчета ссылок. Не пытайтесь использовать assign и retain в среде автоматического подсчета ссылок.

  • assignretain прямой указатель на новое значение без каких-либо вызовов retain / release . Это автоматический эквивалент слабой ссылки.
  • retainretain прямой указатель на новое значение, но вызвать release для старого значения и retain для нового. Это автоматический эквивалент строгой ссылки.
  • copy — Создать копию нового значения. При копировании заявляется право собственности на новый экземпляр, поэтому предыдущему значению отправляется сообщение о release . Это как сильная ссылка на новый экземпляр объекта. Обычно копирование используется только для неизменяемых типов, таких как NSString .

В качестве простого примера рассмотрим следующее объявление свойства:

1
@property (retain) Person *captain;

Атрибут retain сообщает связанному объявлению @synthesize о создании сеттера, который выглядит примерно так:

1
2
3
4
— (void)setCaptain:(Person *)theCaptain {
    [_captain release];
    _captain = [theCaptain retain];
}

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


Теперь, когда у вас есть управление подсчетом ссылок, владением объектами и блоками автоматического выпуска, вы можете полностью забыть обо всем этом. Начиная с Xcode 4.2 и iOS 4, Objective-C поддерживает автоматический подсчет ссылок (ARC), который является этапом предварительной компиляции, который добавляет необходимые вызовы управления памятью для вас.

Если вы отключили ARC в предыдущем разделе, вы должны снова включить его. Помните, что вы можете сделать это, щелкнув проект HelloObjectiveC на панели навигации, выбрав вкладку « Настройки сборки » и выполнив автоматический подсчет ссылок .

Рисунок 24 Включение автоматического подсчета ссылок в настройках сборки проектов

Включение автоматического подсчета ссылок в настройках сборки проекта

Автоматический подсчет ссылок работает, проверяя ваш код, чтобы выяснить, сколько времени объекту нужно придерживаться, и вставляя методы retain , release и autorelease чтобы убедиться, что он освобожден, когда он больше не нужен, но не во время его использования. Чтобы не перепутать алгоритм ARC, вы не должны делать какие-либо вызовы retain , release или autorelease самостоятельно. Например, с помощью ARC вы можете написать следующий метод, и theShip ни theCaptain не будут пропущены, даже если мы явно не отказались от владения ими:

Включенный пример кода: ARC

1
2
3
4
5
6
+ (Ship *)ship {
    Ship *theShip = [[Ship alloc] init];
    Person *theCaptain = [[Person alloc] init];
    [theShip setCaptain:theCaptain];
    return theShip;
}

В среде ARC вы больше не должны использовать атрибуты assign и retain свойств. Вместо этого вы должны использовать weak и strong атрибуты:

  • weak — укажите несобственное отношение к целевому объекту. Это очень похоже на assign ; тем не менее, он имеет удобную функциональность установки свойства равным nil если значение освобождено. Таким образом, ваша программа не будет аварийно завершать работу при попытке доступа к неверному адресу памяти.
  • strong — укажите отношение владельца к объекту назначения. Это ARC эквивалент retain . Это гарантирует, что объект не будет освобожден, пока он назначен свойству.

Вы можете увидеть разницу между слабым и сильным, используя реализацию метода класса ship из предыдущего раздела. Чтобы создать надежную ссылку на капитана корабля, интерфейс Ship должен выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
// Ship.h
#import «Person.h»
 
@interface Ship : NSObject
 
@property (strong) Person *captain;
 
+ (Ship *)ship;
 
@end

И реализация Ship должна выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
// Ship.m
#import «Ship.h»
 
@implementation Ship
 
@synthesize captain = _captain;
 
+ (Ship *)ship {
    Ship *theShip = [[Ship alloc] init];
    Person *theCaptain = [[Person alloc] init];
    [theShip setCaptain:theCaptain];
    return theShip;
}
 
@end

Затем вы можете изменить main.m для отображения капитана корабля:

1
2
3
4
5
6
7
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Ship *ship = [Ship ship];
        NSLog(@»%@», [ship captain]);
    }
    return 0;
}

Это выведет что-то вроде <Person: 0x7fd6c8c14560> в консоли, что говорит нам о том, theCaptain объект theCaptain созданный в методе класса ship все еще существует.

Но попробуйте изменить атрибут свойства (strong) на (weak) и перекомпилировать программу. Теперь вы должны увидеть (null) на панели вывода. Слабая ссылка не гарантирует, что переменная theCaptain , поэтому, как только она прибудет в конец метода класса ship , алгоритм ARC думает, что может избавиться от theCaptain . В результате свойство captain имеет значение nil .


Управление памятью может быть проблемой, но это неотъемлемая часть построения приложения. Для приложений iOS правильное распределение / удаление объектов особенно важно из-за ограниченных ресурсов памяти мобильных устройств. Мы поговорим об этом подробнее во второй части этой серии iOS .

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

Этот урок представляет собой главу из Objective-C, лаконично , бесплатную электронную книгу от команды Syncfusion .