Управление памятью Objective-C, парсер XML и другие нетривиальные примеры - PullRequest
7 голосов
/ 13 февраля 2009

Я знаю основные принципы управления памятью (счет сохранения, пулы авто-выпуска и т. Д.) В Какао, но как только вы пройдете простое сохранение / освобождение, это станет немного более запутанным. Я не смог найти достойных ответов на них, так как большинство уроков посвящено простым сценариям. Я хотел бы спросить о передовых методах написания кода и предотвращения утечек.


1-й вопрос - итерации и временные задания:

for (id object in objectArray) {    
     Model *currentItem = object;
    /* do something with currentItem */
    [currentItem release];
}

Если я удалю релиз в последней строке, код будет работать нормально, но есть утечка. Какое правило здесь? Объект уже есть в objectArray. Я назначаю это непосредственно, чтобы получить тип. Должен ли я сделать это другим способом? Увеличивает ли это назначение retainCount of currentItem? (это что-то вроде [[alloc] initWithObject]?) Как узнать, является ли это назначение (объект) автоматически выпущенным или нет?


2-й вопрос - мгновенно сохраняет:

Model *model = [unarchiver decodeObjectForKey:@"ARCHIVED_MODEL_OBJECT"];
// it has to be here, because (I was told) unarchiver will return autorelease object    
[model retain]; 
label.text = model.data;

Как кто-то знал, что этот конкретный метод работает так странно, что мне нужно немедленно вызвать retain для возвращаемого значения, или я столкнусь с нулем при следующем назначении? Я не мог найти ничего подобного в документации. В соответствии с правилами сохранения / выпуска, я ожидал бы, что decodeObjectForKey вернет автоматически перемещенный объект, но потребуется некоторое время, пока элемент управления вернется в приложение и пул потребует освобождения объекта модели. Есть ли какое-то правило для этого? Как мне их искать?


3-й вопрос - автоматическое освобождение и передача переменных:

- (IBAction) loadXMLButtonClicked:(id) sender {
    objectArray = [self loadData]; // 1 - objectArray is instance var
    NSArray *objectArray = [self loadData]; // 2 - objectArray is local var

    // loadXMLButtonClicked is called on button click, here the method finishes
    // and control goes back to application, autorelease pool is cleaned?

    // case 1 - objectArray stays retained in instance variable? (because setter was used)
    // case 2 - objectArray is soon to be released, there were no retains? 
    // (ignore the fact that it's local var, just hypothetically)

}

- (NSArray *) loadData {
    NSArray *objectArray = [[NSArray alloc] init];
    // populate array here
    return [objectArray autorelease];
}

4-й вопрос - (терпите меня, последний) наилучшая практика парсера xml: (Я не хочу использовать другие решения, использование стандартного синтаксического анализатора - это практическое использование управления памятью и потоковой обработки).

В основном этот код здесь работает, работает хорошо и не имеет утечек, но я не знаю, является ли это правильным подходом. У меня есть отдельный объект, который действует как парсер, анализирует XML для сбора массива объектов типа Model. Теперь, после завершения анализа, внешне я хотел бы получить этот массив, хотя я не знаю как (копирование массива и выпуск всего парсера - хорошая идея?). Пожалуйста, пройдите по коду и посмотрите комментарии.

Я много раз отлаживал этот код, используя gdb для печати retainCounts, зомби и т. Д., И хотя мне удается запустить его без утечек, я не знаю на 100%, почему и хотел бы услышать хорошая аргументация, как это должно быть сделано с объяснением. Это будет высоко ценится.

Controller.m

- (NSArray *) loadData {
(...)
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
ModelXMLParser *parserDelegate = [[ModelXMLParser alloc] init];
[parser setDelegate:parserDelegate];
[parser parse];

objectArray = [[parserDelegate objectArray] copy]; 
// is this ok? *i* don't need the parser object so I think I should get rid of it
// and copy the data. How this copy works, is it shallow (only new reference to array)
// or deep copy (objects allocated again as well)?
// how to do deep copy of NSArray?

[parserDelegate release];
[parser release];
}

ModelXMLParser.m (упрощенно)

