Здравствуйте!
— Не хотите ли войти
Оглавление

Управление памятью в Mac OS X

15 января 2010
Уровень сложности: для разработчиков с небольшим опытом

Аннотация

Статья детально рассказывает об управлении памятью в Mac OS X. Рассмотрены такие темы как управление памятью в Objective-C приложениях использующих Cocoa и C приложениях использующих CoreFoundation.

В этой статье мне бы хотелось рассмотреть особенности управления памятью в Mac OS X. Я не буду рассматривать привычные для C либо C++ способы, такие как malloc, new, free, delete. Основная цель статьи — рассказать о методах, специфичных для Objective-C и C с использованием CoreFoundation.

Garbage Collector

Наиболее простой и рекомендуемый Apple способ управлению памятью — использование Garbage Collector (в дальнейшем GC). Использование GC позволяет не думать об освобождении выделенной памяти, что, в принципе, бывает удобно.

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

 
- (NSString *)fullName {
    NSMutableString *mString = [[NSMutableString alloc] init];
    if ([self firstName] != nil)
        [mString appendString:[self firstName]];
    if (([self firstName] != nil) && ([self lastName] != nil))
        [mString appendString:@" "];
    if ([self lastName] != nil)
        [mString appendString:[self lastName]];
    return [mString copy];
}

Несмотря на то, что объект mString не удаляется, утечек памяти не будет, т.к. заботу об удалении объектов на себя возьмет GC.

По умолчанию, в только что созданном проекте, GC отключен. Для его активации необходимо указать компилятору соответствующий флаг. Существуют 3 варианта настройки GC:

  • компилятору не указывается никакого дополнительного флага: GC отключен;
  • компилятору передан флаг -fobjc-gc-only. Этот флаг означает что в коде используется исключительно GC логика и не используются методы retain/release;
  • компилятору передан флаг -fobjc-gc. Флаг -fobjc-gc означает, что в коде используется как GC логика, так и методы retain/release.

Настраивается использование GC в свойствах проекта на панели Build.

Часть I. Objective-C

Objective-C — это основной язык программирования под Mac OS X. Я думаю, будет логично начать рассмотрение именно с него. Особенности же управления памятью при использовании CoreFoundation в языке C будут рассмотрены во второй части статьи.

Базовые принципы

Управление памятью в Objective-C базируется на принципе «владения объектом». Основные правила управления памятью в Objective-C можно записать так.

  • Для получения объекта во владение необходимо вызвать метод, называющийся «alloc», начинающийся с «new» либо содержащий «copy». Например, alloc, newObject, mutableCopy.
  • Для освобождения объекта, который был получен при помощи перечисленных выше функций, необходимо вызвать функцию «release» либо «autorelease». Во всех остальных случаях освобождение объекта не требуется.
  • Если полученый объект должен быть сохранен, необходимо либо стать его владельцем (передав сообщение retain), либо создать его копию (сохранив значение, которое возвращает обработка сообщения, содержащего в названии «copy»).

Данные правила базируются на соглашении по именованию в Objective-C и, в то же время, сами являются основой этого соглашения.

Базовые принципы на практике

Предположим, в программе существует класс Company, у которого есть селектор workers.

  
@interface Company : NSObject
{
    NSArray *workers;
}

-(NSArray*)workers;

@end

Рассмотрим небольшой пример использования такого класса:

  
Company *company = [[Company alloc] init];
// ...
NSArray *workers = [Company workers];
// ...
[company release];

Так как объект класса Company создается явно, он должен быть удален по окончанию использования ([company release]). В то же время, название метода workers не говорит о том, кто должен удалять массив. В такой ситуации считается, что списком работников управляет объект Компания и его удалять не требуется.

Convenience конструкторы

Многие классы позволяют совместить создание объекта с его инициализацией при помощи методов, называемых convenience конструкторы; такие методы обычно называются +className… Можно предположить, что вызывающая сторона ответственна за управление временем жизни объекта, но подобное поведение противоречило бы соглашению по именованию в Objective-C.

  
Company *company = [Company company];
[company release];

