I've recently implemented the new AWS 2.0 iOS SDK in my application (yay, cocoapods!), and using the sample code from Amazon managed to properly configure access and downloads. I can successfully download a single item without issue, but I need to be able to download multiple files dynamically generated based on the current tableview. There doesn't appear to be a way to set up a batch download, so I'm simply trying to loop through an array of objects and trigger a download with each one. It works, but if the list includes more than a few items, it starts randomly misfiring. For example, if my dynamically created list has 14 items in it, 12 will be downloaded, and the other 2 aren't even attempted. The request just vanishes. In my testing, I added a sleep(1) timer, and then all 14 are triggered and downloaded, so I'm guessing that I'm overwhelming the download requests and they are getting dropped unless I slow it down. Slowing it down is not ideal... perhaps there is another way? Here is the code:
- (IBAction)downloadAllPics:(UIBarButtonItem *)sender {
if (debug==1) {
NSLog(#"Running %# '%#'", self.class, NSStringFromSelector(_cmd));
}
CoreDataHelper *cdh =
[(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
// for loop iterates through all of the items in the tableview
for (Item *item in self.frc.fetchedObjects) {
NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *downloadingFilePath1 = [NSString stringWithFormat:#"%#/%##2x.jpg",docDir, item.imageName];
NSURL *downloadingFileURL1 = [NSURL fileURLWithPath:downloadingFilePath1];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
if ([fileManager fileExistsAtPath:downloadingFilePath1]) {
fileAlreadyExists = TRUE;
if (![fileManager removeItemAtPath:downloadingFilePath1
error:&error]) {
NSLog(#"Error: %#", error);
}
}
__weak typeof(self) weakSelf = self;
self.downloadRequest1 = [AWSS3TransferManagerDownloadRequest new];
self.downloadRequest1.bucket = S3BucketName;
// self.downloadRequest1.key = S3KeyDownloadName1;
self.downloadRequest1.key = [NSString stringWithFormat:#"images/%##2x.jpg", item.imageName];
self.downloadRequest1.downloadingFileURL = downloadingFileURL1;
self.downloadRequest1.downloadProgress = ^(int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite){
// update progress
dispatch_sync(dispatch_get_main_queue(), ^{
weakSelf.file1AlreadyDownloaded = totalBytesWritten;
weakSelf.file1Size = totalBytesExpectedToWrite;
});
};
// this launches the actual S3 transfer manager - it is successfully launched with each pass of loop
[self downloadFiles];
}
[cdh backgroundSaveContext];
}
That launches the downloadFiles method:
- (void) downloadFiles {
//if I add this sleep, all 14 download. If I don't usually 11-13 download.
sleep(1);
AWSS3TransferManager *transferManager = [AWSS3TransferManager defaultS3TransferManager];
__block int downloadCount = 0;
[[transferManager download:self.downloadRequest1] continueWithExecutor:[BFExecutor mainThreadExecutor] withBlock:^id(BFTask *task) {
if (task.error != nil){
if(task.error.code != AWSS3TransferManagerErrorCancelled && task.error.code != AWSS3TransferManagerErrorPaused){
NSLog(#"%s Errorx: [%#]",__PRETTY_FUNCTION__, task.error);
}
} else {
self.downloadRequest1 = nil;
}
return nil;
}];
}
There has got to be a way to download a dynamic list of files from an Amazon S3 bucket, right? Maybe there is a transfer manager that allows an array of files instead of doing them individually?
Any and all help is appreciated.
Zack
Sounds like request timeout interval setting issue.
First, when you configure AWSServiceConfiguration *configuration = ... try to configure the timeoutIntervalForRequest property. Also, maxRetryCount as well. maxRetryCount will attempt to download if failure for downloading each operation.
AWSServiceConfiguration *configuration = [[AWSServiceConfiguration alloc] initWithRegion:DefaultServiceRegionType
credentialsProvider:credentialsProvider];
[configuration setMaxRetryCount:2]; // 10 is the max
[configuration setTimeoutIntervalForRequest:120]; // 120 seconds
Second, for the multiple items downloading try to collect each AWSTask into one array and get the result at the end of group operation. ex)
// task collector
NSMutableSet *uniqueTasks = [NSMutableSet new];
// Loop
for (0 -> numOfDownloads) {
AWSS3TransferManagerDownloadRequest *downloadRequest = [AWSS3TransferManagerDownloadRequest new];
[downloadRequest setBucket:S3BucketNameForProductImage];
[downloadRequest setKey:filename];
[downloadRequest setDownloadingFileURL:sourceURL];
[showroomGroupDownloadRequests addObject:downloadRequest];
AWSTask *task = [[AWSS3TransferManager defaultS3TransferManager] download:downloadRequest];
[task continueWithBlock:^id(AWSTask *task) {
// handle each individual operation
if (task.error == nil) {
}
else if (task.error) {
}
// add to the tasks
[uniqueTasks addObject:task];
return nil;
}
[[AWSTask taskForCompletionOfAllTasks:tasks] continueWithBlock:^id(AWSTask *task) {
if (task.error == nil) {
// all downloads succeess
}
else if (task.error != nil) {
// failure happen one of download
}
return nil;
}];
The reason some requests seem to vanish is that you define AWSS3TransferManagerDownloadRequest as a property. self.downloadRequest1 = nil; is executed on the background thread, and it is possible that when [transferManager download:self.downloadRequest1] is executed, self.downloadRequest1 is nil.
You should remove the property and simply pass an instance of AWSS3TransferManagerDownloadRequest as an argument for - downloadFiles:.
Related
I've managed to successfully upload data using the AWS 2.0 SDK, however I'm having some trouble understanding the "re-wiring" of completion handlers that is meant to take place on resuming of the app.
What I believe I should be doing, is at comment 6, saving the upload task. Then if it reaches the completionHandler block, deleting it. However, if the app is terminated before this, I can look at my saved upload tasks in the block at comment 4. Any saved tasks would be "re-wired" to the completion handler.
For reference, imagine this code happening in an "Upload" class, and in a block, which is the "uploadSuccess()" code you see in the completionHandler.
For reference here is the blog that introduced the AWSS3TransferUtility, and here is its documentation.
Hopefully someone can guide me on this.
// 1. Get/Setup credentials
AWSCognitoCredentialsProvider *credentialsProvider = [[AWSCognitoCredentialsProvider alloc] initWithRegionType:AWSRegionUSEast1 identityPoolId:#"IdentityPoolId"];
AWSServiceConfiguration *configuration = [[AWSServiceConfiguration alloc] initWithRegion:AWSRegionUSEast1 credentialsProvider:credentialsProvider];
AWSServiceManager.defaultServiceManager.defaultServiceConfiguration = configuration;
// 2. Create AWSS3 Transfer blocks
AWSS3TransferUtilityUploadExpression *expression = [AWSS3TransferUtilityUploadExpression new];
expression.uploadProgress = ^(AWSS3TransferUtilityTask *task, int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) {
dispatch_async(dispatch_get_main_queue(), ^{
// Do something e.g. Update a progress bar.
});
};
AWSS3TransferUtilityUploadCompletionHandlerBlock completionHandler = ^(AWSS3TransferUtilityUploadTask *task, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
// Do something e.g. Alert a user for transfer completion.
// On failed uploads, `error` contains the error object.
if (error) {
uploadSuccess(NO);
} else {
uploadSuccess(YES);
}
});
};
// 3. Create Transfer Utility
AWSS3TransferUtility *transferUtility = [AWSS3TransferUtility defaultS3TransferUtility];
// 4. Rewire Transfer Utility blocks
[transferUtility
enumerateToAssignBlocksForUploadTask:^(AWSS3TransferUtilityUploadTask *uploadTask, __autoreleasing AWSS3TransferUtilityUploadProgressBlock *uploadProgressBlockReference, __autoreleasing AWSS3TransferUtilityUploadCompletionHandlerBlock *completionHandlerReference) {
NSLog(#"Upload task identifier = %lu", (unsigned long)uploadTask.taskIdentifier);
// Use `uploadTask.taskIdentifier` to determine what blocks to assign.
//*uploadProgressBlockReference = // Reassign your progress feedback block.
*completionHandlerReference = completionHandler;// Reassign your completion handler.
}
downloadTask:^(AWSS3TransferUtilityDownloadTask *downloadTask, __autoreleasing AWSS3TransferUtilityDownloadProgressBlock *downloadProgressBlockReference, __autoreleasing AWSS3TransferUtilityDownloadCompletionHandlerBlock *completionHandlerReference) {
}];
// 5. Upload data using Transfer Utility
[[transferUtility uploadData:myNSDataObject
bucket:#"bucketName"
key:#"keyName"
contentType:#"text/plain"
expression:expression
completionHander:completionHandler] continueWithBlock:^id(AWSTask *task) {
if (task.error) {
NSLog(#"Error: %#", task.error);
}
if (task.exception) {
NSLog(#"Exception: %#", task.exception);
}
if (task.result) {
AWSS3TransferUtilityUploadTask *uploadTask = task.result;
// 6. Should i be saving uploadTasks here?
}
return nil;
}];
I am developing an chat app where I transfer media like videos / audio/ images.
When the transfer is done with below code snippet, the transfer is smooth on Wifi but is very very slow when moved to 3G network.
Also another issue is when I go in background the AWS SDK pause the transfer and resumes when I come in foreground.
1) How can I make the transfer fast as I have tried changing the AWS region still no success.
2) Also use the S3Background transfer example compared to AWS S3 transfer manager used but the download paused when I press home button.
Below is my code snipped.
//Upload function
AWSS3TransferManager *transferManager = [AWSS3TransferManager defaultS3TransferManager];
AWSS3TransferManagerUploadRequest *uploadRequest = [AWSS3TransferManagerUploadRequest new];
uploadRequest.bucket = S3BucketName;
User *userMO = .....my user details
NSString *fileName = [[filePath componentsSeparatedByString:#"/"] lastObject];
uploadRequest.key = [userMO.user_pool_id stringByAppendingPathComponent:fileName];
NSURL *uploadFileURL = [NSURL fileURLWithPath:filePath];
uploadRequest.body = uploadFileURL;
uploadRequest.expires = [NSDate dateWithTimeIntervalSinceNow:3600];
NSString *fileContentTypeStr = #"image/jpg";
if ([FileManager isVideoFile:fileName]) {
// fileContentTypeStr = [NSString stringWithFormat:#"video/%#",[[fileName pathExtension] lowercaseString]];
fileContentTypeStr = [NSString stringWithFormat:#"video/quicktime"];
}
else if ([FileManager isImageFile:fileName]) {
fileContentTypeStr = [NSString stringWithFormat:#"image/%#",[[fileName pathExtension] lowercaseString]];
}
else if ([FileManager isAudioFile:fileName]) {
fileContentTypeStr = [NSString stringWithFormat:#"audio/%#",[[fileName pathExtension] lowercaseString]];
}
uploadRequest.contentType = fileContentTypeStr;
[[transferManager upload:uploadRequest] continueWithBlock:^id(AWSTask *task) {
if (task.error) {
if ([task.error.domain isEqualToString:AWSS3TransferManagerErrorDomain]) {
switch (task.error.code) {
case AWSS3TransferManagerErrorCancelled:
{
//updating ui to notify cancel
}
break;
case AWSS3TransferManagerErrorPaused:
{
//updating ui to notify paused
}
break;
default:
{
NSLog(#"Upload task failed: %#",[task.error localizedDescription]);
//updating ui to notify failed
}
break;
}
} else {
NSLog(#"Upload task failed: %#",[task.error localizedDescription]);
//updating ui to notify failed
}
}
if (task.result)
{
//updating ui to notify finished
}
return nil;
}];
if(uploadRequest.state == AWSS3TransferManagerRequestStateRunning)
{
//updating ui to notify progress
}
//Add the request to the current upload cache handler entry as handle
Any body have any suggestions and faced similar issue.
Use AWS background transfer not AWS transfer manager to use the background mode supported transfer.
i had to convert big file size song from iTunes library to a smaller 8K song file.
As i did the converting async, the bool always return true even though writing to doc folder are not completed. At the moment i'm using a delay of 10sec before i called the function again and it works fine on the interim for iPhone 5s, but i would like to cater on the slower devices.
kindly give me some pointer / recommendation on my code.
-(void)startUploadSongAnalysis
{
[self updateProgressYForID3NForUpload:NO];
if ([self.uploadWorkingAray count]>=1)
{
Song *songVar = [self.uploadWorkingAray objectAtIndex:0];//core data var
NSLog(#"songVar %#",songVar.songName);
NSLog(#"songVar %#",songVar.songURL);
NSURL *songU = [NSURL URLWithString:songVar.songURL]; //URL of iTunes Lib
// self.asset = [AVAsset assetWithURL:songU];
// NSLog(#"asset %#",self.asset);
NSError *error;
NSString *subString = [[songVar.songURL componentsSeparatedByString:#"id="] lastObject];
NSString *savedPath = [self.documentsDir stringByAppendingPathComponent:[NSString stringWithFormat:#"audio%#.m4a",subString]];//save file name of converted 8kb song
NSString *subStringPath = [NSString stringWithFormat:#"audio%#.m4a",subString];
if ([self.fileManager fileExistsAtPath:savedPath] == YES)
[self.fileManager removeItemAtPath:savedPath error:&error];
NSLog(#"cacheDir %#",savedPath);
//export low bitrate song to cache
if ([self exportAudio:[AVAsset assetWithURL:songU] toFilePath:savedPath]) // HERE IS THE PROBLEM, this return true even the writing is not completed cos when i upload to my web server, it will say song file corrupted
{
// [self performSelector:#selector(sendSongForUpload:) withObject:subStringPath afterDelay:1];
[self sendRequest:2 andPath:subStringPath andSongDBItem:songVar];
}
else
{
NSLog(#"song too short, skipped");
[self.uploadWorkingAray removeObjectAtIndex:0];
[self.songNotFoundArray addObject:songVar];
[self startUploadSongAnalysis];
}
}
else //uploadWorkingAray empty
{
NSLog(#"save changes");
[[VPPCoreData sharedInstance] saveAllChanges];
}
}
#pragma mark song exporter to doc folder
- (BOOL)exportAudio:(AVAsset *)avAsset toFilePath:(NSString *)filePath
{
CMTime assetTime = [avAsset duration];
Float64 duration = CMTimeGetSeconds(assetTime);
if (duration < 40.0) return NO; // if song too short return no
// get the first audio track
NSArray *tracks = [avAsset tracksWithMediaType:AVMediaTypeAudio];
if ([tracks count] == 0) return NO;
NSError *readerError = nil;
AVAssetReader *reader = [[AVAssetReader alloc] initWithAsset:avAsset error:&readerError];
//AVAssetReader *reader = [AVAssetReader assetReaderWithAsset:avAsset error:&readerError]; // both works the same ?
AVAssetReaderOutput *readerOutput = [AVAssetReaderAudioMixOutput
assetReaderAudioMixOutputWithAudioTracks:avAsset.tracks
audioSettings: nil];
if (! [reader canAddOutput: readerOutput])
{
NSLog (#"can't add reader output...!");
return NO;
}
else
{
[reader addOutput:readerOutput];
}
// writer AVFileTypeCoreAudioFormat AVFileTypeAppleM4A
NSError *writerError = nil;
AVAssetWriter *writer = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:filePath]
fileType:AVFileTypeAppleM4A
error:&writerError];
//NSLog(#"writer %#",writer);
AudioChannelLayout channelLayout;
memset(&channelLayout, 0, sizeof(AudioChannelLayout));
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
// use different values to affect the downsampling/compression
// NSDictionary *outputSettings = [NSDictionary dictionaryWithObjectsAndKeys:
// [NSNumber numberWithInt: kAudioFormatMPEG4AAC], AVFormatIDKey,
// [NSNumber numberWithFloat:16000.0], AVSampleRateKey,
// [NSNumber numberWithInt:2], AVNumberOfChannelsKey,
// [NSNumber numberWithInt:128000], AVEncoderBitRateKey,
// [NSData dataWithBytes:&channelLayout length:sizeof(AudioChannelLayout)], AVChannelLayoutKey,
// nil];
NSDictionary *outputSettings = #{AVFormatIDKey: #(kAudioFormatMPEG4AAC),
AVEncoderBitRateKey: #(8000),
AVNumberOfChannelsKey: #(1),
AVSampleRateKey: #(8000)};
AVAssetWriterInput *writerInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:outputSettings];
//\Add inputs to Write
NSParameterAssert(writerInput);
NSAssert([writer canAddInput:writerInput], #"Cannot write to this type of audio input" );
if ([writer canAddInput:writerInput])
{
[writer addInput:writerInput];
}
else
{
NSLog (#"can't add asset writer input... die!");
return NO;
}
[writerInput setExpectsMediaDataInRealTime:NO];
[writer startWriting];
[writer startSessionAtSourceTime:kCMTimeZero];
[reader startReading];
__block UInt64 convertedByteCount = 0;
__block BOOL returnValue;
__block CMSampleBufferRef nextBuffer;
dispatch_queue_t mediaInputQueue = dispatch_queue_create("mediaInputQueue", NULL);
[writerInput requestMediaDataWhenReadyOnQueue:mediaInputQueue usingBlock:^{
// NSLog(#"Asset Writer ready : %d", writerInput.readyForMoreMediaData);
while (writerInput.readyForMoreMediaData)
{
nextBuffer = [readerOutput copyNextSampleBuffer];
if (nextBuffer)
{
[writerInput appendSampleBuffer: nextBuffer];
convertedByteCount += CMSampleBufferGetTotalSampleSize (nextBuffer);
//NSNumber *convertedByteCountNumber = [NSNumber numberWithLong:convertedByteCount];
//NSLog (#"writing");
CFRelease(nextBuffer);
}
else
{
[writerInput markAsFinished];
[writer finishWritingWithCompletionHandler:^{
if (AVAssetWriterStatusCompleted == writer.status)
{
NSLog(#"Writer completed");
returnValue = YES; //I NEED TO RETURN SOMETHING FROM HERE AFTER WRITING COMPLETED
dispatch_async(mediaInputQueue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
// add this to the main queue as the last item in my serial queue
// when I get to this point I know everything in my queue has been run
NSDictionary *outputFileAttributes = [[NSFileManager defaultManager]
attributesOfItemAtPath:filePath
error:nil];
NSLog (#"done. file size is %lld",
[outputFileAttributes fileSize]);
});
});
}
else if (AVAssetWriterStatusFailed == writer.status)
{
[writer cancelWriting];
[reader cancelReading];
NSLog(#"Writer failed");
return;
}
else
{
NSLog(#"Export Session Status: %d", writer.status);
}
}];
break;
}
}
}];
tracks = nil;
writer = nil;
writerInput = nil;
reader = nil;
readerOutput=nil;
mediaInputQueue = nil;
return returnValue;
//return YES;
}
Your method exportAudio:toFilePath: is actually an asynchronous method and requires a few fixes to become a proper asynchronous method.
First, you should provide a completion handler in order to signal the call-site that the underlying task has been finished:
- (void)exportAudio:(AVAsset *)avAsset
toFilePath:(NSString *)filePath
completion:(completion_t)completionHandler;
Note, that the result of the method is passed through the completion handler, whose signature might be as follows:
typedef void (^completion_t)(id result);
where parameter result is the eventual result of the method. You should always return an NSError object when anything goes wrong when setting up the various objects within the method - even though, the method could return an immediate result indicating an error.
Next, if you take a look into to documentation you can read:
requestMediaDataWhenReadyOnQueue:usingBlock:
- (void)requestMediaDataWhenReadyOnQueue:(dispatch_queue_t)queue
usingBlock:(void (^)(void))block
Discussion
The block should append media data to the input either until the input’s readyForMoreMediaData property becomes NO or until there is no more media data to supply (at which point it may choose to mark the input as finished using markAsFinished). The block should then exit. After the block exits, if the input has not been marked as finished, once the input has processed the media data it has received and becomes ready for more media data again, it will invoke the block again in order to obtain more.
You should now be quite sure when your task is actually finished. You determine this within the block which is passed to the method requestMediaDataWhenReadyOnQueue:usingBlock:.
When the task is finished you call the completion handler completionHandler provided in
method exportAudio:toFilePath:completion:.
Of course, you need to fix your implementation, e.g. having the method ending with
tracks = nil;
writer = nil;
writerInput = nil;
reader = nil;
readerOutput=nil;
mediaInputQueue = nil;
return returnValue;
//return YES;
}
makes certainly no sense. Cleaning up and returning a result shall be done when the asynchronous task is actually finished. Unless an error occurs during setup, you need to determine this in the block passed to the method requestMediaDataWhenReadyOnQueue:usingBlock:.
In any case, in order to signal the result to the call-site call the completion handler completionHandler and pass a result object, e.g. if it succeeded the URL where it has been saved, otherwise an NSError object.
Now, since our method startUploadSongAnalysis is calling an asynchronous method, this method inevitable becomes asynchronous as well!
If I understood your original code correctly, you are invoking it recursively in order to process a number of assets. In order to implement this correctly, you need a few fixes shown below. The resulting "construct" is NOT a recursive method though, but instead an iteratively invocation of an asynchronous method ("asynchronous loop").
You may or may not provide a completion handler - same as above. It's up to you - but I would recommend it, it won't hurt to know when all assets have been processed. It may look as follows:
-(void)startUploadSongAnalysisWithCompletion:(completion_t)completionHandler
{
[self updateProgressYForID3NForUpload:NO];
// *** check for break condition: ***
if ([self.uploadWorkingAray count]>=1)
{
... stuff
//export low bitrate song to cache
[self exportAudio:[AVAsset assetWithURL:songU]
toFilePath:savedPath
completion:^(id urlOrError)
{
if ([urlOrError isKindOfClass[NSError class]]) {
// Error occurred:
NSLog(#"Error: %#", urlOrError);
// There are two alternatives to proceed:
// A) Ignore or remember the error and proceed with the next asset.
// In this case, it would be best to have a result array
// containing all the results. Then, invoke
// startUploadSongAnalysisWithCompletion: in order to proceed
// with the next asset.
//
// B) Stop with error.
// Don't call startUploadSongAnalysisWithCompletion: but
// instead invoke the completion handler passing it the error.
// A:
// possibly dispatch to a sync queue or the main thread!
[self.uploadWorkingAray removeObjectAtIndex:0];
[self.songNotFoundArray addObject:songVar];
// *** next song: ***
[self startUploadSongAnalysisWithCompletion:completionHandler];
}
else {
// Success:
// *** next song: ***
NSURL* url = urlOrError;
[self startUploadSongAnalysisWithCompletion:completionHandler];
}
}];
}
else //uploadWorkingAray empty
{
NSLog(#"save changes");
[[VPPCoreData sharedInstance] saveAllChanges];
// *** signal completion ***
if (completionHandler) {
completionHandler(#"OK");
}
}
}
I am not sure, but can not you send a call to a method like following
dispatch_async(mediaInputQueue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
// add this to the main queue as the last item in my serial queue
// when I get to this point I know everything in my queue has been run
NSDictionary *outputFileAttributes = [[NSFileManager defaultManager]
attributesOfItemAtPath:filePath
error:nil];
NSLog (#"done. file size is %lld",
[outputFileAttributes fileSize]);
//calling the following method after completing the queue
[self printMe];
});
});
-(void)printMe{
NSLog(#"queue complete...");
//Do the next job, may be the following task !!!
if ([self exportAudio:[AVAsset assetWithURL:songU] toFilePath:savedPath]) // HERE IS THE PROBLEM, this return true even the writing is not completed cos when i upload to my web server, it will say song file corrupted
{
// [self performSelector:#selector(sendSongForUpload:) withObject:subStringPath afterDelay:1];
[self sendRequest:2 andPath:subStringPath andSongDBItem:songVar];
}
else
{
NSLog(#"song too short, skipped");
[self.uploadWorkingAray removeObjectAtIndex:0];
[self.songNotFoundArray addObject:songVar];
[self startUploadSongAnalysis];
}
}
Note: I'm using ARC.
I have some code that makes 1 request to an http server for a list of files (via JSON). It then parses that list into model objects which it uses to add a download operation (for downloading that file) to a different nsoperationqueue and then once it's done adding all of those operations (queue starts out suspended) it kicks off the queue and waits for all the operations to finish before continuing. (Note: this is all done on background threads so as not to block the main thread).
Here's the basic code:
NSURLRequest* request = [NSURLRequest requestWithURL:parseServiceUrl];
AFHTTPRequestOperation *op = [[AFHTTPRequestOperation alloc] initWithRequest:request];
op.responseSerializer = [AFJSONResponseSerializer serializer];
[op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
//NSLog(#"JSON: %#", responseObject);
// Parse JSON into model objects
NSNumber* results = [responseObject objectForKey:#"results"];
if ([results intValue] > 0)
{
dispatch_async(_processQueue, ^{
_totalFiles = [results intValue];
_timestamp = [responseObject objectForKey:#"timestamp"];
NSArray* files = [responseObject objectForKey:#"files"];
for (NSDictionary* fileDict in files)
{
DownloadableFile* file = [[DownloadableFile alloc] init];
file.file_id = [fileDict objectForKey:#"file_id"];
file.file_location = [fileDict objectForKey:#"file_location"];
file.timestamp = [fileDict objectForKey:#"timestamp"];
file.orderInQueue = [files indexOfObject:fileDict];
NSNumber* action = [fileDict objectForKey:#"action"];
if ([action intValue] >= 1)
{
if ([file.file_location.lastPathComponent.pathExtension isEqualToString:#""])
{
continue;
}
[self downloadSingleFile:file];
}
else // action == 0 so DELETE file if it exists
{
if ([[NSFileManager defaultManager] fileExistsAtPath:file.localPath])
{
NSError* error;
[[NSFileManager defaultManager] removeItemAtPath:file.localPath error:&error];
if (error)
{
NSLog(#"Error deleting file after given an Action of 0: %#: %#", file.file_location, error);
}
}
}
[self updateProgress:[files indexOfObject:fileDict] withTotal:[files count]];
}
dispatch_sync(dispatch_get_main_queue(), ^{
[_label setText:#"Syncing Files..."];
});
[_dlQueue setSuspended:NO];
[_dlQueue waitUntilAllOperationsAreFinished];
[SettingsManager sharedInstance].timestamp = _timestamp;
dispatch_async(dispatch_get_main_queue(), ^{
callback(nil);
});
});
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
callback(nil);
});
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
callback(error);
}];
[_parseQueue addOperation:op];
and then the downloadSingleFile method:
- (void)downloadSingleFile:(DownloadableFile*)dfile
{
NSURLRequest* req = [NSURLRequest requestWithURL:dfile.downloadUrl];
AFHTTPRequestOperation* reqOper = [[AFHTTPRequestOperation alloc] initWithRequest:req];
reqOper.responseSerializer = [AFHTTPResponseSerializer serializer];
[reqOper setCompletionBlockWithSuccess:^(AFHTTPRequestOperation* op, id response)
{
__weak NSData* fileData = response;
NSError* error;
__weak DownloadableFile* file = dfile;
NSString* fullPath = [file.localPath substringToIndex:[file.localPath rangeOfString:file.localPath.lastPathComponent options:NSBackwardsSearch].location];
[[NSFileManager defaultManager] createDirectoryAtPath:fullPath withIntermediateDirectories:YES attributes:Nil error:&error];
if (error)
{
NSLog(#"Error creating directory path: %#: %#", fullPath, error);
}
else
{
error = nil;
[fileData writeToFile:file.localPath options:NSDataWritingFileProtectionComplete error:&error];
if (error)
{
NSLog(#"Error writing fileData for file: %#: %#", file.file_location, error);
}
}
[self updateProgress:file.orderInQueue withTotal:_totalFiles];
}
failure:^(AFHTTPRequestOperation* op, NSError* error)
{
[self updateProgress:dfile.orderInQueue withTotal:_totalFiles];
NSLog(#"Error downloading %#: %#", dfile.downloadUrl, error.localizedDescription);
}];
[_dlQueue addOperation:reqOper];
}
What I'm seeing is a constant spike in memory as more files get downloaded. It's like the responseObject or maybe even the whole completionBlock is not being let go of.
I've tried making the responseObject __weak as well as fileData. I've tried adding an autoreleasepool and I've tried making the actual file domain object __weak too but still memory climbs and climbs.
I've run Instruments and not seen any leaks persay but it never gets to a point where all the files have been downloaded before it runs out of memory with a big fat "can't allocate region" error. Looking at allocations, I see a bunch of connection:didFinishLoading and connection:didReceiveData methods that never seem to be let go of, however. I can't seem to debug it further than that though.
My question: Why is it running out of memory? What is not getting deallocated and how can I get it to do such?
There is a few things going on here. The biggest is that you are downloading the entire file, storing it in memory, and then writing it out to disk when the download is complete. Even with just one file of 500 MB, you will run out of memory.
The correct way to do this is using an NSOutputStream with asynchronous downloads. The key is to write out the data as soon as it arrives. It should look like this:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[self.outputStream write:[data bytes] maxLength:[data length]];
}
Also of note, you are creating your weak references inside the block, not outside. Because of that, you are still creating a retain cycle and leaking memory. When you create weak references, it should look like this.
NSOperation *op = [[NSOperation alloc] init];
__weak NSOperation *weakOp = op;
op.completion = ^{
// Use only weakOp within this block
};
Lastly, your code is using #autoreleasepool. NSAutoreleasePool, and the ARC equivalent #autoreleasepool are only useful in very limited situations. As a general rule, if you aren't absolutely sure you need one, you don't.
With the help of a friend, I was able to figure out the problem.
The problem was actually in the first block of code:
[_dlQueue waitUntilAllOperationsAreFinished];
Apparently , waiting for all operations to finish meant none of those operations would be released either.
Instead of that, I ended up adding a final operation to the queue that would do the final processing and callback and memory is much more stable now.
[_dlQueue addOperationWithBlock:^{
[SettingsManager sharedInstance].timestamp = _timestamp;
dispatch_async(dispatch_get_main_queue(), ^{
callback(nil);
});
}];
What kind of file you are downloading? If you are working with Images or videos you nee to clear URLCache as when you doneload images it create CFDATA and some information in cache and it does not cleared out. You need to clear it explicitly when your single file download completed. It will never caught as a leak also.
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
[sharedCache release];
If you are using ARC replace
[sharedCache release];
with
sharedCache = nil;
Hope It may help you.
I am building an app for displaying assets (PDF, Video, Etc).
It starts by downloading a JSON and parsing it into Core Data Objects <-- This part works fine.
These objects are a hierarchical set of Nodes that have a relationships set up in my model. each node can either be a FILE or a FOLDER. <-- no problems.
Then I have instance methods built into my NSManagedObject Subclasses that will download the file associated with that object (ie. a PDF). Then it sets
self.isAvailable = [NSNumber numberWithBool:YES];
Meanwhile, I have a UITableView that displays a list of assets. eventually it will update in real-time, but for now this is where I am having issue. I first had the view controller keep a pointer to the CoreData object that represents the folder it displays, but it appears that If the context gets updated, the pointer becomes invalid (ie. fails to fault).
Core data is not being very specific on what the problem is, or even where its happening, but it seems to crash when I set isAvailable with
*** Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0x1d5f9e50 <x-coredata://EDE66B97-B142-4E87-B445-76CAB965B676/Node/p58>''
I feel like the problem is that I shouldn't just keep a strong reference to a core data object as my model. Is there a better (less crashy) way to do this?
I have started playing with NSFetchedResultsController and using objectID's instead, but I haven't gotten anywhere yet.
- (void)populateChildren {
NSString * urlString = [NSString stringWithFormat:#"%#/%#", [CMPConstants hostURLString], self.SBUCode];
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest * request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request queue:self.downloadQueue completionHandler:^(NSURLResponse * response, NSData * data, NSError * error) {
if (data) {
NSDictionary * dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
[self processParsedObject:dict];
} else {
NSLog(#"%#", urlString);
}
}];
}
#pragma mark - Parse JSON into NSManagedObjects
- (void)processParsedObject:(id)object {
[self processParsedObject:object depth:0 parent:nil key:nil];
[[NSManagedObjectContext MR_contextForCurrentThread] MR_saveToPersistentStoreAndWait];
}
- (void)processParsedObject:(id)object depth:(int)depth parent:(Node *)parent key:(NSString*)key {
if ([object isKindOfClass:[NSDictionary class]]) {
if (depth == 0) {
// Grab content node if depth is 0;
object = [object valueForKey:#"content"];
}
// FIXME: Change this to a real primary key once we get one.
static NSString * primaryKey = #"name";
// Look for existing object
Node * testNode = [Node MR_findFirstByAttribute:primaryKey withValue:[object valueForKey:primaryKey]];
// Create new node pointer
Node * newNode;
if (testNode) {
// Update existing Node
newNode = testNode;
} else {
// Build a new Node Object
newNode = [Node MR_createEntity];
newNode.isAvailable = [NSNumber numberWithBool:NO];
}
// Get keys
NSArray * keys = #[#"name",
#"type",
#"index",
#"size",
#"videoDemensions",
#"videoId",
#"fileName",
#"fileType",
#"path"];
if ([[object valueForKey:#"type"] isEqual:[NSNull null]]) {
NSLog(#"%#", object);
}
// Loop to set value for keys.
for (NSString * key in keys) {
id value = [object valueForKey:key];
if (![[object valueForKey:key] isKindOfClass:[NSNull class]]) {
[newNode setValue:value forKey:key];
}
}
// Set calculated properties.
[newNode setSbu:[self SBUCode]];
[newNode setParent:parent];
[[NSManagedObjectContext MR_contextForCurrentThread] MR_saveToPersistentStoreAndWait];
// Sync local file.
if (!newNode.isAvailable.boolValue) {
[newNode aquireFileInQueue:self.downloadQueue];
}
// Process children
for(NSString * newKey in [object allKeys]) {
id child = [object objectForKey:newKey];
[self processParsedObject:child depth:depth+1 parent:newNode key:newKey];
}
} else if ([object isKindOfClass:[NSArray class]]) {
for(id child in object) {
[self processParsedObject:child depth:depth+1 parent:parent key:nil];
}
} else {
// Nothing here, this processes each field.
}
}
This Method is an instance method of the Node class.
- (void)aquireFileInQueue:(NSOperationQueue *)queue {
if ([self.type isEqualToString:#"VIDEO"]) {
// Videos are available, but not downloaded.
self.isAvailableValue = YES;
return;
}
if (self.path == nil || self.fileName == nil) {
NSLog(#"Path or Filename for %# was nil", self.name);
return;
}
// Build the download URL !! MAKE SURE TO ADD PERCENT ESCAPES, this will protect against spaces in the file name
// Also make sure to slash-separate the path and fileName
NSURL * downloadURL = [NSURL URLWithString:[NSString stringWithFormat:#"%#/%#",
[self.path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding],
[self.fileName stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]];
// Build the download request
NSURLRequest * downloadRequest = [NSURLRequest requestWithURL:downloadURL];
// FIXME: Authentication Code for JSON service
// Show network activity indicator
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
// Send Asynchronus Request for fileData
[NSURLConnection sendAsynchronousRequest:(NSURLRequest *)downloadRequest queue:queue completionHandler:^(NSURLResponse * response, NSData * data, NSError * error) {
// Hide network activity indicatior
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
// Cast URL Response to HTTPURLResponse
NSHTTPURLResponse * httpResponse = (NSHTTPURLResponse *)response;
// If statusCode is 200 (successful) and data is not nil, save data
if (httpResponse.statusCode == 200 && data) {
[data writeToURL:[self fileURL] atomically:NO];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self setIsAvailable:[NSNumber numberWithBool:YES]];
}];
}
}];
}
- (void)prepareForDeletion {
// Remove file from Filesystem
[[NSFileManager defaultManager] removeItemAtURL:[self fileURL] error:nil];
}
- (NSURL *)fileURL {
// Return local file URL
return [NSURL fileURLWithPath:[NSString stringWithFormat:#"%#/%#", [Node applicationDocumentsDirectory], self.fileName]];
}
I am not familiar with MagicalRecords
A 'Could not fullfil fault' error occur when a context is holding an un-faulted object (an object stub), but the actual object in the database does not exist (deleted or was never saved).
My first advice:
If you work in a multithreaded environment, try to hold faulted objects.
use -existingObjectWithId:error: and fetch requests with:
[fetchRequest setReturnsObjectsAsFaults:NO];
[fetchRequest setIncludesPropertyValues:YES];
[fetchRequest setRelationshipKeyPathsForPrefetching:/*relationships you can afford to prefetch*/];
My second advice (to debug your issue):
Print you deletedObjects set before each save you make to the store to see which context caused the fault.
My third advice:
merge changes to the main context (my guess is that MagicalRecords does that for you).
note 1: deletes may be implied (you don't explicitly use deleteObject: by setting a relationship in cascade/deny mode for example)
note 2: you can not avoid this exception in a multithreaded environment (AFAIK), unless you pass all your saves through the main context (using parentContext) or by always using prefetched objects (not using relationships directly).