Download Multiple Images Sequentially using NSURLSession downloadTask in Objective C - ios

My app offers the option to download 3430 high resolution images from our server, each image of size 50k - 600k bytes.
The original approach was to just download all of them - but we realized that gave a lot of NSURLErrorTimedOut errors and crashed our program. We then implemented it such that we download all of the images, but in batches of 100 images at a time. Someone on SO suggested we actually implement our download like this:
Create a list of all file URLs that need to be downloaded.
Write your code so that it downloads these URLs sequentially. I.e. do
not let it start downloading a file until the previous one has
finished (or failed and you decided to skip it for now).
Use NSURLSession's support for downloading an individual file to a
folder, don't use the code to get an NSData and save the file
yourself. That way, your application doesn't need to be running while
the download finishes.
Ensure that you can tell whether a file has already been downloaded or
not, in case your download gets interrupted, or the phone is restarted
in mid-download. You can e.g. do this by comparing their names (if
they are unique enough), or saving a note to a plist that lets you
match a downloaded file to the URL where it came from, or whatever
constitutes an identifying characteristic in your case.
At startup, check whether all files are there. If not, put the missing
ones in above download list and download them sequentially, as in #2.
Before you start downloading anything (and that includes downloading
the next file after the previous download has finished or failed), do
a reachability check using the Reachability API from Apple's
SystemConfiguration.framework. That will tell you whether the user has
a connection at all, and whether you're on WiFi or cellular (in
general, you do not want to download a large number of files via
cellular, most cellular connections are metered).
We create a list of all images to download here:
- (void)generateImageURLList:(BOOL)batchDownloadImagesFromServer
{
NSError* error;
NSFetchRequest* leafletURLRequest = [[[NSFetchRequest alloc] init] autorelease];
NSEntityDescription* leafletURLDescription = [NSEntityDescription entityForName:#"LeafletURL" inManagedObjectContext:managedObjectContext];
[leafletURLRequest setEntity:leafletURLDescription];
numberOfImages = [managedObjectContext countForFetchRequest:leafletURLRequest error:&error];
NSPredicate* thumbnailPredicate = [NSPredicate predicateWithFormat:#"thumbnailLocation like %#", kLocationServer];
[leafletURLRequest setPredicate:thumbnailPredicate];
self.uncachedThumbnailArray = [managedObjectContext executeFetchRequest:leafletURLRequest error:&error];
NSPredicate* hiResPredicate = [NSPredicate predicateWithFormat:#"hiResImageLocation != %#", kLocationCache];
[leafletURLRequest setPredicate:hiResPredicate];
self.uncachedHiResImageArray = [managedObjectContext executeFetchRequest:leafletURLRequest error:&error];
}
We use NSURLSession to download an individual image to a folder by calling hitServerForUrl and implementing didFinishDownloadingToURL:
- (void)hitServerForUrl:(NSURL*)requestUrl {
NSURLSessionConfiguration *defaultConfigurationObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration:defaultConfigurationObject delegate:self delegateQueue: nil];
NSURLSessionDownloadTask *fileDownloadTask = [defaultSession downloadTaskWithURL:requestUrl];
[fileDownloadTask resume];
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
if (isThumbnail)
{
leafletURL.thumbnailLocation = kLocationCache;
}
else
{
leafletURL.hiResImageLocation = kLocationCache;
}
// Filename to write to
NSString* filePath = [leafletURL pathForImageAtLocation:kLocationCache isThumbnail:isThumbnail isRetina:NO];
// If it's a retina image, append the "#2x"
if (isRetina_) {
filePath = [filePath stringByReplacingOccurrencesOfString:#".jpg" withString:#"#2x.jpg"];
}
NSString* dir = [filePath stringByDeletingLastPathComponent];
[managedObjectContext save:nil];
NSError* error;
[[NSFileManager defaultManager] createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:&error];
NSURL *documentURL = [NSURL fileURLWithPath:filePath];
NSLog(#"file path : %#", filePath);
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
//Remove the old file from directory
}
[[NSFileManager defaultManager] moveItemAtURL:location
toURL:documentURL
error:&error];
if (error){
//Handle error here
}
}
This code calls loadImage, which calls `hitServer:
-(void)downloadImagesFromServer{
[self generateImageURLList:NO];
[leafletImageLoaderQueue removeAllObjects];
numberOfHiResImageLeft = [uncachedHiResImageArray count];
for ( LeafletURL* aLeafletURL in uncachedHiResImageArray)
{
//// Do the same thing again, except set isThumb = NO. ////
LeafletImageLoader* hiResImageLoader = [[LeafletImageLoader alloc] initWithDelegate:self];
[leafletImageLoaderQueue addObject:hiResImageLoader]; // do this before making connection!! //
[hiResImageLoader loadImage:aLeafletURL isThumbnail:NO isBatchDownload:YES];
//// Adding object to array already retains it, so it's safe to release it here. ////
[hiResImageLoader release];
uncachedHiResIndex++;
NSLog(#"uncached hi res index: %ld, un cached hi res image array size: %lu", (long)uncachedHiResIndex, (unsigned long)[uncachedHiResImageArray count]);
}
}
- (void)loadImage:(LeafletURL*)leafletURLInput isThumbnail:(BOOL)isThumbnailInput isBatchDownload:(BOOL)isBatchDownload isRetina:(BOOL)isRetina
{
isRetina_ = isRetina;
if (mConnection)
{
[mConnection cancel];
[mConnection release];
mConnection = nil;
}
if (mImageData)
{
[mImageData release];
mImageData = nil;
}
self.leafletURL = leafletURLInput;
self.isThumbnail = isThumbnailInput;
NSString* location = (self.isThumbnail) ?leafletURL.thumbnailLocation :leafletURL.hiResImageLocation;
//// Check if the image needs to be downloaded from server. If it is a batch download, then override the local resources////
if ( ([location isEqualToString:kLocationServer] || (isBatchDownload && [location isEqualToString:kLocationResource])) && self.leafletURL.rawURL != nil )
{
//NSLog(#"final loadimage called server");
//// tell the delegate to get ride of the old image while waiting. ////
if([delegate respondsToSelector:#selector(leafletImageLoaderWillBeginLoadingImage:)])
{
[delegate leafletImageLoaderWillBeginLoadingImage:self];
}
mImageData = [[NSMutableData alloc] init];
NSURL* url = [NSURL URLWithString:[leafletURL pathForImageOnServerUsingThumbnail:self.isThumbnail isRetina:isRetina]];
[self hitServerForUrl:url];
}
//// if not, tell the delegate that the image is already cached. ////
else
{
if([delegate respondsToSelector:#selector(leafletImageLoaderDidFinishLoadingImage:)])
{
[delegate leafletImageLoaderDidFinishLoadingImage:self];
}
}
}
Currently, I'm trying to figure out how to download the images sequentially, such that we don't call hitServer until the last image is finished downloading. Do I need to be downloading in the background? Thank you for suggestions!

My app offers the option to download 3430 high resolution images from our server, each image of size 50k - 600k bytes.
This seems like a job for on-demand resources. Just turn these files into on-demand resources obtained from your own server, and let the system take care of downloading them in its own sweet time.

This sounds very much like an architectural issue. If you fire off downloads without limiting them of course you're going to start getting timeouts and other things. Think about other apps and what they do. Apps that give the user the ability to do multiple downloads often limit how may can occur at once. iTunes for example can queue up thousands of downloads, but only runs 3 at a time. Limiting to just one at a time will only slow things down for your users. You need a balance that consider your user's available bandwidth.
The other part of this is to again consider what your users want. Does every one of your uses want every single image? I don't know what you are offering them, but in most apps which access resources like images or music, it's up to the user what and when they download. Thus they only download what they are interested in. So I'd recommend only downloading what the users are viewing or have somehow requested they want to download.

Related

Using iCloud for app database backup, but not syncing

I'm seeking advice on using iCloud in a limited way. The docs are focused on syncing between multiple devices, which I do not need. When I say iCloud, I'm also including iCloud Drive, if that matters.
My iPhone app stores its data and state in a single SQLite database file that I want to save to iCloud (or iCloud Drive) for the purpose of backup, not for syncing changes between multiple devices. At launch, SQLite opens the database file and uses it synchronously (I'm using FMDB). The database file should remain on the device so, if iCloud is down, SQLite won't know or care. iCloud would have a shadow copy.
The iCloud copy does not always need to be current -- I'm okay with programmatically initiating an update to iCloud, and the restore if necessary. If the local database file becomes corrupted or if the user deletes the app, I would restore the file from iCloud. The file would usually be smaller than 1 MB, but might be large (100 MB), but mostly invariant.
I'm not syncing because of the data integrity risks of sharing whole SQLite files, and I cannot convert to Core Data (it's a huge app using FMDB).
I know that the iOS does daily backups to iCloud, and that would be sufficient if I could restore just my app from iCloud (and not the entire device).
I need advice on using iCloud for a shadow backup and restore, but not syncing.
UPDATE: I have made progress on this issue; I can save and restore my real SQLite file to/from iCloud. I subclassed UIDocument with these two overrides:
#implementation MyDocument
// Override this method to return the document data to be saved.
- (id)contentsForType:(NSString *)typeName error:(NSError * _Nullable *)outError
{
NSString *databasePath = ... // path to my SQLite file in Documents;
NSData *dbData = [NSData dataWithContentsOfFile: databasePath];
return dbData; // the entire database file
}
// Override this method to load the document data into the app’s data model.
- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError * _Nullable *)outError
{
NSString *databasePath = ... // path to my SQLite file in Documents;
NSData *dbData = contents;
BOOL ret = [dbData writeToFile: databasePath atomically:YES];
return YES;
}
#end
My SQLite database file is in Documents and it stays there, but I write the shadow copy to iCloud thusly:
- (void)sendToICloud
{
NSURL *ubiq = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
NSString *iCloudFileName = #"..."; // a file name I choose
NSURL *ubiquitousPackage = [[ubiq URLByAppendingPathComponent:#"Documents"]
URLByAppendingPathComponent:iCloudFileName];
MyDocument *myDoc = [[MyDocument alloc] initWithFileURL:ubiquitousPackage];
[myDoc saveToURL:[myDoc fileURL] forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
if ( success ) {
[myDoc closeWithCompletionHandler:^(BOOL success) {
}];
} else {
NSLog(#"iCloud write Syncing FAILED with iCloud");
}
}];
}
Now my issues are:
I have created a custom container in Xcode that appears in my entitlements file and in the Dev portal for the App ID. I can save documents to iCloud from either of my two devices (iPhone and iPad). But, my two devices do not see each other's files. There are numerous old threads about this with random solutions, but nothing has solved the problem.
The iCloud documentation says files are updated incrementally, but I don't know if that applies in my case. If I use a new filename for MyDocument (my iCloudFileName, above), I realize that an entire new file will be sent to iCloud. But, if I reuse the previous filename and send an updated NSData each time (as I'm doing in loadFromContents:ofType:error:), will iOS only send the parts that have changed since the last time I saved the file?
Another subject line now would be: Using iCloud for file backup across multiple devices.
I've solved my primary issue of seeing iCloud data sent by multiple devices. According to WWDC 2015 "Building Document Based App," to see all files (even those from other devices), use NSMetadataQuery, not NSFileManager. Also, the video presentation includes a warning about using NSURL because documents might be moved (use Bookmarks), but that doesn't affect my use case.
I still don't know if sending partially-changed NSData will cause incremental updates.
My UIDocument overrides and my sendToICloud method remain the same. What's new is my iCloud search.
If you aren't writing to the default iCloud Documents directory, writing will fail if the directory does not exist.
It's easier to just create it every time, thusly:
- (void)writeToiCloud
{
// ...
#define UBIQUITY_ID #"iCloud.TEAMID.bundleID.appName"
NSFileManager *defaultManager = [NSFileManager defaultManager];
NSURL *ubiq = [defaultManager URLForUbiquityContainerIdentifier:UBIQUITY_ID];
NSURL *ubiquitousFolder = [ubiq URLByAppendingPathComponent:#"MyFolder"];
[defaultManager createDirectoryAtURL:ubiquitousFolder
withIntermediateDirectories:YES
attributes:nil error:nil];
// ...
// ... forSaveOperation:UIDocumentSaveForCreating ...
}
- (void)updateFileList
{
// Use an iVar so query stays in scope
// Instance variables maintain a strong reference to the objects by default
query = [[NSMetadataQuery alloc] init];
[query setSearchScopes:#[ // use both for testing, then only DocumentsScope
NSMetadataQueryUbiquitousDataScope, // not in Documents
NSMetadataQueryUbiquitousDocumentsScope, // in Documents
]
];
// add a predicate if desired to restrict the search.
// No predicate finds everything, but Apple says:
// a query can’t be started if ... no predicate has been specified.
NSPredicate *predicate = [NSPredicate predicateWithFormat: #"%K like '*'", NSMetadataItemFSNameKey];
// NSPredicate *predicate = [NSPredicate predicateWithFormat: #"%K CONTAINS %#", NSMetadataItemPathKey, #"/MyFolder/"];
// NSPredicate *predicate = [NSPredicate predicateWithFormat: #"%K CONTAINS %#", NSMetadataItemPathKey, #"/Documents/"];
[query setPredicate:predicate];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(queryDidFinish:)
name:NSMetadataQueryDidFinishGatheringNotification
object:query];
[query startQuery];
}
- (void)queryDidFinish:(NSNotification *)notification
{
// iVar: NSMetadataQuery *query
for ( NSMetadataItem *item in [query results] ) {
NSURL *itemURL = [item valueForAttribute:NSMetadataItemURLKey];
NSLog(#"fileName: %#",itemURL.lastPathComponent);
// can't use getResourceValue:forKey:error
// that applies to URLs that represent file system resources.
if ( [itemURL.absoluteString hasSuffix:#"/"] ) {
continue; // skip directories
}
// process the itemURL here ...
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSMetadataQueryDidFinishGatheringNotification
object:query];
}

iOS Share Extension - Sending Large Video

So I am trying send a large video file (over 100 mb), and whenever I access the video file with dataWithContentsOfURL, the extension terminates. This works fine with smaller files.
How am I supposed to work around it?
if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie]){
[itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeMovie options:nil completionHandler:urlHandler];
}
NSItemProviderCompletionHandler urlHandler = ^(NSURL *item, NSError *error) {
if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeVideo] | [itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeMovie])
{
NSData *fileData = [NSData dataWithContentsOfURL:item]
// ----> fileData WORKS for small files.
// ----> for large files, extension QUITS - without any trace - and control does not proceed after this. This may be due to memory pressure?
[_shareExtensionActionsManager sendTextMessage:contentText attachmentData:fileData attachmentName:#"video-1" toChatEntity:_selectedItem completion:^(BOOL success)
{
[self.extensionContext completeRequestReturningItems:nil completionHandler:^(BOOL expired) {
exit(0);
}];
}];
}
};
From the app extension docs:
Users tend to return to the host app immediately after they finish their task in your app extension. If the task involves a potentially lengthy upload or download, you need to ensure that it can finish after your extension gets terminated.
and
After your app extension calls completeRequestReturningItems:completionHandler: to tell the host app that its request is complete, the system can terminate your extension at any time.
You will need to use NSURLSession to create a URL session that initiates a background task.
If your extension isn’t running when the background task completes, the system will launch your containing app in the background and call application:handleEventsForBackgroundURLSession:completionHandler: in your AppDelegate.
You'll also need to setup a shared container that both your extension and the containing app can access. For this, you'll want to use NSURLSessionConfiguration's sharedContainerIdentifier property to specify an identifier for the container so you can access it later.
Here's a sample from the docs that shows how you can achieve this:
NSURLSession *mySession = [self configureMySession];
NSURL *url = [NSURL URLWithString:#"http://www.example.com/LargeFile.zip"];
NSURLSessionTask *myTask = [mySession downloadTaskWithURL:url];
[myTask resume];
- (NSURLSession *) configureMySession {
if (!mySession) {
NSURLSessionConfiguration* config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#“com.mycompany.myapp.backgroundsession”];
// To access the shared container you set up, use the sharedContainerIdentifier property on your configuration object.
config.sharedContainerIdentifier = #“com.mycompany.myappgroupidentifier”;
mySession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
}
return mySession;
}
Here's a related resource that might help further.
App extensions may not have the memory capacity for this task.
Memory limits for running app extensions are significantly lower than the memory limits imposed on a foreground app. On both platforms, the system may aggressively terminate extensions because users want to return to their main goal in the host app. Some extensions may have lower memory limits than others: For example, widgets must be especially efficient because users are likely to have several widgets open at the same time.
https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/ExtensionCreation.html#//apple_ref/doc/uid/TP40014214-CH5-SW1

Amazon S3 track downloaded file in Asynchronous method in iOS

During downloading multiple files using following asynchronous method of AWS iOS, I want to keep track which image is downloaded.
Following code is used for asynchronous image download.
S3TransferManager *tm = [S3TransferManager new];
S3TransferOperation *downloadFileOperation = [tm downloadFile:storeFilePath bucket:Bucket key:downloadPath];
Above method is in for loop, so there could be n images to download.
Delegate method which notify image is downloaded
-(void)request:(AmazonServiceRequest *)request didCompleteWithResponse:(AmazonServiceResponse *)response { }
But I did not find anything, using which I can manage that which actual image is downloaded. whether it was the firstID image or second one.
Any idea on where I can put some extra data , which can be received on image download ?
You can set requestTag:
S3PutObjectRequest *putObjectRequest = [ [ S3PutObjectRequest alloc ] initWithKey:keyFile inBucket:self.s3BucketName ];
putObjectRequest.requestTag = urlStringFile;
putObjectRequest.filename = fileName;
and then analyse it:
- (void)request:(AmazonServiceRequest*) request didCompleteWithResponse:(AmazonServiceResponse*) response
{
NSLog(#"Upload finished. RequestTag = %#", request.requestTag);
}
As far as, I have to only download content from Amazon s3, I used ASIHTTPRequest.
(Though this is not maintained since 2011, but I found its very useful and easy to use for my app).
Code Example,
// Initialize network Queue.
ASINetworkQueue *networkQueue = [[ASINetworkQueue alloc] init];
[networkQueue reset];
[networkQueue setRequestDidFinishSelector:#selector(requestDone:)]; //This is where download completion will be notified.
//Initialize Request.
ASIS3ObjectRequest *request = [ASIS3ObjectRequest requestWithBucket:#"Bucket_Name" key:#"/Path/file"];
This is what I was looking, I need all the information about what I am downloading on download completion. This userInfo contains all the data, which is available on download completion.
NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] initWithObjectsAndKeys:#"Data", #"Key", nil];
request.userInfo = userInfo;
userInfo = nil;
// End of userInfo set.
[networkQueue addOperation:request]; // add request in ASINetworkQueue object. We can also add multiple request here.
And last,
[networkQueue go]; // This will start downloading.
// Delegate method, where download completion will be notified
- (void)requestDone:(ASIS3Request *)request
{
NSLog(#"UserInfo : %#", request.userInfo); // Request data, to manage which request is complete.
}
Done.

How can I check that an NSData blob is valid as resumeData for an NSURLSessionDownloadTask?

I have an app that's using background downloads with the new NSURLSession APIs. When a download cancels or fails in such a way that NSURLSessionDownloadTaskResumeData is provided, I store the data blob so that it can be resumed later. A very small amount of the time I am noticing a crash in the wild:
Fatal Exception: NSInvalidArgumentException
Invalid resume data for background download. Background downloads must use http or https and must download to an accessible file.
The error occurs here, where resumeData is the NSData blob and session is an instance of NSURLSession:
if (resumeData) {
downloadTask = [session downloadTaskWithResumeData:resumeData];
...
The data is provided by the Apple APIs, is serialized, and is then deserialized at a later point in time. It may be corrupted, but it is never nil (as the if statement checks).
How can I check ahead of time that the resumeData is invalid so that I do not let the app crash?
This is the workaround suggested by Apple:
- (BOOL)__isValidResumeData:(NSData *)data{
if (!data || [data length] < 1) return NO;
NSError *error;
NSDictionary *resumeDictionary = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:NULL error:&error];
if (!resumeDictionary || error) return NO;
NSString *localFilePath = [resumeDictionary objectForKey:#"NSURLSessionResumeInfoLocalPath"];
if ([localFilePath length] < 1) return NO;
return [[NSFileManager defaultManager] fileExistsAtPath:localFilePath];
}
Edit (iOS 7.1 is not NDA'd anymore): I got this from a Twitter exchange with an Apple engineer, he suggested what to do, and I wrote the above implementation
I have not found an answer to how to tell if the data is valid ahead of time.
However, I am presently working around the issue like so:
NSData *resumeData = ...;
NSURLRequest *originalURLRequest = ...;
NSURLSessionDownloadTask *downloadTask = nil;
#try {
downloadTask = [session downloadTaskWithResumeData:resumeData];
}
#catch (NSException *exception) {
if ([NSInvalidArgumentException isEqualToString:exception.name]) {
downloadTask = [session downloadTaskWithRequest:originalURLRequest];
} else {
#throw exception; // only swallow NSInvalidArgumentException for resumeData
}
}
actually, the resume data is a plist file.
it contains the follows key:
NSURLSessionDownloadURL
NSURLSessionResumeBytesReceived
NSURLSessionResumeCurrentRequest
NSURLSessionResumeEntityTag
NSURLSessionResumeInfoTempFileName
NSURLSessionResumeInfoVersion
NSURLSessionResumeOriginalRequest
NSURLSessionResumeServerDownloadDate
so the steps u need to do are:
check the data is a valid plist;
check the plist have keys as above;
check the temp file is exist;

iOS Amazon S3 download large files

I'm quite new to Amazon S3 and I'm having difficulty downloading large files from S3.
I have successfully downloaded a file that is 35MB every time, but when the size of the file is really big around 500 MB - 1.7GB the application crashes.
When trying on the simulator I would get can't allocate region error after about 1GB of the download.
So then I tried it on the device. Now it seems to just crash at a random time and
no crash report is put in the device, therefor I'm having an issue debugging this problem.
At first I thought it was the device or even the simulator. But i'm not really sure.
Someone mentioned that S3 framework times out the downloads randomly occasionally for large files. Could this be the case?
I'm building the file by opening a data file seeking to the end, adding the data, then closing the file until the download is complete.
I'm not sure how to debug this problem.
Any help would be appreciated.
Thank you.
I am a maintainer of the AWS SDK for iOS. We recently patched the S3GetObjectResponse to allow the streaming of the data directly to disk without keeping the response data in memory.
S3GetObjectResponse.m
To enable this, you simply need to set the stream when creating your request:
NSOutputStream *outputStream = [[[NSOutputStream alloc] initToFileAtPath:FILE_NAME append:NO] autorelease];
[outputStream open];
S3GetObjectRequest *getObjectRequest = [[[S3GetObjectRequest alloc] initWithKey:FILE_NAME withBucket:BUCKET_NAME] autorelease];
getObjectRequest.outputStream = outputStream;
[s3 getObject:getObjectRequest];
Update: We added a post to our AWS Mobile Developer Blog on downloading large files with the AWS SDK for iOS that includes this info as well as other tips.
S3GetObjectRequest has NSMutableData* body where it appends all the data it downloads.
For large files as download progresses data is appended constantly, and it goes over the VM limit of 90MB and then app gets killed by iOS.
Quick and dirty workaround is to create your own S3GetObjectRequest and S3GetObjectResponse classes. AWS framework instantiates Response based on Class Name of Request (Class name of Request without last 7 chars "Request" and appends it with "Response", and tries to instantiate new class of that name).
Then to override -(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data to release body all the time.
This is quick and dirty fix simply because you still have constant data allocation, appending and then release. But it works when you are in a pinch. For my usage of downloading files of 150-700mb, this simple hack kept memory usage of the app at 2.55mb average, +/- 0.2mb.
As stated by the author of ASIHTTP library, it is no longer maintained.
Request - LargeFileS3GetObjectRequest.h
#interface LargeFileS3GetObjectRequest : S3GetObjectRequest
#end
Request - LargeFileS3GetObjectRequest.m
#implementation LargeFileS3GetObjectRequest
#end
Response - LargeFileS3GetObjectResponse.h
#interface LargeFileS3GetObjectResponse : S3GetObjectResponse
#end
Response - LargeFileS3GetObjectResponse.m
#implementation LargeFileS3GetObjectResponse
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// allow original implementation to send data to delegates
[super connection:connection didReceiveData:data];
// release body and set it to NULL so that underlying implementation doesn't
// append on released object, but instead allocates new one
[body release];
body = NULL;
}
#end
Hope it helps.
You may want to stream the data to your application via ASIHTTPRequest
http://allseeing-i.com/ASIHTTPRequest/S3
NSString *secretAccessKey = #"my-secret-access-key";
NSString *accessKey = #"my-access-key";
NSString *bucket = #"my-bucket";
NSString *path = #"path/to/the/object";
ASIS3ObjectRequest *request = [ASIS3ObjectRequest requestWithBucket:bucket key:path];
[request setSecretAccessKey:secretAccessKey];
[request setAccessKey:accessKey];
[request startSynchronous];
if (![request error]) {
NSData *data = [request responseData];
} else {
NSLog(#"%#",[[request error] localizedDescription]);
}
/* Set up the Amazon client */
_s3 = [[AmazonS3Client alloc] initWithAccessKey:k_Amazon_ACCESS_KEY_ID withSecretKey:k_Amazon_SECRET_KEY];
_s3.endpoint = [AmazonEndpoints s3Endpoint:SA_EAST_1];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
/* Open a file stream for the download */
NSOutputStream *outputStream = [[NSOutputStream alloc] initToFileAtPath:[DOCUMENTS_DIRECTORY stringByAppendingPathComponent:k_Amazon_Video_Local_File_Name] append:NO];
[outputStream open];
/* Set up the s3 get object */
S3GetObjectRequest *getVideoRequest = [[S3GetObjectRequest alloc] initWithKey:k_Amazon_Video_Path withBucket:#""];
/* Set the stream */
getVideoRequest.outputStream = outputStream;
/* Get the response from Amazon */
S3GetObjectResponse *getObjectResponse = [_s3 getObject:getVideoRequest];
dispatch_async(dispatch_get_main_queue(), ^{
if(getObjectResponse.error != nil)
{
NSLog(#"S3 Error: %#", getObjectResponse.error);
}
else
{
NSLog(#"S3 - Video download complete and successful");
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:k_Amazon_Video_Downloaded];
}
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
});
});

Resources