下载大文件 - iPhone SDK

21
我正在使用Erica Sadun的异步下载方法(此处提供该项目文件的链接:下载),但她的方法无法处理大尺寸文件(50 MB或以上)。如果我尝试下载一个超过50 MB的文件,它通常会由于内存崩溃而崩溃。有没有办法修改这段代码,使其也能处理大文件?以下是我在DownloadHelper类中拥有的代码(已包含在下载链接中):

.h

@protocol DownloadHelperDelegate <NSObject>
@optional
- (void) didReceiveData: (NSData *) theData;
- (void) didReceiveFilename: (NSString *) aName;
- (void) dataDownloadFailed: (NSString *) reason;
- (void) dataDownloadAtPercent: (NSNumber *) aPercent;
@end

@interface DownloadHelper : NSObject 
{
    NSURLResponse *response;
    NSMutableData *data;
    NSString *urlString;
    NSURLConnection *urlconnection;
    id <DownloadHelperDelegate> delegate;
    BOOL isDownloading;
}
@property (retain) NSURLResponse *response;
@property (retain) NSURLConnection *urlconnection;
@property (retain) NSMutableData *data;
@property (retain) NSString *urlString;
@property (retain) id delegate;
@property (assign) BOOL isDownloading;

+ (DownloadHelper *) sharedInstance;
+ (void) download:(NSString *) aURLString;
+ (void) cancel;
@end

.m

#define DELEGATE_CALLBACK(X, Y) if (sharedInstance.delegate && [sharedInstance.delegate respondsToSelector:@selector(X)]) [sharedInstance.delegate performSelector:@selector(X) withObject:Y];
#define NUMBER(X) [NSNumber numberWithFloat:X]

static DownloadHelper *sharedInstance = nil;

@implementation DownloadHelper
@synthesize response;
@synthesize data;
@synthesize delegate;
@synthesize urlString;
@synthesize urlconnection;
@synthesize isDownloading;