@implementation ModelXMLParser

@synthesize objectArray; // array of objects
@synthesize currentObject; // temporary object
@synthesize currentChars; // temporary chars
@synthesize parseChars; // parse chars only when there's need, leave those /t/n etc

- parser didStartElement (...) {
    if ([elementName isEqualToString:@"objects"]) {
        objectArray = [[NSMutableArray alloc] init];
    }
    else if ([elementName isEqualToString:@"object"]) {
        currentObject = [[Model alloc] init];
    }
    else if ([elementName isEqualToString:@"name"]) {
        // do I have to init currentObject.name (NSString) here? I guess not
        [self setParseChars:YES]; // just set the flag to make parse control easier
    }
    else if ([elementName isEqualToString:@"number"]) {
        // int isn't object anyway, no init
        [self setParseChars:YES]; // just set the flag to make parse control easier
    }   
}

- parser foundCharacters (...) {
    if (parseChars) {
        currentChars = [[NSString alloc] initWithString:string];
        // why is currentChars retainCount = 2 here?
        // is it like currentChars = [NSString new] and then currentChars = string? (so retain once more)
        // is it good way to control parser? (please ignore the NSMutableString and appending example, try this one)
        // should I just do currentChars = string here?

        [currentChars autorelease]; // this is currently my solution, because there's no leak, but I feel it's incorrect
    }
}

-  parser didEndElement (...) {
    if ([elementName isEqualToString:@"object"]) {
        [objectArray addObject:[currentObject copy]]; // should I copy here or just addObject, it retains anyway?
        [currentObject release]; // I've initialized currentObject before, now I don't need it, so I guess retainCount goes to 0 here?
    }
    else if ([elementName isEqualToString:@"name"]) {
        currentObject.name = currentChars; // is this correct, or shoud I do [currentChars copy] as well?
        [self setParseChars:NO];
        [currentChars release]; // as before, initialized, now releasing, but is this really correct?
    }
    else if ([elementName isEqualToString:@"number"]) {
        currentObject.number = [currentChars intValue]; // is this correct, or shoud I do [currentChars copy] as well?
        [self setParseChars:NO];
        [currentChars release]; // as before, initialized, now releasing, but is this really correct?
    }   
}

- (void) dealloc {
    // I shouldn't release currentChars or currentObject, those (I suppose) should be freed after parsing done,
    // as a result of earlier releases?

    [objectArray release];
    [super dealloc];
}

Model.m

@implementation Model

@synthesize name; // this is NSString
@synthesize number; // this is int

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

- (void) dealloc {
    [name release];
    // I don't have to release int, right? it's not an object
    [super dealloc];
}

Меня особенно смущает вопрос 4. Извините за, возможно, слишком длинный вопрос, но на самом деле речь идет об одной теме и более глубоком ее понимании.

Ответы [ 6 ]

7 голосов
/ 13 февраля 2009

1-й вопрос - итерации и временные задания

Вы НЕ должны освобождать объекты здесь, так как вы не являетесь владельцем. Вы должны когда-либо освобождать объект, только если вы являетесь его владельцем. См. Руководство по управлению памятью для какао . Вы являетесь владельцем объекта только в том случае, если вы вызываете метод, имя которого начинается с init, new или содержит copy в своем имени.

Поскольку цикл for не использует методы ни с одним из этих имен, вы не являетесь владельцем, поэтому НЕ ДОЛЖНЫ освобождать объекты. Это приведет к освобождению объектов до того, как они будут выполнены, что почти наверняка приведет к повреждению памяти и падению.

2-й вопрос - мгновенно сохраняет:

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

Поскольку decodeObjectForKey не начинается с init или new или не содержит copy в своем названии, вы не становитесь владельцем. Звонок retain делает вас владельцем.

3-й вопрос - автоматическое освобождение и передача переменных:

Прежде всего, это плохая практика - скрывать члена класса с локальной переменной с тем же именем. Во-вторых, если loadData не используется в качестве многоцелевой служебной функции (что, я полагаю, не так, поскольку она не принимает никаких параметров), он должен просто присвоить результат непосредственно переменной-члену objectArray , Бессмысленно и подвержено ошибкам возвращать результат, а затем назначать вызывающую функцию переменной-члену.

