首页 > 解决方案 > 在Objective C中使用NSURLSession downloadTask顺序下载多个图像

问题描述

我的应用程序提供了从我们的服务器下载 3430 张高分辨率图像的选项,每个图像的大小为 50k - 600k 字节。

最初的方法是只下载所有这些 - 但我们意识到这会产生很多 NSURLErrorTimedOut 错误并使我们的程序崩溃。然后我们实现了它,以便我们下载所有图像,但一次分批下载 100 个图像。SO上的某人建议我们实际上像这样实现我们的下载:

创建需要下载的所有文件 URL 的列表。

编写您的代码,以便它按顺序下载这些 URL。即不要让它开始下载文件,直到前一个文件完成(或失败并且您决定暂时跳过它)。

使用 NSURLSession 支持将单个文件下载到文件夹,不要使用代码获取 NSData 并自己保存文件。这样,您的应用程序就不需要在下载完成时运行。

确保您可以判断文件是否已经下载,以防下载中断或手机在下载过程中重新启动。例如,您可以通过比较它们的名称(如果它们足够独特的话)来做到这一点,或者将注释保存到 plist 中,使您可以将下载的文件与其来源的 URL 匹配,或者在您的情况下构成识别特征的任何内容。

在启动时,检查是否所有文件都在那里。如果没有,请将缺少的放在上面的下载列表中并按顺序下载它们,如#2。

在开始下载任何内容之前(包括在上一次下载完成或失败后下载下一个文件),请使用 Apple 的 SystemConfiguration.framework 中的可达性 API 进行可达性检查。这将告诉您用户是否有连接,以及您使用的是 WiFi 还是蜂窝网络(通常,您不想通过蜂窝网络下载大量文件,大多数蜂窝网络连接都是按流量计费的)。

我们在此处创建要下载的所有图像的列表:

- (void)generateImageURLList:(BOOL)batchDownloadImagesFromServer
{
    NSError* error;
    NSFetchRequest* leafletURLRequest = [[[NSFetchRequest alloc] init] autorelease];
    NSEntityDescription* leafletURLDescription = [NSEntityDescription entityForName:@"LeafletURL" inManagedObjectContext:managedObjectContext];
    [leafletURLRequest setEntity:leafletURLDescription];        
    numberOfImages = [managedObjectContext countForFetchRequest:leafletURLRequest error:&error];
    NSPredicate* thumbnailPredicate = [NSPredicate predicateWithFormat:@"thumbnailLocation like %@", kLocationServer];
    [leafletURLRequest setPredicate:thumbnailPredicate];
    self.uncachedThumbnailArray = [managedObjectContext executeFetchRequest:leafletURLRequest error:&error];      
    NSPredicate* hiResPredicate = [NSPredicate predicateWithFormat:@"hiResImageLocation != %@", kLocationCache];
    [leafletURLRequest setPredicate:hiResPredicate];
    self.uncachedHiResImageArray = [managedObjectContext executeFetchRequest:leafletURLRequest error:&error];
}

hitServerForUrl我们使用 NSURLSession 通过调用和实现将单个图像下载到文件夹didFinishDownloadingToURL

- (void)hitServerForUrl:(NSURL*)requestUrl {
    NSURLSessionConfiguration *defaultConfigurationObject = [NSURLSessionConfiguration defaultSessionConfiguration];

    NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration:defaultConfigurationObject delegate:self delegateQueue: nil];

    NSURLSessionDownloadTask *fileDownloadTask = [defaultSession downloadTaskWithURL:requestUrl];

    [fileDownloadTask resume];

}

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {


    if (isThumbnail)
    {
        leafletURL.thumbnailLocation = kLocationCache;
    }
    else
    {
        leafletURL.hiResImageLocation = kLocationCache;
    }

    // Filename to write to
    NSString* filePath = [leafletURL pathForImageAtLocation:kLocationCache isThumbnail:isThumbnail isRetina:NO];

    // If it's a retina image, append the "@2x"
    if (isRetina_) {
        filePath = [filePath stringByReplacingOccurrencesOfString:@".jpg" withString:@"@2x.jpg"];
    }

    NSString* dir = [filePath stringByDeletingLastPathComponent];

    [managedObjectContext save:nil];

    NSError* error;
    [[NSFileManager defaultManager] createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:&error];

    NSURL *documentURL = [NSURL fileURLWithPath:filePath];

    NSLog(@"file path : %@", filePath);
    if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
        //Remove the old file from directory
    }

    [[NSFileManager defaultManager] moveItemAtURL:location
                                            toURL:documentURL
                                            error:&error];
    if (error){
        //Handle error here
    }
}

