在iOS中批量下载多个文件
我有一个应用程序,现在需要下载数百个小型PDF的基于用户的select。 我遇到的问题是需要花费大量的时间,因为每次打开一个新的连接。 我知道我可以使用GCD来做一个asynchronous下载,但是我怎么去做这样的批处理像10个文件左右。 有没有一个框架已经做到这一点,或者这是我将不得不build立自我?
这个答案现在已经过时了。 现在NSURLConnection
已经被弃用, NSURLSession
现在已经可用了,它提供了更好的下载一系列文件的机制,避免了这里设想的解决scheme的复杂性。 看我讨论NSURLSession
其他答案 。
为了历史的目的,我会保留下面的答案。
我确信有很多很棒的解决scheme,但是我写了一个小的下载器pipe理器来处理这个场景,你想下载一堆文件。 只需将各个下载文件添加到下载pipe理器,完成后,它将启动下一个排队的文件。 你可以指定多less你想要它同时做(我默认为四),所以没有配料需要。 如果没有别的,这可能会引发一些关于如何在自己的实现中这样做的想法。
请注意,这提供了两个优点:
-
如果文件很大,则永远不会将整个文件保存在内存中,而是在下载时将其保存到永久存储器中。 这大大减less了下载过程的内存占用。
-
正在下载文件时,有委托协议通知您或下载进度。
我试图在下载pipe理器的github页面上描述所涉及的类和正确的操作。
不过,我应该说,这是为了解决一个特定的问题,我想跟踪下载大文件的进度,因为它们正在被下载,而且我不想把整个内存放在一个地方(例如,如果你正在下载一个100MB的文件,你真的想在下载的时候把它保存在RAM中吗?)。
虽然我的解决scheme解决了这些问题,但如果您不需要这些,则使用操作队列的解决scheme将简单得多。 其实你甚至暗示了这种可能性:
我知道我可以使用GCD来做一个asynchronous下载,但是我怎样才能像10个文件那样批量执行这个操作。 …
我不得不说,做一个asynchronous下载是正确的解决scheme,而不是试图通过批量下载来缓解下载性能问题。
你谈论使用GCD队列。 就我个人而言,我只是创build一个操作队列,以便我可以指定我想要的并发操作数,并使用NSData
方法dataWithContentsOfURL
和writeToFile: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)提供了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
(尽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个文件,并在线程完成时获取下一个文件。