Search code examples
macoscocoansurlconnectionnsdatansmutableurlrequest

NSMutableURLRequest and memory issue


Good morning, I'm developing an application that uploads some files; there are no real issues, but only one big problem: if I try to upload 1gb of files it allocates 1gb of memory, and you can understand this is not a good thing.

Here's my code: I allocate the data as NSData and then I use NSMutableURLRequest + NSURLConnection to upload them; my question is: are there any ways to do this without needing to allocate whole memory? I've searched out, but I found nothing...

_uploadDataFile is a NSData sub-class instance allocated before.

NSMutableURLRequest *request = [[[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http://my.server/page"]
                                                            cachePolicy:NSURLRequestReloadIgnoringCacheData
                                                        timeoutInterval:60.0] autorelease];

NSData *dataFile;

if (_uploadDataFile == nil) {
    NSError *error = nil;
    dataFile = [[[NSData alloc] initWithContentsOfFile:@"pathToFile" options:NSDataReadingUncached error:&error] autorelease];

    if (error) {
        NSLog(@"Error %i: %@", [error code], [error localizedDescription]);
        [self fermaUpload]; //Stop Upload
        return;
    }
}

else 
    dataFile = [_uploadDataFile file];

NSMutableDictionary *dictPost = [[[NSMutableDictionary alloc] init] autorelease];
[dictPost setValue:dataFile forKey:@"uploads[]"];

NSData *mutData = [self dataForPOSTWithDictionary:dictPost boundary:@"0194784892923" nomeFile:[@"pathToFile" lastPathComponent]];

NSString *postLength = [NSString stringWithFormat:@"%d", [mutData length]];

[request setHTTPMethod:@"POST"];
[request setValue:postLength forHTTPHeaderField:@"Content-Length"];
[request setValue:@"multipart/form-data; boundary=0194784892923" forHTTPHeaderField:@"Content-Type"];
[request setHTTPBody:mutData];
[request setHTTPShouldHandleCookies:YES];

theConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self];

and here's how - (NSData *)dataForPOSTWithDictionary:boundary:nomeFile: method looks like:

- (NSData *)dataForPOSTWithDictionary:(NSDictionary *)aDictionary boundary:(NSString *)aBoundary nomeFile:(NSString *)nomeFile {
NSArray *myDictKeys = [aDictionary allKeys];
NSMutableData *myData = [NSMutableData dataWithCapacity:1];
NSString *myBoundary = [NSString stringWithFormat:@"--%@\r\n", aBoundary];

for(int i = 0;i < [myDictKeys count];i++) {
    id myValue = [aDictionary valueForKey:[myDictKeys objectAtIndex:i]];
    [myData appendData:[myBoundary dataUsingEncoding:NSUTF8StringEncoding]];
    //if ([myValue class] == [NSString class]) {
    if ([myValue isKindOfClass:[NSString class]]) {
        [myData appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", [myDictKeys objectAtIndex:i]] dataUsingEncoding:NSUTF8StringEncoding]];
        [myData appendData:[[NSString stringWithFormat:@"%@", myValue] dataUsingEncoding:NSUTF8StringEncoding]];
    } else if(([myValue isKindOfClass:[NSURL class]]) && ([myValue isFileURL])) {
        [myData appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", [myDictKeys objectAtIndex:i], [[myValue path] lastPathComponent]] dataUsingEncoding:NSUTF8StringEncoding]];
        [myData appendData:[[NSString stringWithFormat:@"Content-Type: application/octet-stream\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
        [myData appendData:[NSData dataWithContentsOfFile:[myValue path]]];
    } else if(([myValue isKindOfClass:[NSData class]])) {
        [myData appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", [myDictKeys objectAtIndex:i], nomeFile] dataUsingEncoding:NSUTF8StringEncoding]];
        [myData appendData:[[NSString stringWithFormat:@"Content-Type: application/octet-stream\r\n\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
        [myData appendData:myValue];
    } // eof if()

    [myData appendData:[[NSString stringWithString:@"\r\n"] dataUsingEncoding:NSUTF8StringEncoding]];
} // eof for()
[myData appendData:[[NSString stringWithFormat:@"--%@--\r\n", aBoundary] dataUsingEncoding:NSUTF8StringEncoding]];

return myData; } // eof dataForPOSTWithDictionary:boundary:

Thank you and sorry for my english.


Solution

  • A most interesting problem! There are two ways of feeding body data into NSURLConnection/Request:

    • NSData
    • NSInputStream

    Unfortunately, neither does what you want out of the box, since you want to upload a file + some form data. Ideally, you could subclass NSInputStream to provide the data formatted as you require. However, the last time I checked, NSURLConnection is unable to handle such subclasses. Instead, do something like:

    1. Create a pair of streams using CFStreamCreateBoundPair(). This returns a CFWriteStream and CFReadStream, which are toll-free bridged to NSOutputStream and NSInputStream, respectively
    2. Set yourself up as the output stream's delegate
    3. -setHTTPBodyStream: on the URL request with the input stream
    4. Start the connection

    The output stream's delegate should now be called periodically, requesting data. You need to feed in the POST data, followed by chunks of file data. Thus the entire file is never entirely in memory at once.