- (void) start
{
    self.isDownloading = NO;

    NSURL *url = [NSURL URLWithString:self.urlString];
    if (!url)
    {
        NSString *reason = [NSString stringWithFormat:@"Could not create URL from string %@", self.urlString];
        DELEGATE_CALLBACK(dataDownloadFailed:, reason);
        return;
    }

    NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:url];
    if (!theRequest)
    {
        NSString *reason = [NSString stringWithFormat:@"Could not create URL request from string %@", self.urlString];
        DELEGATE_CALLBACK(dataDownloadFailed:, reason);
        return;
    }

    self.urlconnection = [[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
    if (!self.urlconnection)
    {
        NSString *reason = [NSString stringWithFormat:@"URL connection failed for string %@", self.urlString];
        DELEGATE_CALLBACK(dataDownloadFailed:, reason);
        return;
    }

    self.isDownloading = YES;

    // Create the new data object
    self.data = [NSMutableData data];
    self.response = nil;

    [self.urlconnection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}

- (void) cleanup
{
    self.data = nil;
    self.response = nil;
    self.urlconnection = nil;
    self.urlString = nil;
    self.isDownloading = NO;
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)aResponse
{
    // store the response information
    self.response = aResponse;

    // Check for bad connection
    if ([aResponse expectedContentLength] < 0)
    {
        NSString *reason = [NSString stringWithFormat:@"Invalid URL [%@]", self.urlString];
        DELEGATE_CALLBACK(dataDownloadFailed:, reason);
        [connection cancel];
        [self cleanup];
        return;
    }

    if ([aResponse suggestedFilename])
        DELEGATE_CALLBACK(didReceiveFilename:, [aResponse suggestedFilename]);
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)theData
{
    // append the new data and update the delegate
    [self.data appendData:theData];
    if (self.response)
    {
        float expectedLength = [self.response expectedContentLength];
        float currentLength = self.data.length;
        float percent = currentLength / expectedLength;
        DELEGATE_CALLBACK(dataDownloadAtPercent:, NUMBER(percent));
    }
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    // finished downloading the data, cleaning up
    self.response = nil;

    // Delegate is responsible for releasing data
    if (self.delegate)
    {
        NSData *theData = [self.data retain];
        DELEGATE_CALLBACK(didReceiveData:, theData);
    }
    [self.urlconnection unscheduleFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    [self cleanup];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    self.isDownloading = NO;
    NSLog(@"Error: Failed connection, %@", [error localizedDescription]);
    DELEGATE_CALLBACK(dataDownloadFailed:, @"Failed Connection");
    [self cleanup];
}

+ (DownloadHelper *) sharedInstance
{
    if(!sharedInstance) sharedInstance = [[self alloc] init];
    return sharedInstance;
}

+ (void) download:(NSString *) aURLString
{
    if (sharedInstance.isDownloading)
    {
        NSLog(@"Error: Cannot start new download until current download finishes");
        DELEGATE_CALLBACK(dataDownloadFailed:, @"");
        return;
    }

    sharedInstance.urlString = aURLString;
    [sharedInstance start];
}

+ (void) cancel
{
    if (sharedInstance.isDownloading) [sharedInstance.urlconnection cancel];
}
@end

最后,这就是我如何使用上面的两个类来编写文件:

- (void) didReceiveData: (NSData *) theData
{
    if (![theData writeToFile:self.savePath atomically:YES])
        [self doLog:@"Error writing data to file"];

    [theData release];

}

如果有人能帮助我,我会非常高兴!

谢谢,

Kevin


2
我按照你描述的方法编写了一个库。我将其放在这里,希望它对一些人有用,或者激发他们编写自己的解决方案。当然,如果你同意的话。https://github.com/thibaultCha/TCBlobDownload - thibaultcha
3个回答

30

NSOutputStream *stream 替换内存中的 NSData *data。在 -start 中创建要追加的流并打开它:

stream = [[NSOutputStream alloc] initToFileAtPath:path append:YES];
[stream open];
随着数据的到来,将其写入流中:
NSUInteger left = [theData length];
NSUInteger nwr = 0;
do {
    nwr = [stream write:[theData bytes] maxLength:left];
    if (-1 == nwr) break;
    left -= nwr;
} while (left > 0);
if (left) {
    NSLog(@"stream error: %@", [stream streamError]);
}

完成时,关闭流:

[stream close];

更好的方法是除了添加数据 ivar 外,还应该添加流(stream),将帮助程序设置为流的代理(delegate),在数据 ivar 中缓冲传入的数据,然后在流发送给帮助程序其可用空间事件时将数据 ivar 的内容转储到帮助程序中,并清除它。


谢谢您的回复,但是是否仍然可以获取有关下载的信息?比如获取已下载了多少数据?我通常只使用:self.data.length,但由于在这种新方法中没有NSMutableData,我不知道如何实现它。另外(因为我对Objective-C还比较陌生),我是否应该在.h文件中完全删除NSMutableData以及在帮助类中的所有实例? - lab12
嘿,我仍然在使用这个下载方法上遇到麻烦。在调试器中,它给了我这个错误:"stream error: Error Domain=NSPOSIXErrorDomain Code=1 "The operation couldn’t be completed. Operation not permitted" UserInfo=0x148660 {}" 我不知道为什么会出现这个错误。是路径设置不正确吗?它应该是一个目录还是一个文件?如果你能提供一个示例代码,那就太好了! - lab12
请发布您的代码(例如在gist.github.com),我可以查看它。输出流应该是写入访问权限的目录中的文件,例如您的应用程序文档目录。听起来问题是您试图写入系统不允许的位置。 - Jeremy W. Sherman
哦,我试图在越狱设备上写入以下目录:“/var/mobile/Library/Downloads”。这个目录与异步下载的代码一起工作得很好。我不知道为什么它突然停止工作了。还有一种方法可以查看已下载的数据量吗?请参考我的第一个评论。我需要使用这些信息来制作进度条。 - lab12
你正在接收和写入字节,因此你可以轻松地跟踪已下载的字节数。 - Jeremy W. Sherman
显示剩余3条评论

3
我有对上面代码的轻微修改建议。 使用这个函数,我觉得它很好用。
- (void) didReceiveData: (NSData*) theData
{   
    NSOutputStream *stream=[[NSOutputStream alloc] initToFileAtPath:self.savePath append:YES];
    [stream open];
    percentage.hidden=YES;
    NSString *str=(NSString *)theData;
    NSUInteger left = [str length];
    NSUInteger nwr = 0;
    do {
        nwr = [stream write:[theData bytes] maxLength:left];
        if (-1 == nwr) break;
        left -= nwr;
    } while (left > 0);
    if (left) {
        NSLog(@"stream error: %@", [stream streamError]);
    }
    [stream close];
}

1
这将在下载完成后将所有数据写入流中。OP遇到的问题是在非常大的下载中使用了所有可用内存,您的答案没有解决这个问题。 - Aran Mulholland

0

尝试使用AFNetworking。还有:

NSString *yourFileURL=@"http://yourFileURL.zip";
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:yourFileURL]];
AFURLConnectionOperation *operation =   [[AFHTTPRequestOperation alloc] initWithRequest:request];

NSString *cacheDir = [NSSearchPathForDirectoriesInDomains
                          (NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *filePath = [cacheDir stringByAppendingPathComponent:
                      @"youFile.zip"];

operation.outputStream = [NSOutputStream outputStreamToFileAtPath:filePath append:NO];

[operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
   //show here your downloading progress if needed
}];

[operation setCompletionBlock:^{
    NSLog(@"File successfully downloaded");
}];

[operation start];

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接