В приведенном коде вызов [company release] не допустим, т.к. в данном случае управление временем жизни объекта должно осуществляться при помощи autorelease пула (о нем будет рассказано ниже).

Ниже приводится пример корректной реализации метода company:

  
+(Company*)company
{
     id ret = [[Company alloc] init];
     return [ret autorelease];
}

autorelease

Вернемся к методу workers класса Company. Так как возвращается массив, временем жизни которого вызывающая сторона не управляет, реализация метода workers будет выглядеть приблизительно так:

 
-(NSArray*)workers
{
     NSArray *copy = [[NSArray alloc] initWithArray:workers];
     return [copy autorelease];
}

Вызов autorelease добавляет объект copy в autorelease пул, в следствии чего возвращаемый объект получит сообщение release при удалении пула, в который он был добавлен. Если объекту, добавленному в autorelease пул, послать сообщение release самостоятельно, при удалении autorelease пула возникнет ошибка (повторный release).

Возвращение объекта по ссылке

В ряде случаев объекты возвращаются по ссылке, например, метод класса NSData initWithContentsOfURL:options:error: в качестве параметра error принимает (NSError **)errorPtr. В этом случае так же работает соглашение по именованию, из которого следует, что явного запроса на владение объектом нет, соответственно, удалять его не требуется.

Удаление объектов

Когда счетчик ссылок объекта становится равным нулю, объект удаляется. При этом у объекта вызывается метод -(void)dealloc. Если в объекте содержатся какие-то данные, их необходимо удалить в этой функции.

  
-(void)dealloc
{
    [workers release];
    [super dealloc];
}

После того, как всем переменным класса было послано сообщение release, необходимо вызвать метод dealloc базового класса. Это единственный случай, в котором допустим вызов метода dealloc напрямую.

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

Autorelease pool

Autorelease пул используется для хранения объектов, которым будет послано сообщение release при удалении пула. Для того, чтобы добавить объект в autorelease пул, ему необходимо отправить сообщение autorelease.

В приложениях Cocoa, autorelease пул всегда доступен по умолчанию. Для не-AppKit приложений необходимо создавать и управлять временем жизни autorelease пула самостоятельно.

Autorelease пул реализуется классом NSAutoreleasePool.

  
int main (int argc, const char * argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
 
     Company *company = [Company company];
     NSArray *workers = [company workers];
 
    [pool drain];
    return 0;
}

Удалить объекты из autorelease пула можно не только посредством отправки пулу сообщения release, но и с помощью сообщения drain. Поведение release и drain в среде с подсчетом ссылок идентично. Но в случае работы в GC среде drain вызывает функцию objc_collect_if_needed.

Autorelease пул в многопоточной среде

В Cocoa для каждого из потоков создается свой собственный autorelease пул. По завершении потока autorelease пул уничтожается и всем содержащимся в нем объектам посылается сообщение release.

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

Доступ к свойствам

Допустим, в классе Company существует instance-переменная name.

  
@interface Company : NSObject
{
     NSString *name;
}

Для доступа к ней извне лучше всего воспользоваться свойствами, которые появились в Objective-C 2.0. Для декларации свойств используется ключевое слово @property.

  
@property (retain) NSString *name;

В скобках перечисляются атрибуты доступа к instance-переменной. Атрибуты разделяются на 3 группы.

Имена акцессора и мутатора

  • getter=getterName, используется для задания имени функции, используемой для изменения значения instance-переменой.
  • setter=setterName, используется для задания имени функции, используемой для установки значения instance-переменной.

Ограничение чтения/записи

  • readwrite — у свойства есть как акцессор, так и мутатор.
  • readonly — у свойства есть только акцессор.

И последняя группа, наиболее интересная в рамках данной статьи — атрибуты акцессора.

  • assign — для задания нового значения используется оператор присваивания. Используется только для POD типов либо для объектов которыми мы не владеем.
  • retain — указывает на то, что объекту, используемому в качестве нового значения instance-переменной, посылается сообщение retain.
  • copy — указывает на то, что для присваивания будет использована копия переданного объекта.

При работе под GC нет никакой разницы в использовании assign/retain.

