Can anybody answer to me, how come below code eats up whole core of a C2D 2.6GHz CPU?
It just downloads files in 10MB chunks, there can be like 600 of them, but the NSOperationQueue has a limit of 6 concurrent tasks.
How come the same app on Windows (written in C# eats only 2%, not 80%!), it is just a simple HTTP request!
for (DownloadFile *downloadFile in [download filesInTheDownload])
{
for (DownloadChunk *downloadChunk in [downloadFile chunksInTheFile])
{
NSString *downloadPath = [[NSString stringWithFormat:#"%#/%#", [download downloadFolder], [download escapedTitle]] stringByExpandingTildeInPath];
NSString *chunkPath = [downloadPath stringByAppendingFormat:#"/%#.%i", [downloadFile fileName], [downloadChunk chunkId]];
NSError *attributesError = nil;
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:chunkPath error:&attributesError];
NSNumber *fileSizeNumber = [fileAttributes objectForKey:NSFileSize];
uint64_t fileSize = [fileSizeNumber longLongValue];
NSLog(#"Chunk file size: %lli", fileSize);
uint64_t expectedSize = ([downloadChunk endingByte] - [downloadChunk startingByte]) + 1;
NSLog(#"Chunk expected size: %lli", expectedSize);
uint64_t newStartingByte = [downloadChunk startingByte] + fileSize;
if (fileSize == expectedSize)
{
NSLog(#"Chunk complete: %#.%i", [downloadFile fileName], [downloadChunk chunkId]);
}
else
{
NSURL *fileURL = [[NSURL alloc] initWithString:[downloadFile filePath]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:fileURL];
NSLog(#"Normal range: %lli-%lli", [downloadChunk startingByte], [downloadChunk endingByte]);
NSString *range = [NSString stringWithFormat:#"bytes=%lli-%lli", newStartingByte, [downloadChunk endingByte]];
[request setValue:range forHTTPHeaderField:#"Range"];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:chunkPath append:YES];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"%#", [NSString stringWithFormat:#"Chunk complete: %#.%i", [downloadFile fileName], [downloadChunk chunkId]]);
if (download.downloadedBytes == download.size)
[[NSNotificationCenter defaultCenter] postNotificationName:#"downloadFinished" object:download];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
[operation setDownloadProgressBlock:^(NSInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
download.downloadedBytes += bytesRead;
}];
[queue addOperation:operation];
}
}
}
Here are some time profiler screenshots, seems everything is because RunLoops if I read it correctly.
Seems like downgrading most of the properties to nonatomic seems to lower the usage from 80% to 20-30%. Now only downloadedBytes are atomic. What can I do also to make improvements?
In assuming the last progress block completes the download, you're sort of shoving a round peg down a square hole. I'm not sure if that's causing your high CPU usage or not, but I'd certainly fix that. AFNetworking has a function just for this, [AFHTTPRequestOperation setCompletionBlockWithSuccess:failure:], documented here. The correct block will be called based on the HTTP status code returned.
In other words, instead of this:
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:chunkPath append:YES];
[operation setDownloadProgressBlock:^(NSInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
download.downloadedBytes += bytesRead;
if (download.downloadedBytes == download.size)
[[NSNotificationCenter defaultCenter] postNotificationName:#"downloadFinished" object:download];
}];
[queue addOperation:operation];
chunkId++;
Use this:
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:chunkPath append:YES];
[output setCompletionBlockWithSuccess: ^(AFHTTPRequestOperation *operation, id responseObject) {
[[NSNotificationCenter defaultCenter] postNotificationName:#"downloadFinished" object:download];
}
failure: ^(AFHTTPRequestOperation *operation, NSError *error) {
// you should probably do something here
}];
After that, I doubt you'd even need downloadedBytes. That removes your last atomic property, which should improve things.
Related
I have a old project which is not working in iOS 9. I've read official documentation on AFNetworking and completed most of the migration.
NetworkManager:
_requestManager = [[AFHTTPSessionManager alloc]initWithBaseURL:[NSURL URLWithString:baseURL ]];
//here we can set the request header as the access token once we have logged in.
AFHTTPRequestSerializer *requestSerializer = [AFHTTPRequestSerializer serializer];
AFHTTPResponseSerializer *responseSerializer = [AFHTTPResponseSerializer serializer];
[_requestManager setRequestSerializer:requestSerializer];
[_requestManager setResponseSerializer:responseSerializer];
Earlier Version:
// 2. Create an `NSMutableURLRequest`.
NSMutableURLRequest *request =
[[NetworkManager sharedInstance].requestManager.requestSerializer multipartFormRequestWithMethod:#"POST" URLString:fullPath
parameters:nil
constructingBodyWithBlock: ^(id <AFMultipartFormData> formData) {
NSString *fileName = [NSString stringWithFormat:#"%#.caf", theAudioItem.media_item_name];
[formData appendPartWithFileData:audioData name:theAudioItem.media_item_name fileName:fileName mimeType:#"audio/caf"];
}];
// 3. Create and use `AFHTTPRequestOperationManager` to create an `AFHTTPRequestOperation` from the `NSMutableURLRequest` that we just created.
AFHTTPRequestOperation *operation =
[[NetworkManager sharedInstance].requestManager HTTPRequestOperationWithRequest:request
success: ^(AFHTTPRequestOperation *operation, id responseObject) {
result(YES, #"");
} failure: ^(AFHTTPRequestOperation *operation, NSError *error) {
if (operation.responseData) {
NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:operation.responseData options:NSJSONReadingMutableContainers error:nil];
result(NO, [responseDict valueForKey:#"Message"]);
[self deleteTmpFilesFromParts:formParts];
} else {
result(NO, [NSString stringWithFormat:#"Failed to upload media to %#!", gallery.gallery_name]);
[self deleteTmpFilesFromParts:formParts];
}
result(NO, errorMessage);
}];
// 4. Set the progress block of the operation.
[operation setUploadProgressBlock: ^(NSUInteger __unused bytesWritten,
long long totalBytesWritten,
long long totalBytesExpectedToWrite) {
DLog(#"progress is %i %lld %lld", bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
progress((float)totalBytesWritten / (float)totalBytesExpectedToWrite);
}];
// 5. Begin!
[operation start];
Converted Response: ( updated with comments what gives errors )
//No visible #interface for 'AFHTTPRequestSerializer<AFURLRequestSerialization>' declares the selector 'multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:'
NSMutableURLRequest *request =
[[NetworkManager sharedInstance].requestManager.requestSerializer multipartFormRequestWithMethod:#"POST" URLString:fullPath parameters:nil
constructingBodyWithBlock: ^(id <AFMultipartFormData> formData) {
NSString *fileName = [NSString stringWithFormat:#"%#.caf", theAudioItem.media_item_name];
[formData appendPartWithFileData:audioData name:theAudioItem.media_item_name fileName:fileName mimeType:#"audio/caf"];
}];
// 3. Create and use `AFHTTPRequestOperationManager` to create an `AFHTTPRequestOperation` from the `NSMutableURLRequest` that we just created.
//No visible #interface for 'AFHTTPSessionManager' declares the selector 'HTTPRequestOperationWithRequest:success:failure:'
NSURLSessionTask *operation =
[[NetworkManager sharedInstance].requestManager HTTPRequestOperationWithRequest:request
success: ^(NSURLSessionTask *operation, id responseObject) {
result(YES, #"");
} failure: ^(NSURLSessionTask *operation, NSError *error) {
//Error for operation doesn't have responseData
if (operation.responseData) {
NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:operation.responseData options:NSJSONReadingMutableContainers error:nil];
result(NO, [responseDict valueForKey:#"Message"]);
[self deleteTmpFilesFromParts:formParts];
} else {
result(NO, [NSString stringWithFormat:#"Failed to upload media to %#!", gallery.gallery_name]);
[self deleteTmpFilesFromParts:formParts];
}
// get the response
result(NO, errorMessage);
}];
// 4. Set the progress block of the operation.
//No visible #interface for 'NSURLSessionTask' declares the selector 'setUploadProgressBlock:'
[operation setUploadProgressBlock: ^(NSUInteger __unused bytesWritten,
long long totalBytesWritten,
long long totalBytesExpectedToWrite) {
DLog(#"progress is %i %lld %lld", bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
progress((float)totalBytesWritten / (float)totalBytesExpectedToWrite);
}];
// 5. Begin!
//No visible #interface for 'NSURLSessionTask' declares the selector 'start'
[operation start];
The equivalent code for AFHTTPSessionManager is simply:
NSURLSessionTask *task = [[[NetworkManager sharedInstance] requestManager] POST:fullPath parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
NSString *fileName = [NSString stringWithFormat:#"%#.caf", theAudioItem.media_item_name];
[formData appendPartWithFileData:audioData name:theAudioItem.media_item_name fileName:fileName mimeType:#"audio/caf"];
} progress:^(NSProgress * _Nonnull uploadProgress) {
NSLog(#"%.3f", uploadProgress.fractionCompleted);
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
// success
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
// failure
// if you need to process the `NSData` associated with this error (if any), you'd do:
NSData *data = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey];
if (data) { ... }
}];
This features two major changes from your v1.x code: First, with 2.x they introduced GET/POST/etc. methods that saved you from manually starting the operation (or adding it to your own queue). Second, with 3.x, they retired NSOperation framework of AFHTTPRequestOperationManager, and now use AFHTTPSessionManager class, where GET/POST/etc. don't return an NSOperation subclass, but rather a NSURLSessionTask reference.
I wrote a function to fetch an NSDictionary from a URL.
[Data loadFromAPI:#"http://example.com/api" withSuccess:^(id data) {
NSMutableDictionary *result = (NSDictionary *)data;
CLS_LOG(#"result: %#", result);
} failure:^(NSError *error) {
CLS_LOG(#"Error: %#", error);
}];
The function it calls is the one below:
typedef void (^Success)(id data);
typedef void (^Failure)(NSError *error);
+ (void)loadFromAPI:(NSString *)apiURL withSuccess:(Success)success failure:(Failure)failure {
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:apiURL]];
NSMutableURLRequest *mutableRequest = [request mutableCopy];
request = [mutableRequest copy];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.responseSerializer = [JSONResponseSerializerWithData serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSArray *jsonArray = (NSArray *)responseObject ;
NSDictionary *result = [NSDictionary dictionaryWithObjectsAndKeys:
[self now], #"time",
jsonArray, #"response",
nil];
CLS_LOG(#"Result is: %#", result);
success(result);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
failure?failure(error):nil;
}];
[operationQueue setMaxConcurrentOperationCount:1];
[operationQueue addOperations:#[operation] waitUntilFinished:NO];
}
Inside the function CLS_LOG(#"Result is: %#", result); returns the data returned. However, where I call the function CLS_LOG(#"result: %#", result); returns null. What am I doing wrong?
As it turns out [self now] returned null, so the NSDictionary wasn't being set properly.
The problem is
NSMutableDictionary *result = (NSDictionary *)data;
you cant directly do that, i wonder why you dont have a warning for that..
NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithDictionary:(NSDictionary *)data];
will probably solve the problem...
I want to upload files to my server from my app. The code below is working great when app is active. If I press home button or open another app uploading stops.
I activated background fetch but still not working.
Afnetworking has background support but I cant figure out how I implement this feature to my code.
NSString *str=[[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:#"Archive.zip"];
NSDictionary *parameters = #{#"foo": #"bar"};
NSURL *filePath = [NSURL fileURLWithPath:str];
AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
NSData *imageData=[NSData dataWithContentsOfURL:filePath];
NSMutableURLRequest *request =
[serializer multipartFormRequestWithMethod:#"POST" URLString:#"http://url"
parameters:parameters
constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
[formData appendPartWithFileData:imageData
name:#"image"
fileName:#"Archive.zip"
mimeType:#"application/zip"];
}];
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
AFHTTPRequestOperation *operation =
[manager HTTPRequestOperationWithRequest:request
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"Success %#", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Failure %#", error.description);
}];
[operation setUploadProgressBlock:^(NSUInteger __unused bytesWritten,
long long totalBytesWritten,
long long totalBytesExpectedToWrite) {
NSLog(#"Wrote %lld/%lld", totalBytesWritten, totalBytesExpectedToWrite);
}];
[operation start];
Change this line
[operation start];
to this
[operation setShouldExecuteAsBackgroundTaskWithExpirationHandler:^{
// Handle iOS shutting you down (possibly make a note of where you
// stopped so you can resume later)
}];
[manager.operationQueue addOperation:operation];
You can look at these links
AFHTTPRequestOperation doesn't work after stand by mode
and Alternative for enqueueHTTPRequestOperation in AFNetworking 2.0
I am using AFNetworking and would like to upload an NSData object to my server. I am trying to upload to our SOAP server, so I have XML that I have converted to an NSData object. For some reason, the follow does NOT work:
// Back to NSData
NSData *convertedFile = [xml dataUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:siteConfiguration.soapURL];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
NSMutableURLRequest *request = [httpClient multipartFormRequestWithMethod:#"POST" path:#"" parameters:nil constructingBodyWithBlock: ^(id <AFMultipartFormData>formData) {
[formData appendPartWithFormData:convertedFile name:assetCreation.name];
}];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
// Progress
float totalProgress = (float)totalBytesWritten/(float)totalBytesExpectedToWrite;
progress(totalProgress, totalBytesWritten, totalBytesExpectedToWrite);
if (totalBytesExpectedToWrite == totalBytesWritten) {
completion();
}
}];
[operation start];
But when not using the multipartFormRequestWithMethod block, it DOES work and the file is correctly uploaded, but there is NO progress shown, as the callback is only called once:
// Back to NSData
NSData *convertedFile = [xml dataUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:siteConfiguration.soapURL];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod: #"POST"];
[request setHTTPBody:convertedFile];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
// Progress
float totalProgress = (float)totalBytesWritten/(float)totalBytesExpectedToWrite;
progress(totalProgress, totalBytesWritten, totalBytesExpectedToWrite);
if (totalBytesExpectedToWrite == totalBytesWritten) {
MatrixAsset *asset = [[MatrixAsset alloc] init];
completion(asset);
}
}];
[operation start];
It seems that my server really wants to be sent the HTTPBody with my NSData object. Is it possible to do the same with the AFNetworking progress callback?
Ok, I got it working. Looks like I was going about it wrong:
// Setup the connection
NSString *wsdlURL = [NSString stringWithFormat:#"%#?WSDL", siteConfiguration.soapURL];
NSURL *serviceUrl = [NSURL URLWithString:wsdlURL];
NSMutableURLRequest *serviceRequest = [NSMutableURLRequest requestWithURL:serviceUrl];
[serviceRequest setValue:#"text/xml" forHTTPHeaderField:#"Content-type"];
[serviceRequest setHTTPMethod:#"POST"];
[serviceRequest setHTTPBody:[xml dataUsingEncoding:NSUTF8StringEncoding]];
// Operation
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:serviceRequest];
[operation setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
// Progress
float totalProgress = (float)totalBytesWritten/(float)totalBytesExpectedToWrite;
progress(totalProgress, totalBytesWritten, totalBytesExpectedToWrite);
}];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"Success");
MatrixAsset *asset = [[MatrixAsset alloc] init];
completion(asset);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"error: %#", error.localizedDescription);
NSLog(#"error: %#", error.localizedFailureReason);
}];
[operation start];
You need SOAPAction to continue:
[serviceRequest setValue:#"SOAP_ACTION_URL" forHTTPHeaderField:#"SOAPAction"];
Is there any easy way to update a request's URL after it was initiated in AFNetworking?
Let's say my app is downloading a 2GB in chunks, we'll have 200 chunks 10MB each. For each I'm creating a AFHTTPRequestOperation and add it to the queue.
Problem is, that the server from which I'm downloading has a timeout on all URLs, which means I'll get 403 after that time and I have to generate a new one. I need to do it before the time runs out.
for (DownloadFile *downloadFile in [download filesInTheDownload])
{
for (DownloadChunk *downloadChunk in [downloadFile chunksInTheFile])
{
NSURL *fileURL = [[NSURL alloc] initWithString:[downloadFile filePath]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:fileURL];
NSLog(#"Normal range: %lli-%lli", [downloadChunk startingByte], [downloadChunk endingByte]);
NSString *range = [NSString stringWithFormat:#"bytes=%lli-%lli", newStartingByte, [downloadChunk endingByte]];
[request setValue:range forHTTPHeaderField:#"Range"];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:chunkPath append:YES];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"%#", [NSString stringWithFormat:#"Chunk complete: %#.%i", [downloadFile fileName], [downloadChunk chunkId]]);
if (download.downloadedBytes == download.size)
[[NSNotificationCenter defaultCenter] postNotificationName:#"downloadFinished" object:download];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
[operation setDownloadProgressBlock:^(NSInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
download.downloadedBytes += bytesRead;
}];
[queue addOperation:operation];
}
}
No, it's not possible to modify a scheduled operation. Generally it's a good thing, meaning that you can schedule the operation and be absolutely sure that it's going to be executed based on original conditions.
I see two options in your case. First, you can schedule operations one by one, and verify if the link is valid, updating it if required.
Second, you can react to 403 Forbidden by updating the link and re-scheduling same operation.