Аватар пользователя yarmolchuk

Управление памятью в Objective-C

Как было обсуждено в модуле Properties , цель любого управления памяти есть снижение объема памяти программы с помощью контролирования продолжительности жизни каждого объекта. Приложения на iOS и OS X выполняют это через владение объектом, что дает возможность убедиться что объект существует до тех пор, пока должен, но не дольше.

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

reference-counting.png
Уничтожение объекта с нулевым показателем

В прошлом, разработчики вручную контролировали счет справки объекта вызывая специальные методы управления памятью ,определенные протоколом NSObject . Это называется Manual Retain Release (MRR). Однако, Xcode 4.2 представленный Automatic Reference Counting (ARC), что автоматически вставляет все методы вместо Вас. Современным приложениям следует всегда использовать ARC, так как это более надежно и позволяет сосредоточиться на особенностях вашего приложения вместо управления памятью.

Этот модуль объясняет суть подсчета ссылок в контексте MRR, потом обсуждает некоторые практические соображения ARC.

Ручное управление памьятю

В среде Manual Retain Release, ваша работа чтоб требовать и передавать собственность каждого объекта в вашей программе. Вы можете это сделать, вызвав специальные методы, что связаны с памятью, которые описаны ниже.

alloc - Создание объекта и объявления его собственности.
retain - Объявление собственности существующего объекта
copy - Копирование объекта и объявление его собственности.
release - Отказ от собственности объекта и его уничтожение немедленно.
autorelease - Отказ от собственности объекта, но с отложением его уничтожения.

Ручное управление объектом собственности может показаться сложной задачей, но на самом деле все очень просто. Все, что сам нужно сделать – объявить собственность любого объекта ,что вам нужен и не забыть отказаться от права собственности, когда закончите работу с ним. С практической точки зрения, это значит, что вам нужно балансировать каждый allocretain, и copy вызванные release или autorelease для одного и того же объекта.

Когда вы забываете балансировать эти вызовы, одно из двух может случиться. Если вы забудете уменьшить количество ссылок на объект, его основная память никогда не освободиться, и как результат – утечка памяти. Маленькие утечки памяти не будут иметь видимое влияние на вашу программу, но если вы съедите достаточно памяти, ваша программа в конечном итоге закрешится. С другой стороны, если вы попытаетесь уменьшить количество ссылок на объект слишком много раз, вы получите то, что называется dangling pointer(висящий пойнтер). Когда вы попытаетесь обратиться к dangling pointer, вы будете запрашивать недействительный адрес памяти , и ваша программа, скорей всего, закрешится.

balance.png
Балансирование претензий собственности с отключением (отказом)

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

Включение MRR

До того,как мы сможем экспериментировать с ручным управлением памяти, нам нужно выключить Automatic Reference Counting. Нажмите на иконку проекта в Project Navigator, и убедиться, что вкладка
Build Settings выбрана, и начинайте печатать automatic reference counting в поисковой строке. Опция компилятора Objective-C Automatic Reference Counting должна появиться. Измените с YES на NO.

turning-off-arc.png
Выключение Automatic Reference Counting

Помните, мы делаем это только для учебных целей-вам никогда не следует использовать Manual Retain Release для новых проектов.

Метод alloc

Мы использовали метод alloc чтоб создать объекты в этом руководстве. Но это не только распределенная память для объекта , но также устанавливает справку, чтоб посчитать до 1. Это делает много смысла, так, как мы не будем создавать объект, если мы не хотим держать его рядом, хотя бы на некоторое время.

// main.m
#import <Foundation/Foundation.h>
 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *inventory = [[NSMutableArray alloc] init];
        [inventory addObject:@"Honda Civic"];
        NSLog(@"%@", inventory);
    }
    return 0;
}

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

Но, так как мы не выпустили его, наша программа в настоящем времени имеет утечку памяти. Мы можем исследовать это в Xcode запустив наш проект через инструмент статический анализатор. Управляйте к Продукт > Анализировать в меню баре или используйте в меню баре сочетание клавиш Shift+Cmd+B . Это выглядит This looks for predictable problems in your code, and it should uncover the following in main.m.

analyzer-memory-leak.png
Утечка памяти! О нет!

Это небольшой объект, так что утечка не является фатальной. Тем не менее, если бы это случилось снова и снова (например, в длинный цикл или каждый раз, когда пользователь нажал кнопку), программа в конечном итоге запустилась бы из памяти и аварии.

Метод release

Метод  release который поможет нам «отказывается» от собственности объекта путем уменьшения количества ссылок. Так что мы можем избавиться от нашей утечки памяти путем доба

[inventory release];

Теперь наш alloc сбалансирован с  release, статический анализатор не должен вызывать никаких проблем. Типично освобождение объекта слишком рано создает dangling pointer(висящий пойнтер). На пример, попробуйте переместить строку выше до того, как вызвать NSLog() . поскольку release немедленно освобождает основную память,  inventory переменная NSLog()  сейчас указывает неверный адрес, и ваша программа закрешится с a EXC_BAD_ACCESS ошибкой кода, когда вы попытаетесь запустить:

