在iOS中批量下载多个文件

我有一个应用程序,现在需要下载数百个小型PDF的基于用户的select。 我遇到的问题是需要花费大量的时间,因为每次打开一个新的连接。 我知道我可以使用GCD来做一个asynchronous下载,但是我怎么去做这样的批处理像10个文件左右。 有没有一个框架已经做到这一点,或者这是我将不得不build立自我?

这个答案现在已经过时了。 现在NSURLConnection已经被弃用, NSURLSession现在已经可用了,它提供了更好的下载一系列文件的机制,避免了这里设想的解决scheme的复杂性。 看我讨论NSURLSession 其他答案 。

为了历史的目的,我会保留下面的答案。


我确信有很多很棒的解决scheme,但是我写了一个小的下载器pipe理器来处理这个场景,你想下载一堆文件。 只需将各个下载文件添加到下载pipe理器,完成后,它将启动下一个排队的文件。 你可以指定多less你想要它同时做(我默认为四),所以没有配料需要。 如果没有别的,这可能会引发一些关于如何在自己的实现中这样做的想法。

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

  1. 如果文件很大,则永远不会将整个文件保存在内存中,而是在下载时将其保存到永久存储器中。 这大大减less了下载过程的内存占用。

  2. 正在下载文件时,有委托协议通知您或下载进度。

我试图在下载pipe理器的github页面上描述所涉及的类和正确的操作。


不过,我应该说,这是为了解决一个特定的问题,我想跟踪下载大文件的进度,因为它们正在被下载,而且我不想把整个内存放在一个地方(例如,如果你正在下载一个100MB的文件,你真的想在下载的时候把它保存在RAM中吗?)。

虽然我的解决scheme解决了这些问题,但如果您不需要这些,则使用操作队列的解决scheme将简单得多。 其实你甚至暗示了这种可能性:

我知道我可以使用GCD来做一个asynchronous下载,但是我怎样才能像10个文件那样批量执行这个操作。 …

我不得不说,做一个asynchronous下载是正确的解决scheme,而不是试图通过批量下载来缓解下载性能问题。

你谈论使用GCD队列。 就我个人而言,我只是创build一个操作队列,以便我可以指定我想要的并发操作数,并使用NSData方法dataWithContentsOfURLwriteToFile:atomically:下载单个文件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您可以享受并发性,而不会因为太多的并发请求而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]; 

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

顺便说一下,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]; } 

也许,考虑到你的下载需要花费大量时间,即使在应用程序进入后台之后,你也可能希望它们继续下载。 如果是这样,你可以使用backgroundSessionConfiguration而不是defaultSessionConfiguration (尽pipe你必须实现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的Apple演示,请参阅简单后台传输示例。

如果所有的PDF都来自您控制的服务器,那么一个选项就是让一个请求传递一个您想要的文件列表(作为URL上的查询参数)。 然后您的服务器可以将请求的文件压缩成单个文件。

这将减less你需要的个人networking请求的数量。 当然,你需要更新你的服务器来处理这样的请求,你的应用程序需要解压缩返回的文件。 但是,这比制作大量的单个networking请求要有效得多。

使用NSOperationQueue,并使每个下载单独的NSOperation。 设置您的队列上的最大并发操作属性,然而,您希望能够同时运行许多下载。 我会保持在4-6范围内。

这里有一篇很好的博客文章,解释了如何进行并发操作。 http://www.dribin.org/dave/blog/archives/2009/05/05/concurrent_operations/

没有什么可以“build立”。 只要在10个线程中循环接下来的10个文件,并在线程完成时获取下一个文件。