在iOS中批量下载多个文件

ios*_*s85 24 objective-c ios

我有一个应用程序,现在需要根据用户选择下载数百个小PDF.我遇到的问题是它需要花费大量时间,因为每次必须打开一个新连接.我知道我可以使用GCD进行异步下载,但是如何批量处理10个左右的文件呢?是否有一个已经做到这一点的框架,或者这是我必须建立自我的东西?

Rob*_*Rob 45

这个答案现在已经过时了.现在NSURLConnection已经弃用并且NSURLSession现在可用,它提供了更好的下载一系列文件的机制,避免了这里考虑的解决方案的大部分复杂性.请参阅我讨论的其他答案NSURLSession.

出于历史目的,我将在下面保留这个答案.


我确信有很多很棒的解决方案,但是我写了一个小的下载管理器来处理这个场景,你想要下载一堆文件.只需将各个下载添加到下载管理器中,一旦完成,它将启动下一个排队的下载管理器.您可以指定您希望它同时执行的数量(我默认为4),因此不需要批处理.如果不出意外,这可能会引发一些关于如何在自己的实现中执行此操作的想法.

请注意,这提供了两个优点:

  1. 如果您的文件很大,则永远不会将整个文件保存在内存中,而是在下载时将其流式传输到持久存储.这显着减少了下载过程的内存占用.

  2. 在下载文件时,会有代理协议通知您或下载进度.

我试图在Download Manager github页面上的主页面上描述所涉及的类和正确的操作.


不过,我应该说,这是为了解决一个特定的问题而设计的,我想跟踪大型文件下载时的下载进度,以及我不希望将大文件全部保存在内存中时间(例如,如果您正在下载100mb文件,您真的想在下载时将其保存在RAM中吗?).

虽然我的解决方案解决了这些问题,但如果您不需要,那么使用操作队列的解决方案就更简单了.事实上,你甚至暗示了这种可能性:

我知道我可以使用GCD进行异步下载,但是如何批量处理10个左右的文件呢?...

我不得不说,做异步下载让我觉得是正确的解决方案,而不是试图通过批量下载来缓解下载性能问题.

您谈到使用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];
    }];
}
Run Code Online (Sandbox Code Playgroud)

很好,很简单.通过设置queue.maxConcurrentOperationCount您享受并发性,同时不会破坏您的应用程序(或服务器)太多并发请求.

如果您需要在操作完成时收到通知,您可以执行以下操作:

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];
Run Code Online (Sandbox Code Playgroud)

这将做同样的事情,除了它将methodToCallOnCompletion在所有下载完成时调用主队列.


Rob*_*Rob 20

顺便说一句,iOS 7(和Mac OS 10.9)提供URLSessionURLSessionDownloadTask,它非常优雅地处理它.如果您只想下载一堆文件,可以执行以下操作:

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];
}
Run Code Online (Sandbox Code Playgroud)

也许,鉴于您的下载需要"大量时间",您可能希望它们在应用程序进入后台后继续下载.如果是这样,你可以使用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;
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

顺便说一句,这假设你的app委托有一个backgroundSessionCompletionHandler属性:

@property (copy) void (^backgroundSessionCompletionHandler)();
Run Code Online (Sandbox Code Playgroud)

如果应用程序被唤醒以处理URLSession事件,则应用程序委托将设置该属性:

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

有关背景的Apple演示,NSURLSession请参阅Simple Background Transfer示例.