iOS Recipes - Matt Drance [69]
self.prepErrorBlock(self, [self.bodyFileOutputStream streamError]);
return;
}
}
Next up is the media file we’re uploading. Easily handled: just append its data to the working body, right? Wrong! What if the file is a 10MB image or a 100MB movie? We can’t just load that into memory as an NSData object—we’re sure to run out of memory and crash. But we need to merge this file data into our HTTP body somehow. We’ll accomplish that by using an input stream. An input stream operates on a run loop and allows us to incrementally load segments of data so we avoid exhausting resources. By setting ourselves as the input stream’s delegate, we can find out when it is ready to read more data. It’s a very similar flow to the asynchronous mode of NSURLConnection.
MultipartHTTPPost/PRPMultipartPOSTRequest.m
if (self.fileToUpload) {
NSMutableString *str = [[NSMutableString alloc] init];
[str appendString:[self preparedBoundary]];
[str appendString:@"Content-Disposition: form-data; "];
[str appendFormat:@"name=\"%@\"; ", self.fileToUpload.nameParam];
[str appendFormat:@"filename=\"%@\"\r\n", self.fileToUpload.fileName];
NSString *contentType = self.fileToUpload.contentType;
[str appendFormat:@"Content-Type: %@\r\n\r\n", contentType];
[self appendBodyString:str];
NSLog(@"Preparing to stream %@", self.fileToUpload.localPath);
NSString *path = self.fileToUpload.localPath;
NSInputStream *mediaInputStream = [[NSInputStream alloc]
initWithFileAtPath:path];
self.uploadFileInputStream = mediaInputStream;
[mediaInputStream release];
[self.uploadFileInputStream setDelegate:self];
[self.uploadFileInputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
[self.uploadFileInputStream open];
} else {
[self handleStreamCompletion];
}
Memory Management
The stream code you see in this recipe might seem like overkill for simple image uploads. However, it works perfectly fine and more importantly works exactly the same for extremely large files.
Run the MultipartHTTPPOST project under the Activity Monitor instrument to see how much memory is consumed with the supplied JPEG file. Then add a large file (for example, a 50MB movie). You should see little to no change in the app’s memory consumption at runtime, despite a much larger upload. This is the power of streams, and it’s invaluable on a platform like iOS.
The media is piped into our body file by the ‑stream:handleEvent: delegate method. We’ll receive this message indefinitely as long as the stream has more data to read; when we do, we just take that data and send it right to our body file. When the input stream reaches the end, we finalize the body by calling ‑handleStreamCompletion.
MultipartHTTPPost/PRPMultipartPOSTRequest.m
case NSStreamEventHasBytesAvailable:
len = [self.uploadFileInputStream read:buf maxLength:1024];
if (len) {
[self.bodyFileOutputStream write:buf maxLength:len];
} else {
NSLog(@"Buffer finished; wrote to %@", self.pathToBodyFile);
[self handleStreamCompletion];
}
break;
The input stream’s asynchronous behavior is why we use blocks to notify the caller of completion or errors: unlike the basic POST recipe, preparing this HTTP body is not a synchronous operation. The caller needs to wait over multiple run loop iterations for the input stream to finish its job.
At the end of the process, we tear down the media input stream and then write one last HTTP boundary to the body file. Remember, this body file is even larger than the media we just streamed, so it may not be safe to load this into memory either. Rather than set body data as we did for the basic POST recipe, we set an input stream to the body file we’ve created. When everything is done, we invoke the completion block that was passed on to ‑prepareForUploadWithCompletionBlock:errorBlock:.
MultipartHTTPPost/PRPMultipartPOSTRequest.m
- (void)handleStreamCompletion {
[self finishMediaInputStream];
[self finishRequestBody];
self.prepCompletionBlock(self);
}
- (void)finishMediaInputStream {
[self.uploadFileInputStream