21

I am using Erica Sadun's method of Asynchronous Downloads (link here for the project file: download), however her method does not work with files that have a big size (50 mb or above). If I try to download a file above 50 mb, it will usually crash due to a memory crash. Is there anyway I can tweak this code so that it works with large files as well? Here is the code I have in the DownloadHelper Classes (which is already in the download link):

.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

And finally this is how I write the file with the two classes above it:

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

    [theData release];

}

If someone could help me out I would be so glad!

Thanks,

Kevin

lab12
  • 6,400
  • 21
  • 68
  • 106
  • 2
    I wrote a library for that, using the method you described. I'm putting it here hoping it will be useful to some people, or inspire them writing their own solution. If you are ok with it of course. https://github.com/thibaultCha/TCBlobDownload – thibaultcha May 31 '13 at 09:40

3 Answers3

30

Replace the in-memory NSData *data with an NSOutputStream *stream. In -start create the stream to append and open it:

stream = [[NSOutputStream alloc] initToFileAtPath:path append:YES];
[stream open];

As data comes in, write it to the stream:

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]);
}

When you're done, close the stream:

[stream close];

A better approach would be to add the stream in addition to the data ivar, set the helper as the stream's delegate, buffer incoming data in the data ivar, then dump the data ivar's contents to the helper whenever the stream sends the helper its space-available event and clear it out of the data ivar.

Jeremy W. Sherman
  • 35,901
  • 5
  • 77
  • 111
  • Thanks for the response, but is it still possible to get information about the download as well? Like getting how much data has been downloaded? I usually just use: self.data.length but since in this new method the NSMutableData isn't there I don't know how to implement it. Also (since i'm kind of new to objective-c), do I get rid of the NSMutableData completely in the .h file and all instances of it in the helper classes? – lab12 Oct 23 '10 at 19:32
  • Hey I'm still having trouble using this download method. In the debugger it gives me this error: "stream error: Error Domain=NSPOSIXErrorDomain Code=1 "The operation couldn’t be completed. Operation not permitted" UserInfo=0x148660 {} " I don't know why this is appearing. Do i have the path set incorrectly? Is it suppose to be a directory or a file? It would be GREAT if you could provide an example code!! – lab12 Oct 24 '10 at 14:53
  • Post your code (e.g. at [gist.github.com](http://gist.github.com/)), and I can look at it. The output stream should be to a file in a directory you have write access to, such as your app's Documents directory. It sounds like the problem is that you're trying to write somewhere that the system will not allow. – Jeremy W. Sherman Oct 24 '10 at 16:23
  • Oh. I was trying to write to the the following directory on a jailbroken device: "/var/mobile/Library/Downloads" This directory worked fine with the code on asynchronous downloading. I don't know why it would suddenly stop working. Also is there a way to see how much data has been downloaded? Please refer to my first comment on it. I need to use this info for a progress bar.. – lab12 Oct 24 '10 at 21:16
  • You're receiving and writing the bytes, so you can readily track how many bytes have been downloaded. – Jeremy W. Sherman Oct 24 '10 at 21:48
  • Ahh thanks for the tip. Didn't know that the information was in the bytes.. Anyway is there any hope for the directory where the downloads are being placed? I have the permissions set so that the app will have access to it. Its a real bummer :( – lab12 Oct 24 '10 at 23:33
  • Hey is there any way to cancel the download? – lab12 Oct 27 '10 at 01:39
  • Use `-[NSURLConnection cancel]`. – Jeremy W. Sherman Oct 27 '10 at 03:41
3

I have a slight modification to the above code.

Use this function, it works fine for me.

- (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];
}
dandan78
  • 13,328
  • 13
  • 64
  • 78
Rahul Juyal
  • 2,124
  • 1
  • 16
  • 33
  • 1
    This will write all the data to a stream when it is finished downloading. The problem the OP had was using all of the available memory with very large downloads, your answer does not address this. – Aran Mulholland Jul 14 '13 at 22:13
0

Try AFNetworking. And:

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];
Ptah
  • 906
  • 1
  • 12
  • 25