我有一个成功使用同步方法下载文件的应用程序(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;
}
答案 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的活动 -
线程被阻止时,不会发生运行循环处理。
我认为您可能需要重新考虑如何设置downloadComplete
变量。考虑使用connectionDidFinishLoading:connection
委托方法来确定下载何时完成而不是循环+睡眠?
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
self.downloadComplete = YES;
// release the connection, and the data object
[connection release];
[receivedData release];
}
您可以使用connection:connection didFailWithError:error
委托方法来确保您处理下载未完成的情况。