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.


// 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

Освобождаем память

Много кому, после создания объекта и после того, как он уже не нужен, освобождать память, выделенную под объект не сильно хочется (например, тем, кто долго писал на 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). Т.е. не сразу.

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


  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 уже ничего нету ;(

Балансируем

Каждый вызов +(alloc), -(copy) или -(retain) должен быть скомпенсирован вызовом -(release) или -(autorelease). Иначе, у нас будет либо утечка памяти, либо будем вылетать с иключениями о том, что объект был удален из памяти.

Как освобождать память, вроде понятно. Теперь, необходимо понять, когда это необходимо делать, а когда – нет.

Мы в ответе за тех, кого за[alloc]или, с[copy]ровали или за[retain]или

Правилом хорошего тона, необходимостью, обусловленной правилами хорошего тона, или просто необходибмостью есть вызов -(release)/-(autorelease) ТОЛЬКО для тех объектов, для которых МЫ ранее вызывали +(alloc), -(copy) или -(retain). Мы отвечаем за балансировку только своих вызовов, и больше ни за какие другие. Абсолютно неважно, сколько ссылок на объект было до того, как мы его получили.

@property вам в руки!

Если использовать директивы @property, и @synthesize то можно не особо заботиться о балансе – они сами обо все позаботится(ну почти). Пример.


// 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;

 

Release or autorelease? That is the question

Так зачем же, все-таки, нужен -(autorelease) ? Зачем откладывать освобождение памяти, если это можно сделоть прямо здесь и прямо сейчас, вызвав -(release)?

Рассмотрим повнимательнее код, который был показан ранее


// Tree.h
@interface Tree : NSObject {
}
+(Tree *) getAnotherTree;
@end
// Tree.m
@implementation Tree
........

// метод должен возвращать ссылку на новое дерево
+(Tree *) getAnotherTree;
   Tree * tree = [[Tree alloc] init]; //создаем, и инициализируем объект
   return tree;  // и возвращаем созданный объект.
     // Все еще ошибка.
}
.......
@end

Ошибка заключается в том, что мы создаем объект, вызываем +(alloc) и совсем неясно, где и когда необходимо вызвать -(release) для балансировки. Ведь данный класс, который по сути, представляет собой паттерн Factory, только создает объекты, но в то же время, он должен решать проблему балансировки количества вызовов. Эту проблему можно решить с помощью метода -(autorelease).


// метод должен возвращать ссылку на новое дерево
+(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) 

 

В результате, после вызова метода getAnotherTree получаем вполне рабочий объект, для которого чуть позже будет вызван -(release).

Как и когда работает -(autorelease) ?

Можно было бы, провести множество аналогий, с тем, как это все работает, но я буду пояснять прямо сходу, как оно есть на примере кода. Вызов метода -(autorelease) , помещает объект, в самый “последний” т. н. autoreleasePool. Он  хранит в себе все помеченные, при помощи -(autorelease) метода объекты, и вызывает для них  -(release), как только для него  самого вызовут метод -(release) или -(drain)(в случае с iPhone/iPod, прнципиальной разницы в этих методах нет). При уничтожении autoreleasePool, у объектов метод -(release) будет вызван ровно столько раз, сколько раз до этого был вызван  -(autorelease). Откуда возьмется этот autoreleasePool? Если посмотреть на main.m, который создается по умолчанию в xcode-project то можно увидеть, что, весь запуск приложения  заключен в  autoreleasePool.


//  main.m

#import <UIKit/UIKit.h>
int main(int argc, char *argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    //          Вот он ^^^^^, родимый
    int retVal = UIApplicationMain(argc, argv, nil, nil);
    [pool release]; // А здесь окончательно вызываем release для всех помеченных объектов
    return retVal;
}

Глядя на код, можно подумать, что объекты будут освобождены только после полного окончания программы, однако это не так. AutoreleasePool может быть не один, более того, он и есть не один. Каждая обработка событий (event loop), заключается в свой AutoreleasePool


.....
- (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
}

Напоследок я хотел бы показать еще пару строчек кода, чтобы пояснить, как не надо использовать  -(autorelease).


.....
- (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;
}

Отсюда видно, почему еще не у каждого человека в мире есть iPhone;). Просто не хватило памяти.

А здесь у вас есть последний шанс вызвать [статья retain];

[внимание release];

[infoPool release];

Комментарии

HitOdessit

в 16:44, 22.04.2009

Хорошая статья, спасибо.

Последний пример с циклом по айфонам особо понравился :)

Андрей

в 22:22, 04.06.2009

Да, статья замечательная! Впринципе, ничего сверхестественного, но на русском языке такую исчерпывающую статью о memory management вижу впервые.
Особенно радует настроение автора :)

УчениГ

в 20:52, 10.10.2009

Спасибо большое за статью на русском! Давно хотел прочитать про управление памятью в Xcode, автору респект!

Andis

в 18:51, 25.11.2009

tree.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

вообще ничего не понятно. ну неужели сложно по человечески написать…

[...] дальнейшего изучения рекомендую ознакомится с Objective-C работаем с объектами.. и памятью или Objective-C для Java-программистов. Удачи в изучении!!! [...]

[...] и инструмент для их обнаружения в xCode. Одна из этих статей была написана в нашей компании (это говорит о том, что [...]

Оставить комментарий