这段代码调用loadImage了 `hitServer:

-(void)downloadImagesFromServer{

    [self generateImageURLList:NO];

    [leafletImageLoaderQueue removeAllObjects];
    numberOfHiResImageLeft = [uncachedHiResImageArray count];

    for ( LeafletURL* aLeafletURL in uncachedHiResImageArray)
        {
            //// Do the same thing again, except set isThumb = NO. ////
            LeafletImageLoader* hiResImageLoader = [[LeafletImageLoader alloc] initWithDelegate:self];
            [leafletImageLoaderQueue addObject:hiResImageLoader]; // do this before making connection!! //

            [hiResImageLoader loadImage:aLeafletURL isThumbnail:NO   isBatchDownload:YES];

            //// Adding object to array already retains it, so it's safe to release it here. ////
            [hiResImageLoader release];
            uncachedHiResIndex++;
            NSLog(@"uncached hi res index: %ld, un cached hi res image array size: %lu", (long)uncachedHiResIndex, (unsigned long)[uncachedHiResImageArray count]);
    }

}

- (void)loadImage:(LeafletURL*)leafletURLInput isThumbnail:(BOOL)isThumbnailInput isBatchDownload:(BOOL)isBatchDownload isRetina:(BOOL)isRetina
{

    isRetina_ = isRetina;

    if (mConnection)
    {
        [mConnection cancel];
        [mConnection release];
        mConnection = nil;
    }
    if (mImageData)
    {
        [mImageData release];
        mImageData = nil;
    }

    self.leafletURL = leafletURLInput;
    self.isThumbnail = isThumbnailInput;

    NSString* location = (self.isThumbnail) ?leafletURL.thumbnailLocation :leafletURL.hiResImageLocation;

    //// Check if the image needs to be downloaded from server. If it is a batch download, then override the local resources////
    if ( ([location isEqualToString:kLocationServer] || (isBatchDownload && [location isEqualToString:kLocationResource])) && self.leafletURL.rawURL != nil )
    {
        //NSLog(@"final loadimage called server");
        //// tell the delegate to get ride of the old image while waiting. ////
        if([delegate respondsToSelector:@selector(leafletImageLoaderWillBeginLoadingImage:)])
        {
            [delegate leafletImageLoaderWillBeginLoadingImage:self];
        }

        mImageData = [[NSMutableData alloc] init];

        NSURL* url = [NSURL URLWithString:[leafletURL pathForImageOnServerUsingThumbnail:self.isThumbnail isRetina:isRetina]];
        [self hitServerForUrl:url];

    }

    //// if not, tell the delegate that the image is already cached. ////
    else
    {
        if([delegate respondsToSelector:@selector(leafletImageLoaderDidFinishLoadingImage:)])
        {

            [delegate leafletImageLoaderDidFinishLoadingImage:self];

        }
    }
}

目前,我正在尝试弄清楚如何按顺序下载图像,这样hitServer在最后一张图像下载完成之前我们不会调用。我需要在后台下载吗?感谢您的建议!

标签: iosobjective-cnsurlsessionnsurlsessiondownloadtask

解决方案


这听起来很像一个架构问题。如果您在不限制下载的情况下启动下载,那么您当然会开始遇到超时和其他问题。想想其他应用程序以及它们的作用。使用户能够进行多次下载的应用程序通常会限制一次下载的可能性。例如,iTunes 可以将数千个下载排队,但一次只能运行 3 个。一次只限制一个只会减慢您的用户的速度。您需要考虑用户可用带宽的平衡。

另一部分是再次考虑您的用户想要什么。您的每个用户都需要每一张图片吗?我不知道你为他们提供什么,但在大多数访问图像或音乐等资源的应用程序中,下载内容和时间取决于用户。因此他们只下载他们感兴趣的内容。所以我建议只下载用户正在查看或以某种方式请求他们想要下载的内容。


推荐阅读