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