Objective-C работаем с объектами.. и памятью
Если retain вызывают, значит, это кому-нибудь нужно? (с) Мысли вслух.
NSAutoreleasePool * infoPool = [[NSAutoreleasePool alloc] init];
[[[статья alloc] init] autorelease];
[внимание retain];
Итак, постановка задачи – разобраться с методами объектов в Objective-C, отвечающих за управление объектами в памяти, а именно: alloc, init, retain, release, autorelease, dealloc. На самом деле, init не относится к методам управления памятью, но его мы тоже рассмотрим.
Выделяем память и инициализируем объект
Зачем вообще память нужна? Не знаю, что и кто подумал, но мое мнение – для того, чтобы временно хранить объекты в “легкодоступном” месте. Итак, для создания объекта, первым делом, выделяем под него кусок памяти, и отдаем ссылку на этот кусок памяти. Этим и заимается метод alloc. Далее, обычно, следует вызвать метод init. Так сложилось исторически, так что, я не советую нарушать эту традицию(в свое время я столкнулся с проблемой, когда sqlite-persistenceObjects не работали только потому, что не вызывался этот самый метод init). Необходимо отметить, что метод init есть у любого объекта, который унаследован от NSObject.
[sourcecode language='c']
// Tree.h
@interface Tree : NSObject {
NSString * color;
}
+(Tree *) getAnotherTree;
@end
// Tree.m
@implementation Tree
........
-(id) init {
if ([super init]){
// Наш код инициализации
color = @"Green"; //Все деревья у нас зеленые
}
return self;
}
+(Tree *) getAnotherTree;
Tree * tree = [[Tree alloc] init]; //создаем, и инициализируем объект
return tree; // и возвращаем созданный объект.
//(ВНИМАНИЕ! в этом методе ошибка. Почему? читайте дальше)
}
.......
@end
[/sourcecode]
Освобождаем память
Много кому, после создания объекта и после того, как он уже не нужен, освобождать память, выделенную под объект не сильно хочется (например, тем, кто долго писал на Java). А тут, оказывается надо. Прийдется привыкать – ничего не поделаешь. И если в iPhone-Simulator’е, у которого оперативной памяти ровно столько. сколько у вас в системе, вы можете не заметить как не освободили пару тысяч объектов, то на реальном iPhone, прийдется следить за каждым(если в приложении больше двух строчек кода, конечно). Что-то я отвлекся.Итак, освобождение памяти от лишних объектов.
Логично предположить, что если есть метод alloc, то где-то есть метод dealloc, который делает все наоборот, т.е освобождает память, выделенную под объект. Да, метод такой есть, но вызывать его руками не надо ни в коем случае(хотя, есть одно исключение).
Память освобождается тогда, тогда и только тогда, когда количество ссылок на объект в памяти равно 0(нулю).Логичный вопрос – а как можно изменить количество ссылок на объект в памяти?
Ссылки на объект
Сразу к делу. Вот перечень методов. которые изменяют количество ссылок на объект.
- +(alloc) выделяет память под объект и устанавливает количество ссылок(reference count, или просто rc) rc = 1
- -(copy) выделяет память под объект, и копирует его, устанавливая количество ссылок rc = 1
- -(retain) увеличивает количество ссылок на объект на 1. rc = rc + 1
- -(release) уменьшает количество ссылок на объект на 1. rc = rc – 1. если rc =0, вызывает -(dealloc)
- -(autorelease) Данная функция вызывает -(release) после выхода из цикла событий(т.н. event-loop’a). Т.е. не сразу.
Все эти функции возвращают ссылки в памяти на выделеный объект, так что, при желании, запросто можно записать что-нибудь вроде:
[sourcecode language='c'] NSObject * obj = [[[[[[NSObject alloc] init] retain] release] retain] release]; // Это был абсолютно бесполезный код ;) // А теперь, более полезный и понятный NSObject * obj = [[NSObject alloc] init]; //создали объект rc = 1 NSObject * obj2 = obj; //просто еще один указатель, rc = 1 [obj release]; //rc = 0 память освобождается [obj2 doSomething]; //Вылетаем, т.к. в памяти, куда указывает obj2 уже ничего нету ;( [/sourcecode]
Балансируем
Каждый вызов +(alloc), -(copy) или -(retain) должен быть скомпенсирован вызовом -(release) или -(autorelease). Иначе, у нас будет либо утечка памяти, либо будем вылетать с иключениями о том, что объект был удален из памяти.
Как освобождать память, вроде понятно. Теперь, необходимо понять, когда это необходимо делать, а когда – нет.
Мы в ответе за тех, кого за[alloc]или, с[copy]ровали или за[retain]или
Правилом хорошего тона, необходимостью, обусловленной правилами хорошего тона, или просто необходибмостью есть вызов -(release)/-(autorelease) ТОЛЬКО для тех объектов, для которых МЫ ранее вызывали +(alloc), -(copy) или -(retain). Мы отвечаем за балансировку только своих вызовов, и больше ни за какие другие. Абсолютно неважно, сколько ссылок на объект было до того, как мы его получили.
@property вам в руки!
Если использовать директивы @property, и @synthesize то можно не особо заботиться о балансе – они сами обо все позаботится(ну почти). Пример.
[sourcecode language='c']
// Tree.h
@interface Tree : NSObject {
NSString * retainColor;
NSString * assignColor;
NSString * copyColor;
}
@property(retain) NSString * retainColor;
@property(assign) NSString * assignColor;
@property(copy) NSString * copyColor;
@end
// Tree.m
@implementation Tree
@synthesize retainColor;
/**
при вызове tree.retainColor = value, на самом деле вызывается
[tree setRetainColor:value];
-(void) setRetainColor:(NSString*) value {
if ( retainColor ) [ retainColor release]; // освобождаем старый
retainColor = value;
[retainColor retain]; // захватываем новый
}
*/
@synthesize copyColor;
/**
при вызове tree.copyColor = value, на самом деле вызывается
[tree setCopyColor:value];
-(void) setCopyColor:(NSString*) value {
if ( copyColor ) [ copyColor release]; // освобождаем старый
copyColor = [value copy]; // копируем и захватываем новый
}
*/
@synthesize assignColor;
/**
при вызове tree.assignColor = value, на самом деле вызывается
[tree setAssignColor:value];
-(void) setAssignColor:(NSString*) value {
assignColor = value; // никого не овобождаем и не захватываем.
// тоже баланс, в принципе
}
*/
-(void) dealloc { // Раз нас уничтожают
//[self retain]; <-- не поможет ;) Все равно нас уничтожат
[retainColor release]; // То чистим все объекты, если есть таковые
[copyColor release]; // Кстати, [nil release] ошибки не вызовет
[super dealloc]; // Себя почистили, отдаем управление дальше
// Это, кстати, единственное место. где
// простым смертным позволяется вызвать
// -(dealloc)
}
.......
@end
..............
Tree * tree = [[Tree alloc] init]; //tree rc = 1
NSString * theColor = @"Green"; //theColor rc = 1
// Создание строки ТАКИМ образом, иницииирует rc = 1
tree.retainColor = theColor; // theColor rc = 2
tree.assignColor = theColor; // theColor rc = 2
tree.copyColor = theColor; // theColor rc = 3
[theColor release]; // theColor rc = 2;
[tree release]; // tree rc = 0
// theColor rc = 0;
[/sourcecode]
Release or autorelease? That is the question
Так зачем же, все-таки, нужен -(autorelease) ? Зачем откладывать освобождение памяти, если это можно сделоть прямо здесь и прямо сейчас, вызвав -(release)?
Рассмотрим повнимательнее код, который был показан ранее
[sourcecode language='c']
// Tree.h
@interface Tree : NSObject {
}
+(Tree *) getAnotherTree;
@end
// Tree.m
@implementation Tree
........
// метод должен возвращать ссылку на новое дерево
+(Tree *) getAnotherTree;
Tree * tree = [[Tree alloc] init]; //создаем, и инициализируем объект
return tree; // и возвращаем созданный объект.
// Все еще ошибка.
}
.......
@end
[/sourcecode]
Ошибка заключается в том, что мы создаем объект, вызываем +(alloc) и совсем неясно, где и когда необходимо вызвать -(release) для балансировки. Ведь данный класс, который по сути, представляет собой паттерн Factory, только создает объекты, но в то же время, он должен решать проблему балансировки количества вызовов. Эту проблему можно решить с помощью метода -(autorelease).
[sourcecode language='c'] // метод должен возвращать ссылку на новое дерево +(Tree *) getAnotherTree; Tree * tree = [[Tree alloc] init]; //создаем, и инициализируем объект // [tree release] - вызывать бессмысленно, т.к. объект будет сразу же удален из памяти return [tree autorelease]; // поэтому, освободим его позже } ....... Tree * tree = [Tree getAnotherTree]; // rc = 1(-1) tree.assignColor = @"Green"; // Пока еще вполне рабочий объект NSLog(tree.assignColor); // Но если мы хотим его использовать в программе дальше, // То необходимо его "захватить" [tree retain]; // rc = 2(-1) [/sourcecode]
В результате, после вызова метода getAnotherTree получаем вполне рабочий объект, для которого чуть позже будет вызван -(release).
Как и когда работает -(autorelease) ?
Можно было бы, провести множество аналогий, с тем, как это все работает, но я буду пояснять прямо сходу, как оно есть на примере кода. Вызов метода -(autorelease) , помещает объект, в самый "последний" т. н. autoreleasePool. Он хранит в себе все помеченные, при помощи -(autorelease) метода объекты, и вызывает для них -(release), как только для него самого вызовут метод -(release) или -(drain)(в случае с iPhone/iPod, прнципиальной разницы в этих методах нет). При уничтожении autoreleasePool, у объектов метод -(release) будет вызван ровно столько раз, сколько раз до этого был вызван -(autorelease). Откуда возьмется этот autoreleasePool? Если посмотреть на main.m, который создается по умолчанию в xcode-project то можно увидеть, что, весь запуск приложения заключен в autoreleasePool.
[sourcecode language='c'] // main.m #importint main(int argc, char *argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; // Вот он ^^^^^, родимый int retVal = UIApplicationMain(argc, argv, nil, nil); [pool release]; // А здесь окончательно вызываем release для всех помеченных объектов return retVal; } [/sourcecode]
Глядя на код, можно подумать, что объекты будут освобождены только после полного окончания программы, однако это не так. AutoreleasePool может быть не один, более того, он и есть не один. Каждая обработка событий (event loop), заключается в свой AutoreleasePool
[sourcecode language='c']
.....
- (void) onEventReceived(int argc, char *argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
......................
/// Вот здесь обычно находится наш выполняемый код
............
[pool release]; // А здесь окончательно вызываем release для всех помеченных объектов
return retVal;
}
-(void) multiplePoolsExample {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSObject * obj = [[[NSObject alloc] init] autorelease];
NSAutoreleasePool * pool2 = [[NSAutoreleasePool alloc] init];
NSObject * obj2 = [[[NSObject alloc] init] autorelease];
[pool2 release]; // После этого уже нельзя будет увидеть obj2
[pool release]; // А после этого перестанет быть виден и obj
}
[/sourcecode]
Напоследок я хотел бы показать еще пару строчек кода, чтобы пояснить, как не надо использовать -(autorelease).
[sourcecode language='c']
.....
- (void) secretPlanByAppleDotCom(int argc, char *argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
......................
for (int i =0; i < 6000000000; i++ ){
Person * person = [[[Person alloc] init] autorelease];
person.name = getPersonsNameById(i);
[person buyiPhone]; // :)
}
............
[pool release]; // Если хватит памяти, и мы дойдем до этого места
// То только здесь освободится память,
// выделенная под 6000000000 объектов.
return retVal;
}
[/sourcecode]
Отсюда видно, почему еще не у каждого человека в мире есть iPhone;). Просто не хватило памяти.
А здесь у вас есть последний шанс вызвать [статья retain];






