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

Загрузка нескольких файлов в пакетах в iOS

У меня есть приложение, которому сейчас нужно скачать сотни небольших PDF файлов на основе выбора пользователей. Проблема, с которой я сталкиваюсь, заключается в том, что она занимает значительное количество времени, потому что каждый раз она должна открывать новое соединение. Я знаю, что я мог бы использовать GCD для загрузки async, но как бы я мог сделать это в пакетах вроде 10 файлов или около того. Есть ли рамки, которые уже делают это, или это то, что мне нужно будет построить самостоятельно?

4b9b3361

Ответ 1

Этот ответ теперь устарел. Теперь, когда NSURLConnection устарел, и теперь доступен NSURLSession, который предлагает лучшие механизмы для загрузки серии файлов, избегая значительной сложности рассматриваемого здесь решения. См. Мой другой ответ, в котором обсуждается NSURLSession.

Я сохраню этот ответ ниже, для исторических целей.


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

Примечание. Это дает два преимущества:

  • Если ваши файлы большие, это никогда не содержит весь файл в памяти, а скорее переносит его на постоянное хранилище при его загрузке. Это значительно уменьшает объем памяти процесса загрузки.

  • Когда файлы загружаются, есть протоколы делегатов, которые информируют вас или ход загрузки.

Я попытался описать задействованные классы и правильную работу на главной странице на странице Download Manager github.


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

Хотя мое решение решает эту проблему, если вам это не нужно, есть гораздо более простые решения, использующие очереди операций. На самом деле вы даже намекаете на такую ​​возможность:

Я знаю, что я мог бы использовать GCD для загрузки async, но как мне это сделать в пакетах вроде 10 файлов или около того....

Я должен сказать, что выполнение async-загрузки поражает меня как правильное решение, вместо того, чтобы пытаться смягчить проблему производительности загрузки, загружая их партиями.

Вы говорите об использовании очередей GCD. Лично я просто создал очередь операций, чтобы я мог указать, сколько одновременных операций я хотел, и загружать отдельные файлы с помощью метода NSData dataWithContentsOfURL, за которым следует writeToFile:atomically:, заставляя каждую загружать собственную операцию.

Итак, например, если у вас есть массив URL-адресов загружаемых файлов, это может быть:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;

for (NSURL* url in urlArray)
{
    [queue addOperationWithBlock:^{
        NSData *data = [NSData dataWithContentsOfURL:url];
        NSString *filename = [documentsPath stringByAppendingString:[url lastPathComponent]];
        [data writeToFile:filename atomically:YES];
    }];
}

Приятный и простой. И, установив queue.maxConcurrentOperationCount, вам понравится concurrency, не сокрушая ваше приложение (или сервер) слишком большим количеством одновременных запросов.

И если вам нужно получить уведомление, когда выполняются операции, вы можете сделать что-то вроде:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;

NSBlockOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        [self methodToCallOnCompletion];
    }];
}];

for (NSURL* url in urlArray)
{
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        NSData *data = [NSData dataWithContentsOfURL:url];
        NSString *filename = [documentsPath stringByAppendingString:[url lastPathComponent]];
        [data writeToFile:filename atomically:YES];
    }];
    [completionOperation addDependency:operation];
}

[queue addOperations:completionOperation.dependencies waitUntilFinished:NO];
[queue addOperation:completionOperation];

Это сделает то же самое, за исключением того, что он вызовет methodToCallOnCompletion в основной очереди, когда будут выполнены все загрузки.

Ответ 2

Кстати, iOS 7 (и Mac OS 10.9) предлагают URLSession и URLSessionDownloadTask, который обрабатывает это довольно изящно. Если вы просто хотите загрузить кучу файлов, вы можете сделать что-то вроде:

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession              *session       = [NSURLSession sessionWithConfiguration:configuration];

NSString      *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSFileManager *fileManager   = [NSFileManager defaultManager];