В-третьих, вы не используете установщик свойства objectArray - вы просто присваиваете переменную-член прямо. Если вы хотите использовать сеттер, вы должны явно сказать self.objectArray = ...self. перед ним). Таким образом, objectArray никогда не сохраняется, поэтому он будет отменен в следующий раз, когда пул авто-релиза будет очищен, а это не то, что вам нужно. Вы должны сохранить его в какой-то момент или, наоборот, просто не высвобождать его автоматически в конце loadData и присвоить ему переменную члена класса objectArray.

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

4-й вопрос - лучшие практики парсера xml:

Вы делаете поверхностную копию массива объектов. Если вы хотите сделать глубокую копию, вы можете использовать сообщение initWithArray:copyItems:.

мне нужно инициализировать currentObject.name (NSString) здесь? наверное нет?

Я не понимаю этого вопроса, нигде рядом не упоминается currentObject.name этого кода.

почему currentChars retainCount = 2 здесь?

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

на данный момент это мое решение, потому что утечки нет, но я чувствую, что это неправильно?

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

я должен скопировать сюда или просто добавить объект, он все равно сохраняется?

Просто addObject: когда вы добавляете элементы в NSArray, NSSet или NSDictionary, они автоматически retain редактируются структурой данных. Когда вы удаляете их, они release d.

На большинство остальных вопросов можно ответить, просто следуя правилам, или получить идентичные ответы на некоторые из предыдущих вопросов, на которые я уже ответил.

1 голос
/ 13 февраля 2009

@ 1-й вопрос

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

См. Здесь для некоторых правил

0 голосов
/ 02 октября 2009

Статический анализатор

В дополнение к: Руководство по программированию управления памятью для какао , Static Analyzer является незаменимым инструментом.

Проект-> Настройки проекта-> Сборка-> Параметры сборки-> Запустить статический анализатор

Убедитесь, что он отмечен галочкой.

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

Я много раз читал принципы управления памятью, но не понял, пока не воспользовался Static Analyzer.

Теперь я лучше справляюсь с этим и большую часть времени правильно понимаю. Статический анализатор, однако, по-прежнему важен, поскольку он указывает на ошибки и пропущенные.

Йоши

0 голосов
/ 16 февраля 2009

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

Я отмечаю ответ Адама как лучший, как наиболее подробный, хотя и не отвечаю на все мои проблемы.

Для других - http://developer.apple.com/documentation/Cocoa/Conceptual/MemoryMgmt/MemoryMgmt.html - отличное чтение. Также читайте мои собственные ответы на вопросы:

1 - Конечно, я не должен выпускать этот объект. Утечка памяти не была вызвана этим.

2 - Этот метод не является чем-то особенным, он просто возвращает простой автоматически выпущенный объект, как другие пытались объяснить мне здесь. Корень моих начальных проблем заключался в том, что у меня раньше не было этого сохранения, но вскоре я вызвал [release release], что вызвало пул авто-релиза, отправляющий выпуск несуществующему объекту. Я не должен делать [выпуск модели], потому что я не владелец этого объекта. На самом деле, это сохранение здесь даже не нужно, потому что мне просто нужен объект, чтобы получить значение из него, а затем я могу перебрать его, чтобы его можно было безопасно передать в пул авто-релиза без сохранения.

3 - Я хотел, чтобы этот метод (loadData) был независимым, поэтому не устанавливал переменные экземпляра, а возвращал массив для других. Это был параллельный пример, не то чтобы у меня в методе были две переменные с одинаковым именем.