HitOdessit
в 16:44, 22.04.2009Хорошая статья, спасибо.
Последний пример с циклом по айфонам особо понравился :)
Андрей
в 22:22, 04.06.2009Да, статья замечательная! Впринципе, ничего сверхестественного, но на русском языке такую исчерпывающую статью о memory management вижу впервые.
Особенно радует настроение автора :)
УчениГ
в 20:52, 10.10.2009Спасибо большое за статью на русском! Давно хотел прочитать про управление памятью в Xcode, автору респект!
Andis
в 18:51, 25.11.2009tree.copyColor = theColor; // theColor rc = 3
а вот здесь разве reference counter увеличится? Мы же копируем объект, в tree у нас будет своя копия объекта, у которой будет свой reference counter = 1, а rc theColor не изменится.
Или я не правильно понимаю копирование?
Kilew
в 19:38, 25.11.2009@Andis :
Заставил меня задуматься ;)
По ходу, ты прав, наверное ;) Жестко прогнал.
Надо будет завтра чуток подправить.
eastern.hiker
в 21:34, 04.12.2010Отличная статья.
Пожалуй, больше искать статьи на эту тему не буду. Пока хватит. Потом, как будет время, почитаю руководства Эппла о памяти.
Константин
в 13:02, 14.07.2011поправьте в статье:
// assign
property = newValue;
// retain
if (property != newValue) {
[property release];
property = [newValue retain];
}
// copy
if (property != newValue) {
[property release];
property = [newValue copy];
}
Андрей
в 20:26, 05.11.2011вообще ничего не понятно. ну неужели сложно по человечески написать…
Александр
в 23:01, 23.02.2012Хорошая статья! Мало понятной инфы про память) А вот замечания Andis так и не подправил;) Копирование не увеличивает счетчик, а создается новый объект со счетчиком rc=1. И еще заметил один косячек. В первом же примере (ошибка кстати копипастится), строка 18 – +(Tree *) getAnotherTree;. Там вроде как скобка должна быть:
+(Tree *) getAnotherTree
{
Это же описание тела пошло.
Вообще программирую на сях и плюсях давно, поэтому такая работа с памятью немного меня выбила из колеи. ) Но вроде все понятно)
Creative russian user
в 14:55, 10.03.2012Большое спасибо за статью! Давно искал описание параметров retain, assign и copy для @property и генерируемые методы @synthesize. Вы мне очень помогли, благодарю!
Разработка iPhone/iPod touch приложений: Начало работы с Interface Builder < Stanfy Блог
в 9:24, 13.05.2009[...] дальнейшего изучения рекомендую ознакомится с Objective-C работаем с объектами.. и памятью или Objective-C для Java-программистов. Удачи в изучении!!! [...]
Разработка iPhone/iPod touch приложений: Отслеживание утечек памяти < Stanfy Блог
в 12:33, 15.05.2009[...] и инструмент для их обнаружения в xCode. Одна из этих статей была написана в нашей компании (это говорит о том, что [...]