I am using AFNetworking for downloading files having size between 1 to 4 gbs,.
Currently while downloading such a huge files I pause the current download when app enters in background state and resume when it gets active.
But what happens wrong in my case is that, first time while downloading when I minimize the app I pause it and when I again maximize app after 20 to 30 mins I resume it and download continues from where it was left paused last time. But it works only first time, second time when I again minimize the app with same download it gets paused and when I again maximizes it , it stuck at the same point showing some wrong values for progress and current transfer speed and it never moves forward or never continues current download.
Strange behaviour??
I have tried both old and new (2.0) versions but no luck.
Can you guess what is happening wrong in my case?
Or
Please suggest me some good alternatives to using AFNetworking.
UPDATE
Method called to download file
-(void) downloadTracksFromProgramArray:(NSArray*) programs
{
if (programs.count == 0) {
return;
}
queue = [[NSOperationQueue alloc] init];
[queue setMaxConcurrentOperationCount:1];
queueSize = 0;
urlString = [programs objectAtIndex:0];
NSString *filename = [urlString lastPathComponent];
// 11-09-12
// remove query string from aws
NSString *string1 = [NSHomeDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:#"Documents/%#",[filename lastPathComponent]]] ;
// remove query string from aws
NSArray *jaysarray = [string1 componentsSeparatedByString:#"?"];
NSString *downloadPath1 = [NSString stringWithFormat:#"%#",[jaysarray objectAtIndex:0]];
extract_file_path_after_download = downloadPath1;
NSLog(#"%#",[jaysarray objectAtIndex:0]);
// NSLog(#"%#",[jaysarray objectAtIndex:1]);
current_downloading_file_path = [downloadPath1 copy];
NSLog(#"download url %#",[NSURL URLWithString:urlString]);
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]];
operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:downloadPath1 append:NO];
//handle successful completion of each track download
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"Successfully downloaded file to %#", downloadPath1);
//if ([[queue operations] count] == 0) {
NSNotification *success = [NSNotification notificationWithName:#"AudioDone" object:[NSNumber numberWithBool: YES]];
[[NSNotificationCenter defaultCenter] postNotification:success];
queueSize = 0;
//} else {
//send total track info
//get total queue size by the first success and add 1 back
if (queueSize ==0) {
queueSize = [[queue operations] count] +1.0;
}
float progress = (float)(queueSize-[[queue operations] count])/queueSize;
NSNumber * totProgress = [NSNumber numberWithFloat:progress];
NSLog(#"Total Progress: %#", totProgress);
current_downloading_file_path = #"";
//Commented by rakesh biradar - becoz #"TotalProgress" notification method does not do anything(memory).
//NSNotification * totalProgressNotification = [NSNotification notificationWithName:#"TotalProgress"
// object:totProgress];
//[[NSNotificationCenter defaultCenter] postNotification:totalProgressNotification];
//}
NSLog(#"QueueCount: %d", [[queue operations] count]); //[[self sharedQueue] operationCount]);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//deletes the partial downloaded file from document folder
if(([current_downloading_file_path length] > 0) && [[NSFileManager defaultManager] fileExistsAtPath:current_downloading_file_path])
[[NSFileManager defaultManager] removeItemAtPath:current_downloading_file_path error:nil];
current_downloading_file_path = #"";
NSLog(#"Error: %#", error);
}];
//Send progress notification
[operation setDownloadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
//NSLog(#"Sent %lld of %lld bytes, %#", totalBytesWritten, totalBytesExpectedToWrite, path);
float percentDone = ((float)((int)totalBytesWritten) / (float)((int)totalBytesExpectedToWrite));
//NSLog(#"Percent: %f", percentDone);
NSDictionary *userInfo = [NSDictionary dictionaryWithObjects: [NSArray arrayWithObjects:filename, [NSNumber numberWithFloat: percentDone],[NSNumber numberWithLongLong:totalBytesWritten],[NSNumber numberWithLongLong:totalBytesExpectedToWrite],[NSNumber numberWithUnsignedInteger:bytesWritten],nil]
forKeys:[NSArray arrayWithObjects:#"message", #"percent",#"totalBytesWritten",#"totalBytesExpectedToWrite",#"bytesWritten", nil]];
NSNotification * progress = [NSNotification notificationWithName:#"DownloadingAudio" object:nil userInfo:userInfo];
[[NSNotificationCenter defaultCenter] postNotification:progress];
}];
[queue addOperation:operation];
//[self enqueueHTTPRequestOperation:operation];
//NSLog(#"Operation Queue: %#", [self sharedQueue]);
}
Method when app goes in background
- (void)applicationWillResignActive:(UIApplication *)application {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
if (operation)
{
NSLog(#"%#",operation);
//[self saveCustomObject:operation];
[operation pause];
}
}
Method called when app becomes active
- (void)applicationDidBecomeActive:(UIApplication *)application {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
// Handle the user leaving the app while the Facebook login dialog is being shown
// For example: when the user presses the iOS "home" button while the login dialog is active
if (operation)
{
//operation = [self loadCustomObjectWithKey:#"myEncodedObjectKey"];
NSLog(#"%#",operation);
[operation resume];
}
[FBAppCall handleDidBecomeActive];
}
Related
I have a task of uploading multiple images to the server one by one. So I am using the batch operation process for this. Every time I start the upload procedure, some operations specially the first one completes as soon as it starts and the image does not get uploaded, and then the batch upoad process continues fine with a rare glitch of missing the other images.
The code I am using is as follows:-
-(void)callWSToUploadRxs{
NSLog(#"the total assets maintained are %lu", (unsigned long)_arr_assetsMaintained.count);
NSMutableArray *mutableOperations = [NSMutableArray array];
int imageUploadCount = (int)[self extractFullSizeImagesToUpload].count;
// second for loop is to initialize the operations and then queue them.
for (int i = 0; i<imageUploadCount; i++) {
NSData *imageData = UIImageJPEGRepresentation([_arr_originalImagesToSend objectAtIndex:i],1.0);
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
[request setHTTPMethod:#"POST"];
NSLog(#"the url constructed is %#", [NSString stringWithFormat:#"%#/%#/%#/%#",uploadRxUrl,#"4004DD85-1421-4992-A811-8E2F3B2E49F7",#"5293",[_arr_imageNames objectAtIndex:i]]);
[request setURL:[NSURL URLWithString:[NSString stringWithFormat:#"%#/%#/%#/%#.jpg",uploadRxUrl,#"4004DD85-1421-4992-A811-8E2F3B2E49F7",#"5293",[_arr_imageNames objectAtIndex:i]]]];
[request setValue:#"binary/octet-stream" forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:imageData];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[mutableOperations addObject:operation];
}
currentUploadIndex++;
NSArray *operations = [AFURLConnectionOperation batchOfRequestOperations:mutableOperations progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) {
NSLog(#"%lu of %lu complete", numberOfFinishedOperations, totalNumberOfOperations);
NSIndexPath * indexOfImageTobeDeleted = [_selectedItemsIndexPaths objectAtIndex:0];//numberOfFinishedOperations-1
[_arr_assetsMaintained removeObjectAtIndex:indexOfImageTobeDeleted.item];
[_arr_images removeObjectAtIndex:indexOfImageTobeDeleted.item];
[_arr_fullSizeImages removeObjectAtIndex:indexOfImageTobeDeleted.item];
[_arr_imageNames removeObjectAtIndex:indexOfImageTobeDeleted.item];
if ( [_arr_selectedCells containsObject:[NSString stringWithFormat:#"%ld",(long)indexOfImageTobeDeleted.item]] )
{
[_arr_selectedCells removeObject:[NSString stringWithFormat:#"%ld",(long)indexOfImageTobeDeleted.item]];
//[cell.img_selctedRxs setHidden:TRUE];
}
countBeforeClearingAssets = countBeforeClearingAssets - 1;
//Reload the items of UICollectionView performBatchUpdates Block
[_albumImagesCollection performBatchUpdates:^{
[_albumImagesCollection deleteItemsAtIndexPaths:#[indexOfImageTobeDeleted]];
} completion:nil];
_selectedItemsIndexPaths = [_albumImagesCollection indexPathsForSelectedItems];
// [_selectedItemsIndexPaths removeObjectAtIndex:0];
NSLog(#"the count of selected items after updation is %lu", (unsigned long)_selectedItemsIndexPaths.count);
} completionBlock:^(NSArray *operations) {
NSLog(#"All operations in batch complete");
[self callWSToAddNoteForRxs];
[_arr_originalImagesToSend removeAllObjects];
[_arr_selectedCells removeAllObjects];
currentUploadIndex = 0;
NSLog(#"the array of image names is %#",_arr_imageNames);
}];
[[NSOperationQueue mainQueue] addOperations:operations waitUntilFinished:NO];
// third is to maintain the progress block for each image to be uploaded one after the other.
for (AFHTTPRequestOperation *operation in mutableOperations){
[operation setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
[_progressOverLayView setAlpha:0.7f];
[_progressView setHidden:FALSE];
[_progressView setProgress: totalBytesWritten*1.0f / totalBytesExpectedToWrite animated: YES];
[_lbl_progressUpdate setHidden:FALSE];
_lbl_progressUpdate.text = [NSString stringWithFormat:#"Image %d of %lu uploading", currentUploadIndex, mutableOperations.count];
NSLog(#"Sent %lld of %lld bytes and progress is %f", totalBytesWritten, totalBytesExpectedToWrite, totalBytesWritten*1.0f / totalBytesExpectedToWrite);
if(totalBytesWritten >= totalBytesExpectedToWrite)
{
//progressView.hidden = YES;
[self setComplete];
}
}];
}
}
In this code, the first operation is getting executed as soon I start uploading the images i.e only 3 out of 4 are getting uploaded. One Image is always left out. Also. if I do have only Image in the grid, that uploads successfully.
Thanks.
A few thoughts...
1) the progress update block. This doesn't tell you which operations completed; only the count of them, so I am suspicious that they might not be completing in the order the code thinks they are all the time....
2) the completion block takes an array of operations.... I wonder if this gets called once with all of the operations, or multiple times with arrays of the operations that completed? You could look at the array length to see. If it does get called for subsets of operations, this would be the place to remove the assets that have already been uploaded and do cleanup work, since you know which operations finished.
3) I would set the upload progress block before adding the operations to the queue; just for safety.
4) I couldn't find documentation for the batchOperation method, and wondered if it has been removed from a more recent version of AFNetworking - and if so - maybe it's buggy or not good API? I'd be tempted to create my own operations in a loop for clarity; and then do a little state management to check on the status of the batch and handle that appropriately.
5) You say one image is always left out.... is it stable - always the first or last? Does it behave the same way on the sim vs on a cell network or simulated slow / unreliable connection??
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:.
My iPhone app has to load 3 data sets when it is first opened. I have 3 view controllers, one for each data set. I notice that when I am on my real iPhone and first open the app and touch a view controller there may be a very long pause, I am assuming while the data is being loaded, but I am not sure.
Here is the relevant code in my AppDelegate:
#import "AppDelegate.h"
#import "MasterViewController.h"
#implementation AppDelegate
- (void)application:(UIApplication *)application performFetchWithCompletionHandler: (void (^)(UIBackgroundFetchResult))completionHandler
{
// Background Fetch for Employees
EmployeeDatabase *tmpEmployeeDatabase = [[EmployeeDatabase alloc] init];
[tmpEmployeeDatabase updateEmployeeData];
completionHandler(UIBackgroundFetchResultNewData);
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Set Background Fetch Interval
[application setMinimumBackgroundFetchInterval: UIApplicationBackgroundFetchIntervalMinimum];
// Take Database Name and get DatabasePath for later use
self.databaseName = #"employees.db";
self.databaseNameLocations = #"locations.db";
self.databaseNameContacts = #"contacts.db";
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDir = [documentPaths objectAtIndex:0];
self.databasePath =[documentDir stringByAppendingPathComponent:self.databaseName];
self.databasePathLocations =[documentDir stringByAppendingPathComponent:self.databaseNameLocations];
self.databasePathContacts =[documentDir stringByAppendingPathComponent:self.databaseNameContacts];
// See if we need to initialize the employee db
EmployeeDatabase *tmpEmployeeDatabase = [[EmployeeDatabase alloc] init];
if (![tmpEmployeeDatabase checkIfDatabaseExists]) {
[tmpEmployeeDatabase updateEmployeeData];
}
// See if we need to initialize the contact db
ContactsDatabase *tmpContactsDatabase = [[ContactsDatabase alloc] init];
if (![tmpContactsDatabase checkIfDatabaseExists]) {
[tmpContactsDatabase updateContactsData];
}
// See if we need to initialize the Locations db
LocationDatabase *tmpLocationDatabase = [[LocationDatabase alloc] init];
if (![tmpLocationDatabase checkIfDatabaseExists]) {
[tmpLocationDatabase updateLocationData];
}
return YES;
}
#pragma mark - Application's Documents directory
// Returns the URL to the application's Documents directory.
- (NSURL *)applicationDocumentsDirectory {
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}
#end
And here is where I call one of the web services and load the data:
#define kBgQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
- (void)callWebService {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.requestSerializer = [AFJSONRequestSerializer serializer];
[manager.requestSerializer setAuthorizationHeaderFieldWithUsername:#"xxxxxx" password:#"xxxxxxxxxxxx"];
manager.responseSerializer = [AFJSONResponseSerializer serializer];
AFHTTPRequestOperation *operation = [manager GET: #"https://xxxx/mobile/mobilede.nsf/restPeople.xsp/People"
parameters: [self jsonDict]
success: ^(AFHTTPRequestOperation *operation, id responseObject)
{
NSMutableArray *employees = (NSMutableArray *)responseObject;
FMDatabase *db = [FMDatabase databaseWithPath:self.employeeDatabasePath];
[db open];
for (NSDictionary *dict in employees) {
BOOL success = [db
executeUpdate:
#"INSERT INTO employees "
"(id,fstNme,midNme,lstNme,fulNme,locNbr,supID,"
"mrkSeg,geoLoc,lvl,vp,ema,ofcPhn,mobPhn) VALUES "
"(?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
[dict objectForKey:#"id"], [dict objectForKey:#"fstNme"],
[dict objectForKey:#"midNme"], [dict objectForKey:#"lstNme"],
[dict objectForKey:#"fulNme"], [dict objectForKey:#"locNbr"],
[dict objectForKey:#"supId"], [dict objectForKey:#"mrkSeg"],
[dict objectForKey:#"geoLoc"], [dict objectForKey:#"lvl"],
[dict objectForKey:#"vp"], [dict objectForKey:#"ema"],
[dict objectForKey:#"ofcPhn"],[dict objectForKey:#"mobPhn"], nil];
if (success) {
} // Only to remove success error
}
}
failure:
^(AFHTTPRequestOperation * operation, NSError * error) {
NSLog(#"Error: %#", error);
}
];
[operation start];
}
[EDIT]
I forgot the part of the code where I called the
[EDIT]
One possible mistake is that I am using the same background queue for each of the three processes? See the #define kBgQueue at the top of this bit of code.
What is the best practice to handle this? Should I NOT put this on a background queue and alert the user to wait?
[Thank you. I changed this and recompiled. The first time the app starts the interface will freeze at some point, and you cannot do anything for 12 seconds or so, and then it comes out of it. Subsequently there are no pauses. As an example, when I first open the app I can get to my first view, Employees by Name, and if I touch it to list them it might go into the the list but be blank. So I will touch the navigation backward and then it stops for 12 seconds (or so) and then it will return to the main menu, and when I go back in there are the employees. And it never stops from then on out. I cannot figure out why if this is on the background que that it is holding up the interface. What could I run to try to determine when this is happening?]
If your app could not work without data put data processing in background and show some activity indicator to user. If your app could work without data let user do whatever he wants to do and after data is being loaded just reload UI with new data.
I am trying to measure time taken per GET request when downloading a file using AFNetworking. I am downloading a file repeatedly in a loop.
The problem I am having is that the way I am measuring total time, it gives a much larger total time than it actually is. For example, for 50 downloads it gives 72 sec but in reality it only took around 5 sec. I also suspect 5 sec is too low for 50 downloads(the download size is 581 kb per file).
How do I effectively measure time in this case? I need time from the moment request is fired till response in received.
My method to download file:
- (void) HTTPGetRequest
{
startTime = CACurrentMediaTime(); // Start measuring time
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:http://myServer];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET"
path:#"/download/Text581KB.txt"
parameters:nil];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[httpClient registerHTTPOperationClass:[AFHTTPRequestOperation class]];
// Save downloaded file
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *path = [[paths objectAtIndex:0] stringByAppendingPathComponent:[NSString stringWithFormat:#"Text581KB.txt"]];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:path append:NO];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
double elapsedTime = (CACurrentMediaTime() - startTime); // Measuring time
totalTime += elapsedTime; // Measuring total time HERE!
[results setString:[NSString stringWithFormat: #"Current Transaction Time: %f sec\nTotal Time: %f sec", elapsedTime, totalTime]];
[_resultLabel performSelectorOnMainThread:#selector(setText:) withObject:results waitUntilDone:NO];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
[operation setDownloadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) { ((int)totalBytesExpectedToWrite));
totalDownloadSize += totalBytesExpectedToWrite;
[_DataTransferredLabel setText:[NSString stringWithFormat:#"Total Download Size: %#", [self getFileSize:totalDownloadSize/1024]]];
}];
[operation setCacheResponseBlock:^NSCachedURLResponse *(NSURLConnection *connection, NSCachedURLResponse *cachedResponse) {
return nil;
}];
[operationQueue addOperation:operation];
}
I am creating a NSOperationQueue in my viewDidLoad:
operationQueue = [NSOperationQueue new];
[operationQueue setMaxConcurrentOperationCount:1]; // using this as I was suspecting downloads were happening in parallel & thus 50 downloads finishing in a few secs
I am invoking the HTTPGetRequest method as follows:
- (IBAction)startDownload:(UIButton *)sender {
totalCount = [[_countText text] longLongValue]; // get # of times to download
long currentCount = 1;
completedCount = 0;
totalTime = 0;
totalDownloadSize = 0;
while (currentCount <= totalCount)
{
[self HTTPGetRequest];
[results setString:#""];
currentCount++;
}
Use AFHTTPRequestOperationLogger.
In terms of calculating cumulative time (not elapsed time), I have just created a subclass of AFHTTPRequestOperation that captures the start time. Otherwise, you won't know precisely when it started:
#interface TimedAFHTTPRequestOperation : AFHTTPRequestOperation
#property (nonatomic) CFAbsoluteTime startTime;
#end
#implementation TimedAFHTTPRequestOperation
- (void)start
{
self.startTime = CFAbsoluteTimeGetCurrent();
[super start];
}
#end
(Note I'm using CFAbsoluteTimeGetCurrent versus CACurrentMediaTime; use whatever you want, but just be consistent.)
Then in the code that's doing the downloads, you can use this TimedAFHTTPRequestOperation instead of AFHTTPRequestOperation:
TimedAFHTTPRequestOperation *operation = [[TimedAFHTTPRequestOperation alloc] initWithRequest:request];
That code's completion block can then use the startTime property of TimedAFHTTPRequestOperation to calculate the time elapsed for the given operation and add it to the total time:
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
TimedAFHTTPRequestOperation *timedOperation = (id)operation;
CFTimeInterval elapsedTime = CFAbsoluteTimeGetCurrent() - timedOperation.startTime;
self.totalTime += elapsedTime; // Measuring total time HERE!
NSLog(#"finished in %.1f", elapsedTime);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
That's how you calculate the elapsedTime and append them together to calculate the totalTime.
In terms of how to know when the operations are done, I would
modify HTTPGetRequest to return a NSOperation;
have startDownload create a completion operation and then add all of these operations as dependencies:
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"finished all in cumulative time: %.1f", self.totalTime);
}];
for (NSInteger i = 0; i < totalCount; i++)
{
NSOperation *operation = [self HTTPGetRequest];
[completionOperation addDependency:operation];
}
[self.operationQueue addOperation:completionOperation];
That achieves several goals, namely creating a completion operation, calculating the total time (as opposed to the total time elapsed).
By the way, I'd also suggest pulling the creation of AFHTTPClient out of your HTTPGetRequest. You should probably only create one per app. This is especially important in case you ever started using enqueueHTTPRequestOperation instead of creating your own operation queue. I also see no need for your call to registerHTTPOperationClass.
You are incrementing the totalElapsed by elapsedTime, but elapsedTime is calculated from startTime, which itself represents the time that the jobs were first queued, not when the download actually started. Remember that HTTPGetRequest returns almost immediately (having set elapsedTime). Thus if you're queueing five downloads, I wouldn't be surprised that HTTPGetRequest runs five times (and sets and resets startTime five times) before the first request even is initiated.
The question is further complicated by the question of whether you're doing concurrent downloads, and if so, what you then mean by "total elapsed". Let's say you have two concurrent downloads, one that takes 5 seconds, another takes 7 seconds. Do you want the answer to be 7 (because they both finished in 7 seconds)? Or do you want the answer to be 12 (because they both finished in a cumulative 12 seconds)?
I'm presuming that you're looking for, in this scenario, 7 seconds, then you should set the startTime once before you initiate all of your requests, and then only calculate when all of the downloads are done. You could, for example, rather than doing any calculations in HTTPGetRequest at all, just add your own operation that is dependent upon all the other operations you added, which calculates the total elapsed. at the very end. Or, if you want the the total elapsed to just reflect the total elapsed while you're in the process of downloading, then just set totalElapsed rather than incrementing it.
Another option is to inject the "fire" date in the operation's userInfo by observing the AFNetworkingOperationDidStartNotification notification.
//AFHTTPRequestOperation *operation = [...]
id __block observer = [[NSNotificationCenter defaultCenter] addObserverForName:AFNetworkingOperationDidStartNotification
object:operation
queue:nil
usingBlock:^(NSNotification *note) {
operation.userInfo = #{#"fireDate": [NSDate date]};
[[NSNotificationCenter defaultCenter] removeObserver:observer];
}];
I'm using an ASINetworkQueue to download around 50 files in an iPad app. I'm looking for a way of allowing the user to pause and resume the queue.
The ASIHTTP docs refer to
[request setAllowResumeForFileDownloads:YES];
but this operates at an individual request level, not at the queue level. As ASINetworkQueue is a subclass of NSOperationQueue I've also tried
[queue setSuspended:YES];
and while this will pause a queue, it does not affect the downloads in progress, it just waits until they've finished and then pauses the queue, which in my case means many seconds between the user pressing the button and the queue actually pausing, which is not the UI experience I want.
Can anyone suggest another way of solving this problem?
My solution..
- (void) pause
{
if(self.queue && self.queue.requestsCount>0)
{
NSLog(#"%#", self.queue.operations);
for (ASIHTTPRequest * req in self.queue.operations) {
req.userInfo = [NSDictionary dictionaryWithObject:#"1" forKey:#"ignore"];
}
[self.queue.operations makeObjectsPerformSelector:#selector(cancel)];
[self.queue setSuspended:YES];
for (ASIHTTPRequest * req in self.queue.operations) {
ASIHTTPRequest * newReq = [[ASIHTTPRequest alloc] initWithURL:req.url];
[newReq setDownloadDestinationPath:req.downloadDestinationPath];
[newReq setTemporaryFileDownloadPath:req.temporaryFileDownloadPath];
// [newReq setAllowResumeForFileDownloads:YES];
[newReq setUserInfo:req.userInfo];
[self.queue addOperation:newReq];
}
}
}
- (void) resume
{
if(self.queue && self.queue.requestsCount>0)
{
[self _setupQueue];
[self.queue go];
}
}
- (void) _setupQueue
{
[self.queue setShouldCancelAllRequestsOnFailure:NO];
[self.queue setRequestDidStartSelector:#selector(downloadDidStart:)];
[self.queue setRequestDidFinishSelector:#selector(downloadDidComplete:)];
[self.queue setRequestDidFailSelector:#selector(downloadDidFailed:)];
[self.queue setQueueDidFinishSelector:#selector(queueDidFinished:)];
[self.queue setDownloadProgressDelegate:self.downloadProgress];
[self.queue setDelegate:self];
self.queue.maxConcurrentOperationCount = 3;
// self.queue.showAccurateProgress = YES;
}
First, pause function cancel all running operations, and recreate new requests push them into queue.
Then resume function unsuspends the queue.
Be noticed, request should not set setAllowResumeForFileDownloads:YES, otherwise the totalBytesToDownload will calculate wrong..If allowResumeForFileDownloads=NO, its value will be same as the count of requests in queue.
Here is my request fail handler, I add retry when file download fail. But I don't whant when I cancel a request, the retry mechanism will be invoked, so I set userInfo(ignore:true) to request object to prevent it happens.
- (void) downloadDidFailed:(ASIHTTPRequest *)req
{
NSLog(#"request failed");
NSLog(#"%d", self.queue.requestsCount);
if(![self.queue showAccurateProgress])
{
[self.queue setTotalBytesToDownload:[self.queue totalBytesToDownload]-1];
NSLog(#"totalBytesToDownload=%lld", self.queue.totalBytesToDownload);
}
NSDictionary * userInfo = req.userInfo;
if([[userInfo valueForKey:#"ignore"] boolValue]) return; // ignore retry
int retryTimes = [[req.userInfo objectForKey:#"retryTimes"] intValue];
if(retryTimes<kMU_MaxRetry)
{
++ retryTimes;
NSLog(#"retry %d", retryTimes);
ASIHTTPRequest * newReq = [ASIHTTPRequest requestWithURL:req.url];
[newReq setDownloadDestinationPath:req.downloadDestinationPath];
[newReq setTemporaryFileDownloadPath:req.temporaryFileDownloadPath];
newReq.userInfo = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:#"%d", retryTimes] forKey:#"retryTimes"];
[self.queue addOperation:newReq];
NSLog(#"%d", self.queue.requestsCount);
}else{ // reach max retry, fail it
[self.failures addObject:req];
}
}
I am not sure if there is a better solution, hope it can help you.