Подтвердить что ты не робот

Существуют ли циклы и методы удобства для пиков памяти с помощью 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 = @"";
    }
}

Здесь, что эти два цикла петли, как в Инструментах, с помощью инструмента Allocations:

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, который выглядит примерно так:

<person>
<firstname>John</firstname>
<lastname>Doe</lastname>
</person>

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

@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: парсер: didStartElement: NamespaceURI: QualifiedName: атрибуты: синтаксический анализатор: foundCharacters: парсер: didStartElement: NamespaceURI: QualifiedName: парсер: didStartElement: NamespaceURI: QualifiedName: атрибуты: синтаксический анализатор: foundCharacters: парсер: didStartElement: NamespaceURI: QualifiedName: парсер: didStartElement: NamespaceURI: QualifiedName: атрибуты: синтаксический анализатор: foundCharacters: парсер: didStartElement: NamespaceURI: QualifiedName: parserDidEndDocument:

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

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

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

Edit:

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

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

Это ничего не фиксирует в ситуации с строками, поскольку 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];
            }
        }
    }

И трассировка Allocations:

NSMutableString is apparently immune to the autorelease pool

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

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

4b9b3361

Ответ 1

Вы загрязняете пул авторефератов тоннами и тоннами автореализованных объектов.

Окружать внутреннюю часть цикла пулом автозапуска:

for (...) {
    @autoreleasepool {
        ... your test code here ....
    }
}

Ответ 2

Пока вы работаете с ошибками, связанными с памятью, вы должны заметить, что @ "и @" Hello" будут бессмертными объектами. Вы можете думать об этом как о const, но о объектах. В течение всего времени будет один и только один экземпляр этого объекта в памяти.

Как отметил @bbum, и вы подтвердили, что @autoreleasepool - это правильный способ справиться с этим в цикле.

В вашем примере с @autoreleasepool и NSMutableString пул на самом деле мало что делает. Единственный смертный объект внутри цикла - ваш mutableCopy из @"", но это будет использоваться только один раз. Другой случай - это просто objc_msgSend для сохраняющегося объекта (NSMutableString), который ссылается только на бессмертный объект и селектор.

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