Я работаю с 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 = @"";
}
}
Вот как эти два цикла зацикливаются в инструментах с запущенным инструментом выделения:
Видеть? Постепенное и постоянное использование памяти, пока не появится целая куча предупреждений о памяти, а затем приложение, естественно, не умрет.
Затем я попробовал что-то немного другое. Я использовал экземпляр NSMutableString
, вот так:
NSMutableString *text;
for (NSInteger i = 0; i < 600000000; i++) {
if (text) {
[text appendString:@" Hello"];
}else{
text = [@"" mutableCopy];
}
}
Этот код работает намного лучше, но все равно дает сбой. Вот как это выглядит:
Затем я попробовал это на меньшем наборе данных, чтобы увидеть, сможет ли какой-либо цикл выдержать наращивание достаточно долго, чтобы завершиться. Вот версия NSString
:
NSString *text;
for (NSInteger i = 0; i < 1000000; i++) {
if (text) {
text = [text stringByAppendingString:@" Hello"];
}else{
text = @"";
}
}
Он также дает сбой, и результирующий граф памяти похож на первый, сгенерированный с использованием этого кода :
. При использовании NSMutableString
тот же цикл из миллиона -итераций не только завершается успешно, но и выполняется за гораздо меньшее время.Вот код:
NSMutableString *text;
for (NSInteger i = 0; i < 1000000; i++) {
if (text) {
[text appendString:@" Hello"];
}else{
text = [@"" mutableCopy];
}
}
И посмотрите на график использования памяти:
Короткий всплеск в начале — это использование памяти, вызванное циклом. Помните, я отметил тот, казалось бы, несущественный факт, что экран черный во время обработки цикла, потому что я запускаю его в 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 выглядит так:
Я замечаю постепенное наращивание памяти с течением времени, но это около 150 килобайт, а не 350 мегабайт, как было раньше. Однако этот код с использованием NSMutableString
ведет себя так же, как и без пула автоматического освобождения :
NSMutableString *text;
for (NSInteger i = 0; i < 600000000; i++) {
@autoreleasepool {
if (text) {
[text appendString:@" Hello"];
}else{
text = [@"" mutableCopy];
}
}
}
. И трассировка распределений:
Похоже, что NSMutableString явно невосприимчив к пулу автоматического выпуска. Я не уверен, почему, но на первый взгляд, я бы связал это с тем, что мы видели ранее, что NSMutableString
может обрабатывать около миллиона итераций самостоятельно, тогда как NSString
не может.
Итак, каков правильный способ решения этой проблемы?