Для создания кода свойств в соответствии с их описанием в декларации можно воспользоваться автогенерацией:

  
@synthesize name;

Автоматически созданный код не всегда подходящее решение и может потребоваться создание методов доступа к instance-переменным вручную.

Возможные реализации доступа к свойствам

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

Пример 1

 
-(NSString*)name
{
     return name;
}

-(void)setName:(NSString*)newName
{
     if (name != newName) {
          [name release];
          name = [newName retain];
     }
}

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

Пример 2

 
-(NSString*)name
{
     return [[name retain] autorelease];
}

-(void)setName:(NSString*)newName
{
     if (name != newName) {
          [name release];
          name = [newName retain];
     }
}

Второй пример демонстрирует несколько более сложное решение, но в то же время не имеющее потенциальной проблемы решения 1.

Пример 3

 
-(NSString*)name
{
     return [[name copy] autorelease];
}

-(void)setName:(NSString*)newName
{
     if (name != newName) {
          [name release];
          name = [newName copy];
     }
}

В примерах 1 и 2 в случае изменения оригинального значения будет изменено и значение instance-переменной, т.к. объект один и тот же. В ряде случаев такое поведение может быть нежелательным. В таких случаях в акцессоре можно создавать копию instance-переменной и добавлять ее в autorelease пул, а в мутаторе делать копию перед присвоением.

Копирование объектов

Все объекты в Objective-C потенциально поддерживают копирование. Для того, чтобы создать копию объекта, необходимо вызвать метод copy, определенный в классе NSObject. Для создания копии будет вызван метод copyWithZone интерфеса NSCopying. NSObject не имеет поддержки этого протокола и при необходимости протокол NSCopying должен быть реализован в классах-наследниках.

Копии бывают двух видов: поверхностная копия (shallow copy) и глубокая копия (deep copy). Разница между этими копиями состоит в том, что при создании поверхностной копии копируются не данные, а ссылка на объект с данными. В случае полной копии копируется объект с данными, которые он содержит.

При реализации копирования необходимо обратить внимание на то, каким образом реализован доступ к instance-переменным класса. Так, для примеров 1 и 2 описанных выше, можно создать легкую копию. Для примера 3 необходимо создавать полную копию данных.

Пример реализации

Реализация копирования может различаться в зависимости от того, поддерживает ли класс-родитель протокол NSCopying. Пример кода для ситуации, когда родитель не реализует протокол NSCopying:

@interface Company : NSObject <NSCopying>
{
     NSString *name;
}

@property(retain) NSString *name;

-(id)copyWithZone:(NSZone *)zone;
@end
 
@implementation Company

@synthesize name;

-(id)copyWithZone:(NSZone *)zone
{
     id *copy = [[[self class] allocWithZone:zone] init];
     [copy setName:[self name]];
     return copy;
}

@end

Если родитель поддерживает протокол NSCopying, реализация будет несколько иной: вызов allocWithZone заменяется на copyWithZone.

  
id *copy = [super copyWithZone:zone];

Копирование неизменяемых объектов

Для immutable объектов создание копии нецелесообразно, и можно ограничиться отправкой самому себе сообщения retain.

  
-(id)copyWithZone:(NSZone *)zone
{
     return [self retain];
}

Часть II. CoreFoundation and C

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

Базовые принципы

Принципы управления памятью в CoreFoundation отличается от того, к чему привыкло большинство C или C++ разработчиков. Управление памятью в CoreFoundation построено на тех же самых понятиях, что и управление памятью в Objective-C.

Например, вами был создан объект, значит, вы его владелец.

Если вы передали созданный объект в какую-то функцию, вы перестаете им владеть и владение переходит к этой функции. Если при этом вы не хотите, чтобы объект был уничтожен этой функцией, необходимо добавить себя в качестве владельца (используя CFRetian).

Если вы владеете объектом, то после окончания его использования необходимо исключить себя из списка владельцев объекта (используя CFRelease).

Соглашение по используемым именам