Если я объявляю объект в этом методе (ситуация # 2), то просто так получается, что он автоматически освобождается в конце этого метода, потому что после его завершения элемент управления возвращается к приложению и освобождает пул. хорошо со мной в этом примере, потому что мне не нужен массив позже. В реальном мире у меня, вероятно, должна быть переменная экземпляра (ситуация # 1), а затем идти с self.objectArray = [self loadData], потому что это запустит установщик, и объект с автоматически выпущенным объектом будет сохранен для меня здесь.

4 - Я кое-что перепутал здесь. По сути, я пытался написать код objecive-c с ручным управлением памятью, но все еще придерживался позиции «сборщика мусора». Очень важно помнить, что если вы выполняете [[alloc alloc] init], а затем [освобождение объекта] - это не обязательно означает, что объект будет уничтожен! Релиз не отменяет! Это, конечно, фундаментальное правило (сохранить / освободить), но даже зная его, его легко забыть. Следите за тем, что вы делаете со своим объектом, между этими двумя линиями - объект может действительно прожить очень долго, потому что «кто-то» станет его владельцем. Метод release в конце этого класса livecycle не означает - объект теперь уничтожен, но означает: «Мне уже все равно, мои руки чисты» *

Итак, строка за строкой:

objectArray = [[parserDelegate objectArray] copy]; 

Это прекрасно, я не копирую глубоко. Я копирую массив, что означает, что он выделяет новую память для объекта массива, но не для содержимого. НО, копия, отправленная в objectArray, также отправляет сохранение каждому объекту. В моем примере я выпускаю свой parserDelegate, который также выпускает свой собственный objectArray, уменьшая retainCount для каждого объекта. Если я не сделаю копию здесь, объекты достигнут retainCount = 0 и будут уничтожены. Таким образом, у меня есть новый массив с указателями на старые объекты, но они по сути становятся моими объектами, потому что предыдущий массив уничтожен, и из-за моего сохранения я становлюсь владельцем. Извините, если это говорит слишком много, но я действительно должен был сосредоточиться, чтобы понять это.

else if ([elementName isEqualToString:@"name"]) {
    // do i have to init currentObject.name (NSString) here? i guess not?
    [self setParseChars:YES]; // just set the flag to make parse control easier
}

Вопрос здесь заключался в том, следует ли мне инициализировать свойство currentObject.name NSString, потому что оно будет заполнено вскоре после запуска foundCharacters. Теперь это интересно. Когда вы инициализируете весь объект, его свойства NSString равны нулю. Теперь, позже, я делаю

currentObject.name = currentChars;

Который запускает сеттер. Этот установщик определен как (неатомный, сохраняемый), что означает, что новое значение сохраняется, старое значение освобождается и указатель назначается. Как ни странно, не имеет значения, инициализировано ли предыдущее значение или оно равно нулю - если оно инициализировано, оно все равно будет выпущено, если оно равно нулю, то ноль все еще может принять выпуск (хотя я не уверен на 100% ?) - ничего не случится. Но для корректности, я думаю, начальная строка должна выглядеть так:

else if ([elementName isEqualToString:@"name"]) {
    currentObject.name = [NSString new]; // just alloc/init routine
    [self setParseChars:YES]; // just set the flag to make parse control easier
}

Сейчас:

[currentChars autorelease];

Не должно быть там. Это сбивает с толку дизайн, и это было плохое решение. Все, что должно быть там, это просто строка init.

[objectArray addObject:[currentObject copy]];

Копирование здесь не требуется. addObject сохранит в любом случае. Нет смысла создавать другое распределение. Это было моей одной из причин моих утечек.

Все остальное в порядке. Поскольку я освобождаю currentChars сразу после установки значения для моего объекта, он теперь сохраняет его и становится владельцем, а я просто освобождаю его «здесь» в синтаксическом анализаторе, потому что он мне больше не нужен (это будет выделено в следующем цикле).

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

0 голосов
/ 13 февраля 2009

@ 3-ий вопрос

Строка "objectArray = [self loadData]; // 1 - objectArray is instance var" на самом деле не является установщиком, поскольку она напрямую обращается к переменной экземпляра. Чтобы использовать сеттер, нужно получить к нему доступ, хотя self

self.objectArray = [self loadData];

... и если ваше свойство объявлено как (nonatomic, copy), старый objectArray будет освобожден, а новый будет создан с копией, таким образом, сохранится.

0 голосов
/ 13 февраля 2009

@ 2-й вопрос

Похоже, вы устанавливаете свойство текста UILabel, которое в этом случае использует copy . В документации сказано:

@property(nonatomic, copy) NSString *text;

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...