我有一个应用程序,现在需要根据用户选择下载数百个小PDF。我遇到的问题是它需要花费大量时间,因为每次必须打开一个新连接。我知道我可以使用GCD进行异步下载,但是如何批量处理10个左右的文件呢?有没有一个框架可以做到这一点,或者这是我必须建立自己的东西?
答案 0 :(得分:45)
这个答案现在已经过时了。既然NSURLConnection
已被弃用且NSURLSession
现已可用,则可提供更好的下载一系列文件的机制,从而避免了此处设想的解决方案的大部分复杂性。请参阅我的other answer讨论NSURLSession
。
出于历史目的,我会在下面保留这个答案。
我确信有很多很棒的解决方案,但是我写了一点downloader manager来处理这种情况,你想要下载一堆文件。只需将各个下载添加到下载管理器中,一旦完成,它将启动下一个排队的下载管理器。您可以指定您希望它同时执行的数量(我默认为4),因此不需要批处理。如果不出意外,这可能会引发一些关于如何在自己的实现中执行此操作的想法。
注意,这提供了两个优点:
如果您的文件很大,这永远不会将整个文件保存在内存中,而是在下载时将其流式传输到持久存储。这大大减少了下载过程的内存占用。
在下载文件时,会有代理协议通知您或下载进度。
我试图在Download Manager github page的主页上描述所涉及的类和正确的操作。
我应该说,这是为了解决一个特定的问题而设计的,我想跟踪大型文件下载时的下载进度,以及我不希望永久保存的大文件内存一次(例如,如果您正在下载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];
}];
}
美好而简单。通过设置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
。
答案 1 :(得分:20)
顺便说一句,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;
});
}
}
顺便说一下,这假设你的app委托有一个backgroundSessionCompletionHandler
属性:
@property (copy) void (^backgroundSessionCompletionHandler)();
如果应用程序被唤醒以处理URLSession
事件,则应用委托将设置该属性:
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
self.backgroundSessionCompletionHandler = completionHandler;
}
对于背景NSURLSession
的Apple演示,请参阅Simple Background Transfer示例。
答案 2 :(得分:2)
如果所有PDF都来自您控制的服务器,那么一个选项就是让一个请求传递您想要的文件列表(作为URL上的查询参数)。然后,您的服务器可以将请求的文件压缩到一个文件中。
这将减少您需要进行的单个网络请求的数量。当然,您需要更新服务器以处理此类请求,并且您的应用需要解压缩返回的文件。但这比制作大量单独的网络请求更有效。
答案 3 :(得分:1)
使用NSOperationQueue并使每次下载成为单独的NSOperation。将队列上的最大并发操作数设置为您希望能够同时运行的下载数量。我会亲自将它保持在4-6范围内。
这是一篇很好的博客文章,解释了如何进行并发操作。 http://www.dribin.org/dave/blog/archives/2009/05/05/concurrent_operations/
答案 4 :(得分:0)
一个大惊喜是下载多个文件时dataWithContentsOfURL多么慢!
要自己查看,请运行以下示例: (您不需要downloadTaskWithURL的downloadQueue,它只是为了更容易比较而已)
- (IBAction)downloadUrls:(id)sender {
[[NSOperationQueue new] addOperationWithBlock:^{
[self download:true];
[self download:false];
}];
}
-(void) download:(BOOL) slow
{
double startTime = CACurrentMediaTime();
NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
static NSURLSession* urlSession;
if(urlSession == nil)
urlSession = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
dispatch_group_t syncGroup = dispatch_group_create();
NSOperationQueue* downloadQueue = [NSOperationQueue new];
downloadQueue.maxConcurrentOperationCount = 10;
NSString* baseUrl = @"https://via.placeholder.com/468x60?text=";
for(int i = 0;i < 100;i++) {
NSString* urlString = [baseUrl stringByAppendingFormat:@"image%d", i];
dispatch_group_enter(syncGroup);
NSURL *url = [NSURL URLWithString:urlString];
[downloadQueue addOperationWithBlock:^{
if(slow) {
NSData *urlData = [NSData dataWithContentsOfURL:url];
dispatch_group_leave(syncGroup);
//NSLog(@"downloaded: %@", urlString);
}
else {
NSURLSessionDownloadTask* task = [urlSession downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//NSLog(@"downloaded: %@", urlString);
dispatch_group_leave(syncGroup);
}];[task resume];
}
}];
}
dispatch_group_wait(syncGroup, DISPATCH_TIME_FOREVER);
double endTime = CACurrentMediaTime();
NSLog(@"Download time:%.2f", (endTime - startTime));
}
答案 5 :(得分:-2)
没有什么可以“建立”。只需在10个线程中循环接下来的10个文件,并在线程完成时获取下一个文件。