Соглашение по именованию функций в CoreFoundation довольно простое, тем не менее, нужно четко понимать, какие из функций передают объект во владение, а какие — нет. Если функция содержит в своем названии «Create» или «Copy», владение объектом передается. Если функция содержит в названии «Get», то владение объектом не передается.

Создание объектов

В CoreFoundation объект может быть создан двумя способами:

  • как копия уже существующего объекта, в этом случае в имени функции будет присутствовать «Copy»;
  • как новый объект, в этом случае в имени будет присутствовать «Create»;

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

CFMutableArrayRef CFArrayCreateMutable(CFAllocatorRef allocator, CFIndex capacity, const CFArrayCallBacks *callBacks);

CFMutableArrayRef CFArrayCreateMutableCopy(CFAllocatorRef allocator, CFIndex capacity, CFArrayRef theArray);

Данное правило актуально для всех функций CoreFoundation.

Получение объекта

При получении объекта при помощи методов «Get» владелец объекта не меняется и освобождение переданного объекта не требуется.

Например, функция CFArrayGetValueAtIndex возвращает объект с заданным индексом, но при этом массив CFArray управляет его временем жизни.

  
const void *CFArrayGetValueAtIndex(CFArrayRef theArray, CFIndex idx);

Если необходимо гарантировать возможность доступа к элементу после его удаления из массива, необходимо добавить себя в список владельцев (вызов CFRetain) либо сделать его копию. По окончанию использования элемента необходимо исключить себя из списка владельцев (вызов CFRelease).

Немного более детально

Приведу краткое объяснение того, что же происходит внутри:

  • при создании нового объекта счетчик устанавливается в единицу;
  • при копировании объекта счетчик копии объекта устанавливается в единицу и может быть не равен счетчику оригинала;
  • при вызове функции CFRetain счетчик объекта увеличивается на единицу;
  • при вызове функции CFRelease счетчик объекта уменьшается на единицу; если счетчик объекта равен нулю, объект уничтожается;
  • узнать значение счетчика существующего объекта можно при помощи функции CFGetRetainCount; информация о текущем значении счетчика может быть крайне полезна, если в программе наблюдаются «самопроизвольные» удаления объектов.

Копирование объектов

Существуют два вида копий объектов: легкая копия (shallow copy) и полная копия (deep copy).
Легкая копия используется при копировании константных объектов, таких как CFString, CFData и т.д. При создании легкой копии объект не копируется, а внутренний счетчик увеличивается на единицу. Легкая копия контейнеров, например, CFArray, создается несколько иначе. При создании легкой копии контейнера создается новый объект-контейнер, но хранимые элементы не копируются, просто значение счетчика каждого из элементов увеличивается на единицу.

При создании полной копии дублируются все вложенные элементы составного объекта. На данный момент CoreFoundation поддерживает создание полной копии только для объекта CFPropertyList (функция CFPropertyListCreateDeepCopy). Полная копия всех остальных объектов должна создаваться пользователем самостоятельно.

Пример:

 
#include <CoreFoundation/CoreFoundation.h>
 
void print_str(const void *val, void *context)
{
    CFNumberRef numb;
    CFIndex ref_cnt;
 
    numb = (CFNumberRef)val;
    ref_cnt = CFGetRetainCount(numb);
    printf("%in", ref_cnt);
}
 
void print_arr_info(CFArrayRef arr)
{
    CFIndex count;
    CFRange range;
 
    range = CFRangeMake(0, CFArrayGetCount(arr));
    CFArrayApplyFunction(arr, range, print_str, &count);
}
 
int main (int argc, const char * argv[])
{
    CFArrayRef array, copy_array;
    int numb = 100;
    CFNumberRef numbers[3] = {
        CFNumberCreate(NULL, kCFNumberIntType, &numb),
        CFNumberCreate(NULL, kCFNumberIntType, &numb),
        CFNumberCreate(NULL, kCFNumberIntType, &numb)
    };
 
    array = CFArrayCreate(NULL, (const void**)numbers, 3,
                                &kCFTypeArrayCallBacks);
    printf("retain count in 'array'n");
    print_arr_info(array);
    copy_array = CFArrayCreateCopy(NULL, array);
 
    printf("retain count in 'array' after"
            " CFArrayCreateCopy calln");
    print_arr_info(array);
    printf("retain count in 'copy_array'n");
    print_arr_info(copy_array); 
 
    return 0;
}

