Проблема нехватки памяти при загрузке нескольких файлов с помощью AFNetworking

В моем приложении я пытаюсь загрузить тысячи изображений (каждое изображение размером не более 3 МБ) и 10 видео (каждое видео размером не более 100 МБ) и сохранить их в каталоге документов.

Для этого я использую AFNetworking.

Моя проблема заключается в том, что я успешно получаю все данные, когда использую медленный Wi-Fi (около 4 Мбит/с), но такая же загрузка происходит, если я использую Wi-Fi со скоростью 100 Мбит/с приложение получает предупреждение о нехватке памяти при загрузке изображений и проблему нехватки памяти при загрузке видео, а затем происходит сбой приложения.

-(void) AddVideoIntoDocument :(NSString *)name :(NSString *)urlAddress{

    NSMutableURLRequest *theRequest=[NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlAddress]];
    [theRequest setTimeoutInterval:1000.0];

    AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:theRequest];

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *path = [[paths objectAtIndex:0] stringByAppendingPathComponent:name];
    operation.outputStream = [NSOutputStream outputStreamToFileAtPath:path append:NO];

    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
        NSLog(@"Successfully downloaded file to %@", path);
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"Error: %@", error);
    }];
    [operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {

        //NSLog(@"Download = %f", (float)totalBytesRead / totalBytesExpectedToRead);

    }];
    [operation start];
}

-(void)downloadRequestedImage : (NSString *)imageURL :(NSInteger) type :(NSString *)imgName{

    NSMutableURLRequest *theRequest=[NSMutableURLRequest requestWithURL:[NSURL URLWithString:imageURL]];
    [theRequest setTimeoutInterval:10000.0];
    AFHTTPRequestOperation *posterOperation = [[AFHTTPRequestOperation alloc] initWithRequest:theRequest];
    posterOperation.responseSerializer = [AFImageResponseSerializer serializer];
    [posterOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
        //NSLog(@"Response: %@", responseObject);

        UIImage *secImg = responseObject;
        if(type == 1) { // Delete the image from DB
            [self removeImage:imgName];
        }
        [self AddImageIntoDocument:secImg :imgName];
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"Image request failed with error: %@", error);
    }];

    [posterOperation start];
}

Приведенный выше код я зацикливаю в соответствии с количеством видео и изображений, которые мне нужно загрузить

В чем причина такого поведения

У меня даже есть скриншоты распределения памяти для обоих сценариев.

Пожалуйста помоги

Также добавлен код для сохранения загруженных изображений

-(void)AddImageIntoDocument :(UIImage *)img :(NSString *)str{

    if(img) {
        NSData *pngData = UIImageJPEGRepresentation(img, 0.4);
        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

        NSString *filePathName =[[paths objectAtIndex:0]stringByAppendingPathComponent:str];
        [pngData writeToFile:filePathName atomically:YES];
    }
    else {
        NSLog(@"Network Error while downloading the image!!! Please try again.");
    }
}

person kumar    schedule 22.10.2014    source источник


Ответы (2)


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

Вы можете смягчить это, контролируя пиковое использование памяти, не загружая эти загрузки в память. При загрузке больших файлов часто лучше передавать их напрямую в постоянное хранилище. Чтобы сделать это с помощью AFNetworking, вы можете установить outputStream из AFURLConnectionOperation, и он должен передавать содержимое непосредственно в этот файл, например.

AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];

NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *path          = [documentsPath stringByAppendingPathComponent:[url lastPathComponent]]; // use whatever path is appropriate for your app

operation.outputStream = [[NSOutputStream alloc] initToFileAtPath:path append:NO];

[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
    NSLog(@"successful");
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    NSLog(@"failure: %@", error);
}];

[self.downloadQueue addOperation:operation];

Кстати, вы заметите, что я не просто звоню start по этим запросам. Лично я всегда добавляю их в очередь, для которой я указал максимальное количество одновременных операций:

self.downloadQueue = [[NSOperationQueue alloc] init];
self.downloadQueue.maxConcurrentOperationCount = 4;
self.downloadQueue.name = @"com.domain.app.downloadQueue";

Я думаю, что это менее критично в отношении использования памяти, чем потоковая передача результатов непосредственно в outputStream с использованием постоянного хранилища, но я считаю, что это еще один механизм управления системными ресурсами при инициировании множества одновременных запросов.

person Rob    schedule 22.10.2014
comment
в чем разница между outputStreamToFileAtPath и initToFileAtPath.. во время загрузки и записи в постоянную строку я не нашел никакой разницы - person kumar; 10.12.2014
comment
Вы на 100% правы в том, что нет никакой разницы (по крайней мере, существенной здесь) в отношении этих двух техник. Но вы используете подход с выходным потоком в AddVideoIntoDocument (но, кстати, вы ничего не делаете с ним, когда он готов), но в downloadRequestedImage вы загружаете изображения в память, а не передаете их в постоянное хранилище. . Признаюсь, я не заметил, что первый вообще использует output stream, а зациклился на том, что второй не streaming. - person Rob; 10.12.2014
comment
@kumar Кстати, вы не поделились AddImageIntoDocument, поэтому неясно, каковы последствия этого метода для памяти. (Этот метод хранит данные в какой-то структуре памяти?) Убедитесь, что вы не пытаетесь удерживать все изображения в памяти одновременно. - person Rob; 10.12.2014
comment
@kumar Хорошо, так что вы загружаете NSData изображения, конвертируете его в UIImage, конвертируете обратно в другой NSData (который будет отличаться от исходного) и сохраняете это. Таким образом, для каждой загрузки изображения вы удерживаете изображение в памяти три раза. Вы должны просто использовать ту же технику потока вывода, что и в AddVideoIntoDocument, и (а) это будет гораздо более эффективным использованием памяти; и (б) вы не пострадаете от ухудшения изображения, которое испытывает ваш текущий код. - person Rob; 10.12.2014
comment
если я использую nsoutputstream, как я могу убедиться, что изображение загружено правильно? - person kumar; 10.12.2014
comment
Я бы попробовал использовать AFImageResponseSerializer в сочетании с потоком вывода (признаюсь, я никогда не использовал эту комбинацию, но вам стоит попробовать). В худшем случае используйте outputStream и попробуйте создать UIImage, когда закончите, и убедитесь, что это удастся. Это не идеально (поскольку вы загружаете все изображение в память за один раз), но, по крайней мере, мы говорим только об одной копии изображения, хранящейся в памяти за один раз. - person Rob; 10.12.2014
comment
Я попытался использовать NSURLSessionDownloadTask, и мне кажется, что при загрузке и сохранении изображений потребляется меньше памяти, поэтому я изменил реализацию на NSURLSession. - person kumar; 12.12.2014

Вы можете начать использовать downloadTask NSURLSession.

Думаю, это решит вашу проблему.

NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://someSite.com/somefile.zip"]];
[[NSURLSession sharedSession] downloadTaskWithRequest:request
                                        completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error)
     {
         // Use location (it's file URL in your system)
     }];
person l0gg3r    schedule 22.10.2014