4

我有一个成功使用同步方法下载文件的应用程序(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 来创建连接并手动启动它(我不明白这怎么行不通 -我一定做得不对),但似乎没有任何效果。编辑:我做的不对的是理解运行循环是如何工作的,并且你需要在辅助线程中手动运行它们。

等到文件下载完成的最佳方法是什么?

编辑 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;
}
4

3 回答 3

5

... didReceiveResponse 和 didReceiveData 永远不会被调用。我已经知道为什么了。奇怪的是,异步下载发生在我正在使用的同一个主线程中。它不会创建新线程。所以当我睡主线程时,我也在睡做工作的东西。

确切地。连接由run loop驱动;如果你让线程休眠,运行循环就会停止,这会阻止你的连接做它的事情。

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

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

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

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

  • 创建一个 URL 请求

  • 启动一个 NSURLConnection

  • 返回

此外,实现必要的 NSURLConnectionDelegate 方法,尤其是connectionDidFinishLoading:. 让该方法执行以下操作:

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

  • 称呼startNextConnection

  • 返回

如果从未发生错误,则足以检索列表中所有 URL 的数据。(当然,您需要startNextConnection足够聪明,以便在列表为空时返回。)但是确实会发生错误,因此您必须考虑如何处理它们。如果连接失败,是否要停止整个过程?如果是这样,只需让您的connection:didFailWithError:方法做一些适当的事情,但不要让它调用startNextConnection. 如果出现错误,是否要跳到列表中的下一个 URL?然后...didFailWithError:打电话startNextRequest

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

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

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

于 2012-05-15T21:14:56.250 回答
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]];

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

顺便说一句,鉴于您正在下载到文件而不是内存,您应该考虑NSURLConnectionNSURLDownload.

于 2012-05-16T13:28:47.283 回答
0

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

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

来自NSThread 文档


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

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

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

来自NSURLConnection 指南

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

于 2012-05-15T20:25:52.010 回答