Очередь AFHTTPRequestOperations, создающая наращивание памяти
Я только что обновился до AFNetworking 2.0, и я переписываю свой код, чтобы загрузить данные и вставить их в Core Data.
Я загружаю файлы данных JSON (где-нибудь из файлов размером 10-200 МБ), записываю их на диск, а затем передаю их фоновым потокам для обработки данных. Ниже приведен код, который загружает JSON и записывает его на диск. Если я просто позволю этому запускаться (даже не обрабатывая данные), приложение использует память до тех пор, пока оно не будет уничтожено.
Я предполагаю, что когда данные поступают, они сохраняются в памяти, но как только я сохраняю их на диск, почему они остаются в памяти? Разве пул авторелизов не должен позаботиться об этом? Я также установил responseData и downloadData в ноль. Есть ли что-то явно очевидное, что я здесь делаю не так?
@autoreleasepool
{
for(int i = 1; i <= totalPages; i++)
{
NSString *path = ....
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:path]];
AFHTTPRequestOperation *op = [[AFHTTPRequestOperation alloc] initWithRequest:request];
op.responseSerializer =[AFJSONResponseSerializer serializer];
[op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject)
{
//convert dictionary to data
NSData *downloadData = [NSKeyedArchiver archivedDataWithRootObject:responseObject];
//save to disk
NSError *saveError = nil;
if (![fileManager fileExistsAtPath:targetPath isDirectory:false])
{
[downloadData writeToFile:targetPath options:NSDataWritingAtomic error:&saveError];
if (saveError != nil)
{
NSLog(@"Download save failed! Error: %@", [saveError description]);
}
}
responseObject = nil;
downloadData = nil;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
DLog(@"Error: %@", error);
}];
}
[mutableOperations addObject:op];
}
NSArray *operations = [AFURLConnectionOperation batchOfRequestOperations:mutableOperations progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) {
DLog(@"%lu of %lu complete", (unsigned long)numberOfFinishedOperations, (unsigned long)totalNumberOfOperations);
} completionBlock:^(NSArray *operations) {
DLog(@"All operations in batch complete");
}];
mutableOperations = nil;
[manager.operationQueue addOperations:operations waitUntilFinished:NO];
Спасибо!
РЕДАКТИРОВАТЬ #1 Добавление @autoreleasepool
в моем полном блоке, казалось, немного замедлилось использование памяти, но он все еще накапливается и в конечном итоге вылетает приложение.
1 ответ
Если ваши файлы JSON действительно имеют размер 10-200 МБ каждый, это определенно вызовет проблемы с памятью, потому что такого рода запросы будут загружать ответы в память (а не передавать их в постоянное хранилище). Хуже того, потому что вы используете JSON, я думаю, что проблема в два раза хуже, потому что вы собираетесь загружать это в словарь / массив, который также занимает память. Таким образом, если у вас происходит четыре загрузки по 100 МБ, пиковое использование памяти может составить порядка 800 МБ (100 МБ для NSData
плюс ~100 МБ для массива / словаря (возможно, намного больше), четыре раза для четырех одновременных запросов). Вы можете быстро исчерпать память.
Итак, пара реакций:
При работе с этим объемом данных вы захотите использовать потоковый интерфейс (
NSURLConnection
или жеNSURLSessionDataTask
где вы записываете данные по мере их поступления, а не удерживаете их в памяти; или использоватьNSURLSessionDownloadTask
который делает это для вас), тот, который записывает данные непосредственно в постоянное хранилище (вместо того, чтобы пытаться сохранить его вNSData
в оперативной памяти, как это загружается).Если вы используете
NSURLSessionDownloadTask
это действительно просто. Если вам нужно поддерживать версии iOS до 7.0, я не уверен, поддерживает ли AFNetworking потоковую передачу ответов непосредственно в постоянное хранилище. Держу пари, что вы могли бы написать свой собственный сериализатор ответов, который делает это, но я не пробовал. Я всегда писал свой собственныйNSURLConnectionDataDelegate
методы, которые загружаются непосредственно в постоянное хранилище (например, что-то вроде этого).Вы можете не захотеть использовать JSON для этого (потому что
NSJSONSerialization
загрузит весь ресурс в память, а затем проанализирует егоNSArray
/NSDictionary
(также в памяти), но лучше использовать формат, который поддается потоковому анализу ответа (например, XML), и напишите синтаксический анализатор, который сохраняет данные в вашем хранилище данных (Core Data или SQLite) по мере их анализа, а не пытается загрузить все это в оперативной памяти.Обратите внимание, даже
NSXMLParser
на удивление неэффективна память (см. этот вопрос). В примере XMLPerformance Apple демонстрирует, как вы можете использовать более громоздкий LibXML2, чтобы минимизировать использование памяти вашего анализатора XML.Кстати, я не знаю, включает ли ваш JSON какие-либо двоичные данные, которые вы закодировали (например, base 64 или тому подобное), но если это так, вы можете рассмотреть двоичный формат передачи, который не должен делать это преобразование. Использование base-64 или uuencode или чего-либо еще может увеличить ваши требования к пропускной способности и памяти. (Если вы не имеете дело с двоичными данными, которые были закодированы, игнорируйте этот пункт.)
Кроме того, вы можете использовать Reachability для подтверждения типа подключения пользователя (Wi-Fi против сотовой связи), потому что считается плохой формой загружать такое количество данных по сотовой сети (по крайней мере, без разрешения пользователя), не только из-за скорости проблемы, но и риск использования чрезмерной части ежемесячного тарифного плана их перевозчика. Я даже слышал, что Apple исторически отклоняла приложения, которые пытались загружать слишком много данных по сотовой связи.