【问题标题】:Downloading multiple files in batches in iOS在iOS中批量下载多个文件
【发布时间】:2012-12-21 20:12:46
【问题描述】:

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

【问题讨论】:

  • 你用什么来下载文件? NSURLConnection?您使用的是+sendAsynchronousRequest:queue:completionHandler: 还是+connectionWithRequest:delegate:?如果您使用的是委托,您实现了哪些委托方法?
  • @robmayoff 我们还没有任何异步下载。现在我们只是使用NSData dataWithContentsOfURL 一次下载一个文件。
  • ios85,我可能会建议考虑不接受基于NSURLConnection 的答案。当NSURLSession 在当今时代不再适用时,人们正在看到该答案并提出有关它的问题。或许你可以考虑接受that answer,而不是?

标签: objective-c ios


【解决方案1】:

这个答案现在已经过时了。现在NSURLConnection 已被弃用并且NSURLSession 现在可用,这为下载一系列文件提供了更好的机制,避免了此处设想的解决方案的大部分复杂性。请参阅我的other answer,其中讨论了NSURLSession

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


我确信有很多很棒的解决方案,但我写了一点downloader manager 来处理这种情况,你想下载一堆文件。只需将单个下载添加到下载管理器中,当一个下载完成后,它将启动下一个排队的下载。您可以指定您希望它同时执行多少个(我默认为四个),因此不需要批处理。如果不出意外,这可能会激发您在自己的实现中如何做到这一点的一些想法。

注意,这有两个好处:

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

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

我试图在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

【讨论】:

  • 完美运行!谢谢。
  • “如果服务器能够告诉我们文件的长度”是什么意思?在哪里定义文件长度?
  • @VaibhavSaran 在 HTTP 请求中,服务器通常会提供一个Content-length 头,NSURLConnection 可以通过didReceiveResponse 方法在NSURLResponse 中返回,并且这个响应对象有一个@ 987654338@财产。但有时,NSURLConnection 无法确定大小,或者是因为 Web 服务器提供了 gzipContent-encoding(它透明地压缩内容以提高效率,但您无法确定长度),或者因为响应具有 @ 987654342@ of chunked(即它是流式传输的),或者是因为响应格式错误。
  • 哦!非常感谢你解释它:)
  • 是否有任何 iPhone 应用程序可以一次粘贴 url 列表,然后将它们全部下载并在最后报告一次损坏?
【解决方案2】:

顺便说一句,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(尽管您必须实现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 演示,请参阅Simple Background Transfer 示例。

【讨论】:

  • 嗨,Rob,我可以使用您的下载管理器下载 HLS (.m3u8) 文件(包含超过 500 个块)它将成为下载管理器吗?
  • 现在,我只需使用 NSURLSession 和后台会话配置(如答案所示)来下载大文件。
  • 是的,我明白 :) 但是,您下载管理器(包含NSURLConnection 方法)是否适合下载 n 个文件?坦率地说,我没有时间实现自己的下载管理器来满足我的时间表;)
  • 谢谢。您是否正在使用 NSURLSession 方法更新您的存储库?
【解决方案3】:

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

这将减少您需要发出的单个网络请求的数量。当然,您需要更新服务器以处理此类请求,并且您的应用程序需要解压缩返回的文件。但这比发出大量单独的网络请求要高效得多。

【讨论】:

  • 除其他外,zip 默认情况下会压缩文件。
【解决方案4】:

使用 NSOperationQueue 并使每次下载都成为单独的 NSOperation。将队列上的最大并发操作属性设置为您希望能够同时运行的下载数量。我个人会保持在 4-6 范围内。

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

【讨论】:

    【解决方案5】:

    令人惊讶的是,下载多个文件时 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));
    }
    

    【讨论】:

      【解决方案6】:

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

      【讨论】:

      • 可能是一种可能的解决方案,但不是最佳解决方案。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-05-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多