Вызывают ли циклы и удобные методы пики памяти при использовании ARC?

Я работаю с ARC и вижу странное поведение при изменении строк в цикле.

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

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

Для простоты вот простой цикл, который я вставил в пустое приложение с одним -представлением. Я вставил этот код прямо в метод viewDidLoad. Он запускается до появления представления, поэтому экран остается черным, пока цикл не завершится.

NSString *text;

for (NSInteger i = 0; i < 600000000; i++) {

    NSString *newText = [text stringByAppendingString:@" Hello"];

    if (text) {
        text = newText;
    }else{
        text = @"";
    }
}

Следующий код также потребляет память до тех пор, пока цикл не завершится:

NSString *text;

for (NSInteger i = 0; i < 600000000; i++) {

    if (text) {
        text = [text stringByAppendingString:@" Hello"];
    }else{
        text = @"";
    }
}

Вот как эти два цикла зацикливаются в инструментах с запущенным инструментом выделения:

Instruments profiling repeating string manipulation

Видеть? Постепенное и постоянное использование памяти, пока не появится целая куча предупреждений о памяти, а затем приложение, естественно, не умрет.

Затем я попробовал что-то немного другое. Я использовал экземпляр NSMutableString, вот так:

NSMutableString *text;

for (NSInteger i = 0; i < 600000000; i++) {

    if (text) {
        [text appendString:@" Hello"];
    }else{
        text = [@"" mutableCopy];
    }
}

Этот код работает намного лучше, но все равно дает сбой. Вот как это выглядит:

NSMutableStrings being profiles instead

Затем я попробовал это на меньшем наборе данных, чтобы увидеть, сможет ли какой-либо цикл выдержать наращивание достаточно долго, чтобы завершиться. Вот версия NSString:

NSString *text;

for (NSInteger i = 0; i < 1000000; i++) {

    if (text) {
        text = [text stringByAppendingString:@" Hello"];
    }else{
        text = @"";
    }
}

Он также дает сбой, и результирующий граф памяти похож на первый, сгенерированный с использованием этого кода :

NSString crashes again

. При использовании NSMutableStringтот же цикл из миллиона -итераций не только завершается успешно, но и выполняется за гораздо меньшее время.Вот код:

NSMutableString *text;

for (NSInteger i = 0; i < 1000000; i++) {

    if (text) {
        [text appendString:@" Hello"];
    }else{
        text = [@"" mutableCopy];
    }
}

И посмотрите на график использования памяти:

NSMutableStrings seem to work with smaller datasets

Короткий всплеск в начале — это использование памяти, вызванное циклом. Помните, я отметил тот, казалось бы, несущественный факт, что экран черный во время обработки цикла, потому что я запускаю его в viewDidLoad? Сразу после этого всплеска появляется вид. Таким образом, оказывается, что в этом сценарии NSMutableStrings не только более эффективно обрабатывают память, но и намного быстрее. Очаровательный.

Теперь вернемся к моему реальному сценарию... Я использую NSXMLParserдля анализа результатов вызова API. Я создал объекты Objective -C, чтобы они соответствовали моей структуре ответа XML. Итак, рассмотрим, например, ответ XML, который выглядит примерно так:


John
Doe

Мой объект будет выглядеть так:

@interface Person : NSObject

@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;

@end

Теперь, в моем делегате NSXMLParser, я бы просмотрел свой XML и отследил бы текущий элемент (Мне не нужно представление полной иерархии, так как мои данные довольно плоские, это дамп базы данных MSSQL как XML ), а затем в методе foundCharactersя запускал что-то вроде этого:

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
  if((currentProperty is EqualToString:@"firstname"]){
    self.workingPerson.firstname = [self.workingPerson.firstname stringByAppendingString:string]; 
  }
}

Этот код очень похож на первый код. Я эффективно перебираю XML, используя NSXMLParser, поэтому, если бы я регистрировал все вызовы моих методов, я бы увидел что-то вроде этого:

parserDidStartDocument: parser:didStartElement:namespaceURI:qualifiedName:attributes: parser:foundCharacters: parser:didStartElement:namespaceURI:qualifiedName: parser:didStartElement:namespaceURI:qualifiedName:attributes: parser:foundCharacters: parser:didStartElement:namespaceURI:qualifiedName: parser:didStartElement:namespaceURI:qualifiedName:attributes: parser:foundCharacters: parser:didStartElement:namespaceURI:qualifiedName: parserDidEndDocument:

Видишь узор? Это петля. Обратите внимание, что возможно также несколько последовательных вызовов parser:foundCharacters:, поэтому мы добавляем свойство к предыдущим значениям.

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

В общем, есть ли способ преодолеть это накопление памяти при циклическом просмотре строк с использованием ARC? Есть ли что-то конкретное для NSXMLParser, что я могу сделать?

Изменить:

Первоначальные тесты показывают, что даже использование второго @autoreleasepool{...}не решает проблему.

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

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

(Обратите внимание, что я называю это пиком памяти, потому что теоретически ARC в какой-то момент очистит память, но только после того, как она достигнет пика. На самом деле ничего не течет, но эффект тот же.)

Редактировать 2:

Закрепление пула автоматического освобождения внутри цикла имеет некоторые интересные эффекты. Кажется, это почти уменьшает накопление при добавлении к объекту NSString:

NSString *text;

for (NSInteger i = 0; i < 600000000; i++) {

        @autoreleasepool {
            if (text) {
                text = [text stringByAppendingString:@" Hello"];
            }else{
                text = [@"" mutableCopy];
            }
        }
    }

Трассировка Allocations выглядит так:

enter image description here

Я замечаю постепенное наращивание памяти с течением времени, но это около 150 килобайт, а не 350 мегабайт, как было раньше. Однако этот код с использованием NSMutableStringведет себя так же, как и без пула автоматического освобождения :

NSMutableString *text;

for (NSInteger i = 0; i < 600000000; i++) {

        @autoreleasepool {
            if (text) {
                [text appendString:@" Hello"];
            }else{
                text = [@"" mutableCopy];
            }
        }
    }

. И трассировка распределений:

NSMutableString is apparently immune to the autorelease pool

Похоже, что NSMutableString явно невосприимчив к пулу автоматического выпуска. Я не уверен, почему, но на первый взгляд, я бы связал это с тем, что мы видели ранее, что NSMutableStringможет обрабатывать около миллиона итераций самостоятельно, тогда как NSStringне может.

Итак, каков правильный способ решения этой проблемы?

13
задан Moshe 9 August 2012 в 19:09
поделиться