crash-invalid-address.png
Попытка доступа к неверному адресу памяти

Дело в том, что вы не «уступать» право собственности объекта, прежде чем закончиться его использование.

Метод retain

Метод  retain  объявляет право собственности уже существующего объекта. Это как сказать операционной системе “Ей! Мне нужен тот объект тоже, так что не избавляйся от него!” Это полезная возможность, когда другие объекты должны убедиться, что их свойства ссылаются на действительный инстанс.

К примеру, мы используем retain чтоб создать strong reference в нашем inventory масиве. Создадим новый класс и назовем CarStore и изменим его заголовок на следующий.

// CarStore.h
#import <Foundation/Foundation.h>
 
@interface CarStore : NSObject
 
- (NSMutableArray *)inventory;
- (void)setInventory:(NSMutableArray *)newInventory;
 
@end

Это дает возможность вручную дать аксессорам свойства, что называются inventory. Наша первая итерация CarStore.m обеспечивает простую имплементацию getter и setter, вместе с переменной экземпляра, чтоб записать объект:

// CarStore.m
#import "CarStore.h"
 
@implementation CarStore {
    NSMutableArray *_inventory;
}
 
- (NSMutableArray *)inventory {
    return _inventory;
}
 
- (void)setInventory:(NSMutableArray *)newInventory {
    _inventory = newInventory;
}
 
@end

Возвращаясь к main.m, нужно отметить, что inventory переменная CarStore’s inventory будет иметь свойство:

// main.m
#import <Foundation/Foundation.h>
#import "CarStore.h"
 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *inventory = [[NSMutableArray alloc] init];
        [inventory addObject:@"Honda Civic"];
 
        CarStore *superstore = [[CarStore alloc] init];
        [superstore setInventory:inventory];
        [inventory release];
 
        // Do some other stuff...
 
        // Try to access the property later on (error!)
        NSLog(@"%@", [superstore inventory]);
    }
    return 0;
}

Свойство inventory в последней строчке является оборванным пойнтером потому, что объект уже был выпущен ранее в main.m. Прямо сейчас, объект superstore имеет  weak reference в array. Чтоб включить его в сильную ссылку, CarStore нужно объявить право собственности array в ssetInventory: аксессора:

// CarStore.m
- (void)setInventory:(NSMutableArray *)newInventory {
    _inventory = [newInventory retain];
}

Это гарантирует, что объект inventory не будет освобожден , пока superstore его использует. Заметьте, что метод retain возвращает сам объект , который позволяет нам выполнить сохранение и назначение в одной строке.

К сожалению, этот код создает другую проблему: retain не балансирует с  release, в итогу получается очередная утечка памяти. Как только мы даем другое значение setInventory:, мы не можем обратиться к старому значению, что значит, что мы никогда не сможем его освободить. Чтоб это исправить, setInventory: должен вызвать release на старое значение:

// CarStore.m
- (void)setInventory:(NSMutableArray *)newInventory {
    if (_inventory == newInventory) {
        return;
    }
    NSMutableArray *oldValue = _inventory;
    _inventory = [newInventory retain];
    [oldValue release];
}

Это в основном то, что retain и strong атрибуты собственности делают. Очевидно, использование @property гораздо удобнее, чем создавать эти аксессоры самостоятельно.

retain-release-calls.png
Управление памятью вызывает инвентарный объект

Диаграмма выше визуализирует как управление памятью вызывает inventory аррей, что мы создали в  main.m, вместе с их существующими локациями. Как вы можете видеть, все  alloc и  retain балансируют с  release где-то внизу строки, обеспечивая чтоб основная память в конечном итоге будет освобождена.

Метод copy

Альтернативным методом к retain являться copy , что создает новый экзкмпляр объекта и увеличивает количество ссылок на них, оставляя оригинальным и неизменным.Так что, если вы хотите скопировать массив inventory вместо того, чтоб сослаться на изменчивый, вы можете изменить setInventory: на нижеследующий.

// CarStore.m
- (void)setInventory:(NSMutableArray *)newInventory {
    if (_inventory == newInventory) {
        return;
    }
    NSMutableArray *oldValue = _inventory;
    _inventory = [newInventory copy];
    [oldValue release];
}

Также вы можете отозвать copy Attribute , что имеет дополнительные привилегии в замораживании изменяемых коллекций во время назначения. Некоторые классы обеспечивают несколько методов copy  (так же, как и методы init ), и можно с уверенностью предположить, что любой метод, что стартует с copy будет иметь такое же поведение.

Метод autorelease

Методы release и autorelease отказываются от права собственности объекта, но вместо того,чтоб уничтожить объект немедленно, он откладывает фактическое освобождение памяти на позже в программе , Это позволяет вам освободить объекты , когда вы “предполагаете” , пока храните их для использования другими.

На пример, рассмотрите простой factory метод, что создает и возвращает объект CarStore :

// CarStore.h
+ (CarStore *)carStore;