Вывод:

 
retain count in 'array'
{value = +100, type = kCFNumberSInt32Type}
ref. counter = 2
{value = +100, type = kCFNumberSInt32Type}
ref. counter = 2
retain count in 'array' after CFArrayCreateCopy call
{value = +100, type = kCFNumberSInt32Type}
ref. counter = 3
{value = +100, type = kCFNumberSInt32Type}
ref. counter = 3
retain count in 'copy_array'
{value = +100, type = kCFNumberSInt32Type}
ref. counter = 3
{value = +100, type = kCFNumberSInt32Type}
ref. counter = 3

Думаю, что с примером и распечаткой вывода все очевидно. Единственный момент, который может вызывать вопросы, это число счетчика, равное двум при выводе первого массива. Но здесь все просто: при создании массива из объектов CFNumberRef (массив numbers) каждому из элементов при первичной инициализации присваивается единица, а при создании массива (вызов CFArrayCreate) значение счетчика инкрементируется еще раз.

Хранение нестандартных элементов в контейнерах

При создании любого контейнера указываются call-back функции, используемые для манипуляции с сохраняемыми элементами. Ниже приводится определение структуры CFArrayCallBacks используемой массивами.

  
struct CFArrayCallBacks {
   CFIndex version;
   CFArrayRetainCallBack retain;
   CFArrayReleaseCallBack release;
   CFArrayCopyDescriptionCallBack copyDescription;
   CFArrayEqualCallBack equal;
};

В случае, когда в массиве хранятся только CoreFoundation типы (CFType), можно использовать call-back функции по умолчанию: kCFTypeArrayCallBacks. Но если в массиве сохраняются составные объекты либо объекты, не относящиеся к CFType, возникает необходимость в создании собственных обработчиков. Обычно это как минимум обработчики для release и equal.

Аллокаторы

В каждую из функций CoreFoundation, отвечающую за создание нового объекта, передается указатель на аллокатор. Аллокатор может быть как системный так и созданный пользователем.

В качестве аллокатора обычно указываются следующие параметры: kCFAllocatorSystemDefault. Как и следует из названия, системный аллокатор, используемый по умолчанию.

Если в качестве аллокатора передать NULL, то будет использован текущий аллокатор. Текущим аллокатором может быть либо аллокатор установленный пользователем, либо kCFAllocatorSystemDefault.

Кроме того, есть еще несколько аллокаторов, необходимость в использовании которых возникает нечасто. Это kCFAllocatorMalloc, kCFAllocatorMallocZone, kCFAllocatorNull аллокаторы.

Создание собственного аллокатора довольно экзотическая задача, и я не вижу смысла ее рассматривать. Если потребность в собственном аллокаторе все же возникла, всегда можно обратиться к документации Apple.

Взаимодействие с GC

GC может быть использован не только в Objective-C приложениях, но и в С приложениях, использующих CoreFoundation. В этом случае необходимо инициализировать GC вручную.

  
int main (int argc, const char * argv[]) {
    objc_startCollectorThread();
    // your code
    return 0;
}

В GC окружении для управления жизнью объекта также можно использовать функции CFRelease(), CFRetain(). Основное отличие в поведении CFRelease() в GC окружении состоит в том, что объект не удаляется сразу же при достижении счетчиком нуля; объект лишь помечается как удаляемый GC. Таким образом, до тех пор, пока на объект есть ссылки, он не будет удален. Исключение сделано для объектов, находящихся в malloc зоне (объекты созданные аллокатором kCFAllocatorMalloc), которые удаляются немедленно.

Пример использования GC:

  
CFStringRef myCFString = CFRelease(CFStringCreate...(...));

После того, как все ссылки на созданный объект исчезнут, он будет удален GC.

Ресурсы

© 2009-2010, ООО «Инру»
Вход
Имя пользователя:
Пароль:
Или…
Twi
Отмена
Войти
Восстановить забытый пароль…