如何等待下载大文件?

时间:2012-05-15 20:23:55

标签: objective-c ios ios5

我有一个成功使用同步方法下载文件的应用程序(NSData的initWithContentsOfURL和NSURLConnection的sendSynchronousRequest),但现在我需要支持大文件。这意味着我需要逐位流式传输到磁盘。即使流式传输到磁盘并变为异步也应该是完全正交的概念,Apple的API会强迫我进行异步以便流式传输。

要明确,我的任务是允许更大的文件下载,而不是重新构建整个应用程序,使其更加异步友好。我没有资源。但我承认,依赖于重新架构的方法是有效的。

所以,如果我这样做:

NSURLConnection* connection = [ [ NSURLConnection alloc ] initWithRequest: request delegate: self startImmediately: YES ];

..我最终有自己的didReceiveResponse和didReceiveData。优秀。但是,如果我尝试这样做:

NSURLConnection* connection = [ [ NSURLConnection alloc ] initWithRequest: request delegate: self startImmediately: YES ];
while( !self.downloadComplete )
    [ NSThread sleepForTimeInterval: .25 ];

... didReceiveResponse和didReceiveData永远不会被调用。而且我已经弄明白了为什么。奇怪的是,异步下载发生在我正在使用的同一主线程中。因此,当我睡觉主线程时,我也正在睡觉做这项工作的事情。无论如何,我已经尝试了几种不同的方法来实现我想要的东西,包括告诉NSURLConnection使用不同的NSOperationQueue,甚至做dispatch_async来创建连接并手动启动它(我不知道这是怎么回事 - 我一定不能做得对,但似乎没有任何效果。 编辑:我做得不对的是了解Run Loops的工作方式,以及您需要在辅助线程中手动运行它们。

等待文件下载的最佳方法是什么?

编辑3,工作代码: 以下代码实际上有效,但请告诉我是否有更好的方法。

在原始线程中执行代码,用于设置连接并等待下载完成:

dispatch_queue_t downloadQueue = dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 );
dispatch_async(downloadQueue, ^{
    self.connection = [ [ NSURLConnection alloc ] initWithRequest: request delegate: self startImmediately: YES ];
    [ [ NSRunLoop currentRunLoop ] run ];
});

while( !self.downloadComplete )
    [ NSThread sleepForTimeInterval: .25 ];

在响应连接事件的新线程中执行的代码:

-(void)connection:(NSURLConnection*) connection didReceiveData:(NSData *)data {
    NSUInteger remainingBytes = [ data length ];
    while( remainingBytes > 0 ) {
        NSUInteger bytesWritten = [ self.fileWritingStream write: [ data bytes ] maxLength: remainingBytes ];
        if( bytesWritten == -1 /*error*/ ) {
            self.downloadComplete = YES;
            self.successful = NO;
            NSLog( @"Stream error: %@", self.fileWritingStream.streamError );
            [ connection cancel ];
            return;
        }
        remainingBytes -= bytesWritten;
    }
}

-(void)connection:(NSURLConnection*) connection didFailWithError:(NSError *)error {
    self.downloadComplete = YES;
    [ self.fileWritingStream close ];
    self.successful = NO;
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    self.downloadComplete = YES;
    [ self.fileWritingStream close ];
    self.successful = YES;
}

3 个答案:

答案 0 :(得分:5)

  

... didReceiveResponse和didReceiveData永远不会被调用。而且我   想出原因。奇怪的是,异步下载发生在   我正在使用的主线程相同。它不会创建新线程。所以   当我睡觉主线程时,我也正在睡觉   工作

完全。连接由运行循环驱动;如果您在线程中休眠,则运行循环停止,这会阻止您的连接执行此操作。

所以不要做任何特别的事情。让应用程序坐在那里,运行循环运行。也许在屏幕上放一点微调器以娱乐用户。如果可以的话,开展您的业务。如果可能的话,让用户继续使用该应用程序。连接完成后将调用您的委托方法,然后您可以执行数据所需的操作。

当您将代码移动到后台线程时,您将再次需要一个运行循环来驱动连接。因此,您将开始创建一个运行循环,安排连接,然后返回。运行循环将继续运行,并且在连接完成时将再次调用您的委托方法。如果线程完成,则可以停止运行循环并让线程退出。这就是它的全部内容。

示例:让我们以具体的方式说明这一点。假设您想要一次创建多个连接。将URL粘贴在可变数组中。创建一个名为(例如)startNextConnection的方法,它执行以下操作:

  • 从数组中获取一个URL(在此过程中将其删除)

  • 创建网址请求

  • 启动NSURLConnection

  • 返回

此外,实现必要的NSURLConnectionDelegate方法,尤其是connectionDidFinishLoading:。请使用该方法执行以下操作:

  • 将数据存储在某处(将其写入文件,将其交给另一个线程进行解析,无论如何)

  • 致电startNextConnection

  • 返回

如果错误从未发生过,那就足以检索列表中所有网址的数据了。 (当然,你会希望startNextConnection足够智能,只能在列表为空时返回。)但是错误确实发生了,所以你必须考虑如何处理它们。如果连接失败,您是否要停止整个过程?如果是这样,只需让您的connection:didFailWithError:方法执行适当的操作,但不要将其调用startNextConnection。如果出现错误,是否要跳到列表中的下一个URL?然后...didFailWithError:拨打startNextRequest

备选方案:如果你真的想要保持同步代码的顺序结构,那么就有了类似的东西:

[self downloadURLs];
[self waitForDownloadsToFinish];
[self processData];
...

然后你必须在另一个线程中进行下载,以便你可以自由地阻止当前线程。如果这是你想要的,那么用运行循环设置下载线程。接下来,像您一样使用-initWithRequest:delegate:startImmediately:创建连接,但在最后一个参数中传递NO。使用-scheduleInRunLoop:forMode:将连接添加到下载线程的运行循环,然后使用-start方法启动连接。这使您可以自由地睡眠当前线程。让连接委托的完成例程设置一个标志,例如示例中的self.downloadComplete标志。

答案 1 :(得分:1)

我毫不犹豫地提供这个答案,因为其他人都是正确的,你真的应该围绕异步模型构建你的应用程序。尽管如此:

NSURLConnection* connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
NSString* myPrivateMode = @"com.yourcompany.yourapp.DownloadMode";
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:myPrivateMode];
[connection start];
while (!self.downloadComplete)
    [[NSRunLoop currentRunLoop] runMode:myPrivateMode beforeDate:[NSDate distantFuture]];

不要在主线程上执行此操作。你的应用程序很可能因为阻塞主线程而被终止,因为下载太大的文件到内存。

顺便说一下,如果您要下载到文件而不是内存,则应考虑从NSURLConnection切换到NSURLDownload

答案 2 :(得分:0)

我认为您的sleepForInterval阻止了NSURLConnection的活动 -

  

线程被阻止时,不会发生运行循环处理。

来自NSThread documentation


我认为您可能需要重新考虑如何设置downloadComplete变量。考虑使用connectionDidFinishLoading:connection委托方法来确定下载何时完成而不是循环+睡眠?

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{        
    self.downloadComplete = YES;

    // release the connection, and the data object
    [connection release];
    [receivedData release];
}

来自NSURLConnection guide

您可以使用connection:connection didFailWithError:error委托方法来确保您处理下载未完成的情况。