С технической точки зрения, это обязанность метода carStore , чтоб освобождать объект потому, что тот, кто вызывает не имеет способа узнать, что он владеет возвращаемым объектом. Эта имплементация должна возвращать autoreleased объект, как:

// CarStore.m
+ (CarStore *)carStore {
    CarStore *newStore = [[CarStore alloc] init];
    return [newStore autorelease];
}

Будет отказ от права собственности объекта сразу после того, как вы его создали, но будет хранить в памяти достаточно долго для того, кто вызывает, чтоб взаимодействовать с ним. Конкретно, он будет ждать до тех пор, пока ближайший  @autoreleasepool{} заблокирует, и после этого вызовет нормальный метод release. Потому всегда есть функция @autoreleasepool{} , что окружает main() — что дает возможность убедиться, что все autoreleased объекты уничтожаются после того, как программа закончила выполнение. Все эти встроенные factory методы, как NSString’s stringWithFormat:and stringWithContentsOfFile: работают точно таким же образом, как наши методы carStore. До ARC, это была удобная конвенция, так как это позволяла создавать объекты, не беспокоясь о вызывании release где-то по дороге.

Если вы поменяете конструктор  superstore с alloc/init на нижеследующий, вам не нужно будет освобождать его в конце main().

// main.m
CarStore *superstore = [CarStore carStore];

По факту, вам не разрешено освобождать superstore пример сейчас потому, что вы больше не являетесь его владельцем - а carStore factory метод является. Очень важно избегать объекты, что явно выпускают автовыпущениые объекты (в противном случае, вы получите висящий пойнтер и закрешеную программу).

Метод dealloc

Метод объекта dealloc противоположный его методу init . Он вызывается до того, как объект уничтожается, давая шанс очистить любые внутренние объекты. Этот метод вызывается автоматично рантаймом – вам никогда не следует пробовать вызывать dealloc самостоятельно.

В окружающей среде MRR, наиболее распрастраненная вещь которую вам нужно сделать в методе dealloc это освободить объекты, что храняться в переменных экземплярах. Подумайте о том,что случиться с нашим текущем CarStore, когда экземпляр освобождается: его  переменная экземпляра_inventory, что удерживалась сеттером , никогда не будет иметь шанс, чтоб освободиться. Это еще одна форма утечки памяти. Для того,чтоб исправить, все,что нам нужно сделать - это добавить обычный dealloc в CarStore.m:

// CarStore.m
- (void)dealloc {
    [_inventory release];
    [super dealloc];
}

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

MRR выводы

И это ручное управление памятью в двух словах. Главное- это баланс между каждым alloc, retain, и copy с release или autorelease,в противном случае, вы будете сталкиваться с оборванным указателем или утечкой памяти в какой-то момент в вашем приложении.

Запомните, что эта секция используется только в Manual Retain Release чтоб понять внутреннюю работу iOS и OS X управления памятью. В реальном мире, большинство кода, что выше устарело, хотя вы можете столкнуться с ним в старой документации. Очень важно понимать, что утверждение и отказ от права собственности, было полностью заменено Automatic Reference Counting.
Автоматический подсчет ссылок

Теперь, когда вы получили информацию про ручное управление памятью , вы можете забыть об этом. Automatic Reference Counting работает точно таким же образом, как MRR, но автоматически вставляет подходящие методы управления памятью вместо вас. Это большое дело для Objective-C девелоперов , так как это позволяет полностью фокусироваться на том, что нужно сделать их приложению вместо того как это сделать.

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

Для начала, давайте пойдем вперед и снова включим ARC в проектном Build Settings tab. Нужно изменить опцию компилятора Automatic Reference Counting на YES. Еще раз повторю, что это условие по умолчанию для всех шаблонов Xcode, и это то, что вам следует использовать во всех ваших проектах.

turning-on-arc.png
Включение Automatic Reference Counting

Методы без памяти

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

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

New Property Attributes

ARC включает новые атрибуты @property. вам следует использовать атрибут strong на месте  retain, и weak на месте assign. Все эти атрибуты описаны в модуле Properties .

Метод dealloc в ARC

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

По большей части, это значит, что вам никогда не нужно будет включать кастомный метод dealloc. Одно из немногих исключений, когда вы используете низкоуровневые функции распределения памяти, как malloc(). В этом случае, вам все так же нужно вызывать l free() в  dealloc чтоб избежать утечку памяти.

Выводы

По большей части, Automatic Reference Counting позволяет вам полностью забыть о управлении памятью. Идея в том, чтоб сфокусироваться на высокоуровневой функциональности вместо лежащего в основе управления памятью. Единственной вещью, о которой вам стоит беспокоиться это retain cycles, которые были закрыты в модуле Properties .

Если вам нужно более детальное обсуждение про нюансы ARC, посетите пожалуйста Transitioning to ARC Release Notes.

К этому времени, вам следует знать почти все ,что вам нужно знать про Objective-C. Единственная тема, что мы не обсудили это основные типы данных , что предоставлены в обеих фреймворках: C и Foundation. Следующий модуль представляет все стандартные типы, от цифр до стрингов, массивов, словарей и даже дат.