for (NSString *filename in self.filenames) {
    NSURL *url = [baseURL URLByAppendingPathComponent:filename];
    NSURLSessionTask *downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
        NSString *finalPath = [documentsPath stringByAppendingPathComponent:filename];

        BOOL success;
        NSError *fileManagerError;
        if ([fileManager fileExistsAtPath:finalPath]) {
            success = [fileManager removeItemAtPath:finalPath error:&fileManagerError];
            NSAssert(success, @"removeItemAtPath error: %@", fileManagerError);
        }

        success = [fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:finalPath] error:&fileManagerError];
        NSAssert(success, @"moveItemAtURL error: %@", fileManagerError);

        NSLog(@"finished %@", filename);
    }];
    [downloadTask resume];
}

Возможно, учитывая, что ваши загрузки занимают "значительное количество времени", вы можете захотеть, чтобы они продолжали загрузку даже после того, как приложение перешло в фоновый режим. Если это так, вы можете использовать backgroundSessionConfiguration, а не defaultSessionConfiguration (хотя вам необходимо реализовать методы NSURLSessionDownloadDelegate, а не использовать completionHandler). Эти фоновые сессии медленнее, но опять же, они происходят, даже если пользователь оставил ваше приложение. Таким образом:

- (void)startBackgroundDownloadsForBaseURL:(NSURL *)baseURL {
    NSURLSession *session = [self backgroundSession];

    for (NSString *filename in self.filenames) {
        NSURL *url = [baseURL URLByAppendingPathComponent:filename];
        NSURLSessionTask *downloadTask = [session downloadTaskWithURL:url];
        [downloadTask resume];
    }
}

- (NSURLSession *)backgroundSession {
    static NSURLSession *session = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:kBackgroundId];
        session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    });

    return session;
}

#pragma mark - NSURLSessionDownloadDelegate

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
    NSString *documentsPath    = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
    NSString *finalPath        = [documentsPath stringByAppendingPathComponent:[[[downloadTask originalRequest] URL] lastPathComponent]];
    NSFileManager *fileManager = [NSFileManager defaultManager];

    BOOL success;
    NSError *error;
    if ([fileManager fileExistsAtPath:finalPath]) {
        success = [fileManager removeItemAtPath:finalPath error:&error];
        NSAssert(success, @"removeItemAtPath error: %@", error);
    }

    success = [fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:finalPath] error:&error];
    NSAssert(success, @"moveItemAtURL error: %@", error);
}

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes {
    // Update your UI if you want to
}

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
    // Update your UI if you want to
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (error)
        NSLog(@"%s: %@", __FUNCTION__, error);
}

#pragma mark - NSURLSessionDelegate

- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error {
    NSLog(@"%s: %@", __FUNCTION__, error);
}

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
    AppDelegate *appDelegate = (id)[[UIApplication sharedApplication] delegate];
    if (appDelegate.backgroundSessionCompletionHandler) {
        dispatch_async(dispatch_get_main_queue(), ^{
            appDelegate.backgroundSessionCompletionHandler();
            appDelegate.backgroundSessionCompletionHandler = nil;
        });
    }
}

Кстати, это предполагает, что ваш делегат приложения имеет свойство backgroundSessionCompletionHandler:

@property (copy) void (^backgroundSessionCompletionHandler)();

И что делегат приложения установит это свойство, если приложение было пробуждено обрабатывать события URLSession:

- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
    self.backgroundSessionCompletionHandler = completionHandler;
}

Для демонстрации фона фона NSURLSession на примере Простая передача фона.

Ответ 3

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

Это сократит количество индивидуальных сетевых запросов, которые вам нужно выполнить. Конечно, вам нужно обновить свой сервер для обработки такого запроса, и вашему приложению нужно разархивировать возвращенный файл. Но это намного эффективнее, чем создание множества индивидуальных сетевых запросов.

Ответ 4

Используйте NSOperationQueue и каждый раз загружайте отдельный NSOperation. Задайте максимальное свойство одновременных операций в вашей очереди, чтобы одновременно загрузить несколько загрузок, которые вы хотите запустить одновременно. Я бы сохранил его в классе 4-6 лично.

Здесь хорошая запись в блоге, в которой объясняется, как выполнять параллельные операции. http://www.dribin.org/dave/blog/archives/2009/05/05/concurrent_operations/

Ответ 5

Ничего не нужно "строить". Просто пропустите следующие 10 файлов каждый раз в 10 потоках и получите следующий файл, когда заканчивается нить.