I have an iOS application which is using an NSOperationQueue, NSOperations and AFNetworking 2.1.0 to fire off requests to a server. The -[NSOperation main] method looks something like:
- (void)main {
AFHTTPSessionManager *sessionManager = [AFHTTPSessionManager sharedSessionManager];
[sessionManager GET:#"url"
parameters:nil
success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"Success");
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"Failure");
}
];
}
I have noticed that, from time to time, that the callbacks for a particular operation never get executed, when multiple operations are created and added to the NSOperationQueue in quick succession. I dove into AFNetworking to try to figure out why. I ended up in -[AFURLSessionManager dataTaskWithRequest:completionHandler], which looks like:
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler
{
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request];
AFURLSessionManagerTaskDelegate *delegate = [AFURLSessionManagerTaskDelegate delegateForManager:self completionHandler:completionHandler];
[self setDelegate:delegate forTask:dataTask];
return dataTask;
}
I added a logging statement right after dataTask is created:
NSLog(#"Task with id %# created for %# on queue %#", #(dataTask.taskIdentifier), request.URL.path, dispatch_get_current_queue());
The log reveals the problem:
2014-02-26 14:11:25.071 App[50094:6a2f] Task with id 15 created for /url1 on queue <OS_dispatch_queue: NSOperationQueue 0xc4b8560[0xc4b8ac0]>
2014-02-26 14:11:25.071 App[50094:460f] Task with id 16 created for /url2 on queue <OS_dispatch_queue: NSOperationQueue 0xc4b8560[0xc4b8ac0]>
2014-02-26 14:11:26.274 App[50094:6a2f] Task with id 18 created for /url2 on queue <OS_dispatch_queue: NSOperationQueue 0xc4b8560[0xc4b8ac0]>
2014-02-26 14:11:26.274 App[50094:6c17] Task with id 17 created for /url1 on queue <OS_dispatch_queue: NSOperationQueue 0xc4b8560[0xc4b8ac0]>
2014-02-26 14:11:27.546 App[50094:6307] Task with id 20 created for /url2 on queue <OS_dispatch_queue: NSOperationQueue 0xc4b8560[0xc4b8ac0]>
2014-02-26 14:11:27.546 App[50094:6b17] Task with id 19 created for /url1 on queue <OS_dispatch_queue: NSOperationQueue 0xc4b8560[0xc4b8ac0]>
2014-02-26 14:11:28.705 App[50094:6b17] Task with id 21 created for /url1 on queue <OS_dispatch_queue: NSOperationQueue 0xc4b8560[0xc4b8ac0]>
2014-02-26 14:11:28.705 App[50094:6307] Task with id 21 created for /url2 on queue <OS_dispatch_queue: NSOperationQueue 0xc4b8560[0xc4b8ac0]>
2014-02-26 14:11:32.091 App[50094:6307] Task with id 22 created for /url2 on queue <OS_dispatch_queue: NSOperationQueue 0xc4b8560[0xc4b8ac0]>
2014-02-26 14:11:32.091 App[50094:6b17] Task with id 23 created for /url1 on queue <OS_dispatch_queue: NSOperationQueue 0xc4b8560[0xc4b8ac0]>
Notice the fourth set in the log has the same taskIdentifier which is what AFNetworking uses to associate tasks with their callbacks, via delegate.
If I force the NSOperations to run on the main queue, then I am unable to recreate the issue - the taskIdentifier is always unique.
Has anyone seen anything like this before? Do I need to ensure that -[NSURLSession dataTaskWithRequest:] runs only on the main thread in order to not get taskIdentifier collisions?
Don't know if this is still relevant to you or not, but this exact thing had me banging my head around all night. Turns out you are partially answering the problem in your question.
As it would turn out, if
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request];
is run asynchronously, then there is the small chance that the instance of NSURLSession will assign the same taskIdentifier to different tasks.
Without forking AFNetworking and synchronizing on all of the dataTaskWithRequest: methods then there were two ways that I could go about fixing this that stood out.
If you don't need the NSURLSessionTask returning from this method then the best way would be to make your own dispatch queue and any requests you want to make with your session manager you just send it to that queue asynchronously like so:
static dispatch_queue_t my_queue;
my_queue = dispatch_queue_create("MyQueueName", DISPATCH_QUEUE_CONCURRENT);
// ... Later, that very same day
dispatch_async(my_queue, ^{
// [sessionManager GET: ...
});
The only problem with this method for your issue however was that it all seemed to be executed on the same operation queue, so maybe this way wouldn't work in your case. The other way (which is how I did it) is actually waay simpler. Just synchronize on the AFURLSessionManager so the invocation of dataTaskWithRequest: can only ever happen synchronously like so:
#synchronized(sessionManager) {
// [sessionManager GET: ...
}
Or, yes, you could just do the task creations on the main thread. But for some projects its not always as simple as that.
I would enqueue a NSURLSessionDataTask to your AFURLSessionManager instead of trying to directly make GET requests. The session manager is intended to abstract the functionality of NSURLSession.
Example:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
NSURL *URL = [NSURL URLWithString:#"http://example.com/testapi.php"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
if (error) {
NSLog(#"error!");
} else {
NSLog(#"task successful!");
}
}];
[dataTask resume];
Related
My app get a silent push, then do some http request in background using AFNetworking,but it didn't enter the complete block, code is:
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] init];
[manager GET:urlString
parameters:nil
success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"response objece:%#", responseObject);
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"error:%#", error);
}];
then I found maybe I could use NSURLSessionConfiguration to config the session:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:#"com.company.backgroundDownloadSession"];
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:configuration];
[manager GET:urlString
parameters:nil
success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"response objece:%#", responseObject);
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"error:%#", error);
}];
but AFNetworking crash says:'Terminating app due to uncaught exception 'NSGenericException', reason: 'Data tasks are not supported in background sessions.'
What should I do? And I appreciate your help!
A couple of thoughts:
Proper background NSURLSessionConfiguration requires NSURLSessionDownloadTask or NSURLSessionUploadTask. The GET method, though, creates NSURLSessionDataTask.
To use download or upload tasks, you'll have to build your request separately, and then leverage AFURLSessionManager to issue download or upload. Having said that, you can, though, create requests using the various request serializers, if you're trying to create HTTP GET/POST style requests. Just use the AFHTTPRequestSerializer method requestWithMethod.
For a basic introduction to using AFNetworking in conjunction with background NSURLSessionConfiguration, see https://stackoverflow.com/a/21359684/1271826. You'll have to marry that with the requestWithMethod, discussed above.
Note, be wary about using the task-specific completion blocks (because the tasks continue even if the app is terminated and these blocks are long gone). As the AFNetworking documentation for the background download tasks says:
Warning: If using a background NSURLSessionConfiguration on iOS, these blocks will be lost when the app is terminated. Background sessions may prefer to use setDownloadTaskDidFinishDownloadingBlock: to specify the URL for saving the downloaded file, rather than the destination block of this method.
If you're making a modest request, it may be easier to just ask the OS for a little time to do so if the user happens to leave your app while the request is still in progress. See Executing Finite-Length Tasks section of App Programming Guide for iOS: Background Execution.
Bottom line, before issuing the request, do something like:
UIApplication *application = [UIApplication sharedApplication];
bgTask = [application beginBackgroundTaskWithName:#"MyTask" expirationHandler:^{
// Clean up any unfinished task business by marking where you
// stopped or ending the task outright.
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
And then in the completion block of the request, you can terminate the background task:
if (bgTask != UIBackgroundTaskInvalid) {
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}
This is only good for finite length tasks, but it probably much easier than trying to do background NSURLSessionConfiguration.
I am requesting an API that takes some time to give response, so during that time no other operation can be performed. e.g. back button or Tabs are not pressed. I am Using the following code:
NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
NSURL * url = [NSURL URLWithString:urlString];
NSURLSessionDataTask * dataTask = [defaultSession dataTaskWithURL:url
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if(error == nil)
{
}else{
}
}];
[dataTask resume];
Can anyone Suggest why my app is freezing during this operation. Thanks in advance.
Because you are performing operation in main thread, you need to do this task in background thread.
For this you can use NSOperationQueue and add a operation of you api calling.
see belo links
NSOperation and NSOperationQueue working thread vs main thread
How To Use NSOperations and NSOperationQueues
Working with the NSOperationQueue Class
or you can also use DispatchQueue
see : Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// simply call your function here
});
you are doing operation on main thread swhich interrupt app execution. you should do this operation in background through GCD by creating async request to download data in background it will not interrupt your app execution.
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{
NSURL * url = [NSURL URLWithString:urlString];
NSData *response = [[NSData alloc] initWithContentsOfURL:url];
// above code will download data in background
dispatch_async(dispatch_get_main_queue(), ^{
// here you can access main thread of application and do something here
});
});
I have a program that download a video from a url using NSURLSession, but i'm not able to do multiple download at the same time.
How can i do it?
How can i manage multiple simultaneous download?
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
NSURLSessionDownloadTask *getVideo = [session downloadTaskWithURL:fileURL
completionHandler:^(NSURL *location,
NSURLResponse *response,
NSError *error) {
// 2
receivedData = [NSData dataWithContentsOfURL:location];
dispatch_async(dispatch_get_main_queue(), ^{
// do stuff with image
NSLog(#"%s receiveData:%d",__FUNCTION__,[receivedData length]);
});
}];
[getVideo resume];
From the code you have provided above you are not using any of the properties of NSURLSessionConfiguration class that would enable better download performance.
First of all I would look suggest using your own delegate queue. If you do not provide a queue then the session creates a serial operation queue for all delegate and completion handler calls see the "Creating a Session" section of the NSURLSession Class Reference document for more detail. You can look at the following properties of NSOperationQueue to help improve performance;
qualityOfService
maxConcurrentOperationCount
Next I would look at NSURLSessionConfiguration properties that may help.
HTTPMaximumConnectionsPerHost
HTTPShouldUsePipelining
Finally you should review the section "Life Cycle of a URL Session with Custom Delegates". You should confirm whether your using the delegate methods of NSURLSessionTaskDelegate and NSURLSessionDownloadTaskDelegate or just the completion handler.
You need to put more time into configuring NSURLSession to support the work you want to do.
I want to execute a series of AFJSONRequestOperation in order, and be able to interrupt the queue when one fails.
At the moment, the way I do it is not reliable, as sometimes the next operation will get a chance to start.
I have a singleton to call my api endpoint
AFJSONRequestOperation *lastOperation; // Used to add dependency
NSMutableArray *operations = [NSMutableArray array]; // Operations stack
AFAPIClient *httpClient = [AFAPIClient sharedClient];
[[httpClient operationQueue] setMaxConcurrentOperationCount:1]; // One by one
And then I add the operations this way
NSMutableURLRequest *request = ...; // define request
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
// Takes care of success
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
[[httpClient operationQueue] setSuspended:YES];
[[httpClient operationQueue] cancelAllOperations];
}];
[push:operation addDependency:lastOperation];
[operations $push:operation]; // This is using ConciseKit
lastOperation = operation;
// Repeat with other operations
// Enqueue a batch of operations
[httpClient enqueueBatchOfHTTPRequestOperations:operations ...
Trouble is, sometimes the operation following the one that fails still gets a chance to start.
So it seems that that having 1 concurrent operation max and a dependency chain isn't enough to tell the queue to wait until after the failure callback is fully executed.
What's the proper way to do this ?
Thanks
The failure callback is executed on the main thread and the operation (which is running on a background thread) doesn't wait for it. So, you'd need to do some editing to prevent the next operation from starting before the operation and its completion blocks have completed.
Or, instead of putting all the operations into the queue at the start, hold the list of operations in an array and just add the next operation following each success.
I am doing a lot of URL requests (about 60 small images) and I have started to do them Asynchronously. My code adds another image (little downloading thing) and then sets a Request going.
When the request is done I want "data" to be put in the location which was originally added for it, however, I can not see how to pass "imageLocation" to the block for it to store the image in the correct location.
I have replaced the 3rd line with below which seems to work but I am not 100% it is correct (it is very hard to tell as the images are nearly identical). I am also thinking that it is possible to pass "imageLocation" at the point where the block is declared.
Can any confirm any of this?
__block int imageLocation = [allImages count] - 1;
// Add another image to MArray
[allImages addObject:[UIImage imageNamed:#"downloading.png"]];
imageLocation = [allImages count] - 1;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[request setTimeoutInterval: 10.0];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue currentQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (data != nil && error == nil)
{
//All Worked
[allImages replaceObjectAtIndex:imageLocation withObject:[UIImage imageWithData:data]];
}
else
{
// There was an error, alert the user
[allImages replaceObjectAtIndex:imageLocation withObject:[UIImage imageNamed:#"error.png"]];
}];
Dealing with asynchronous methods is a pain ;)
In your case its guaranteed that the completion block will execute on the specified queue. However, you need to ensure that the queue has a max concurrent operations count of 1, otherwise concurrent access to shared resources is not safe. That's a classic race http://en.wikipedia.org/wiki/Race_condition. The max concurrent operations of a NSOperationQueue can be set with a property.
In general, completion handlers may execute on any thread, unless otherwise specified.
Dealing with asynchronous methods gets a lot easier when using a concept called "Promises" http://en.wikipedia.org/wiki/Promise_(programming). Basically, "Promises" represent a result that will be evaluated in the future - nonetheless the promise itself is immediately available. Similar concepts are named "futures" or "deferred".
There is an implementation of a promise in Objective-C on GitHub: RXPromise. When using it you also get safe access from within the handler blocks to shared resources. An implementation would look as follows:
-(RXPromise*) fetchImageFromURL:(NSString*)urlString queue:(NSOperationQueue*) queue
{
#autoreleasepool {
RXPromise* promise = [[RXPromise alloc] init];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[NSURLConnection sendAsynchronousRequest:request
queue:queue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (data != nil) {
[promise fulfillWithValue:data];
}
else { // There was an error
[promise rejectWithReason:error];
};
}];
return promise;
}
}
Then call it:
- (void) fetchImages {
...
for (NSUInteger index = 0; index < N; ++index)
{
NSString* urlString = ...
[self fetchImageFromURL:urlString, self.queue]
.then(^id(id data){
[self.allImages replaceObjectAtIndex:index withObject:[UIImage imageWithData:data]];
return #"OK";
},
^id(NSError* error) {
[self.allImages replaceObjectAtIndex:index withObject:[UIImage imageNamed:#"error.png"]];
return error;
});
}
}
A couple of thoughts:
If you want to download 60 images, I would not advise using a serial queue for the download (e.g. do not use an operation queue with maxConcurrentOperationCount of 1), but rather use a concurrent queue. You will want to synchronize the updates to make sure your code is thread-safe (and this is easily done by dispatching the updates to a serial queue, such as the main queue, for that final update of the model), but I wouldn't suggest using a serial queue for the download itself, as that will be much slower.
If you want to use the NSURLConnection convenience methods, I'd suggest something like the following concurrent operation request approach (where, because it's on a background queue, I'm using sendSynchronousRequest rather than sendAsynchronousRequest), where I'll assume you have an NSArray, imageURLs, of NSURL objects for the URLs of your images:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"all done %.1f", CFAbsoluteTimeGetCurrent() - start);
NSLog(#"allImages=%#", self.allImages);
}];
[imageURLs enumerateObjectsUsingBlock:^(NSURL *url, NSUInteger idx, BOOL *stop) {
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSURLResponse *response = nil;
NSError *error = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url] returningResponse:&response error:&error];
if (!data) {
NSLog(#"%s sendSynchronousRequest error: %#", __FUNCTION__, error);
} else {
UIImage *image = [UIImage imageWithData:data];
if (image) {
dispatch_sync(dispatch_get_main_queue(), ^{
[self.allImages replaceObjectAtIndex:idx withObject:image];
});
}
}
}];
[queue addOperation:operation];
[completionOperation addDependency:operation];
}];
[[NSOperationQueue mainQueue] addOperation:completionOperation];
A couple of asides: First, I'm using an operation queue rather than a GCD concurrent queue, because it's important to be able to constrain the degree on concurrency. Second, I've added a completion operation, because I assume it would be useful to know when all the downloads are done, but if you don't need that, the code is obviously simper. Third, that benchmarking code using CFAbsoluteTime is unnecessary, but useful solely for diagnostic purposes if you want to compare the performance using a maxConcurrentOperationCount of 4 versus 1.
Better than using the NSURLConnection convenience methods, above, you might want to use a NSOperation-based network request. You can write your own, or better, use a proven solution, like AFNetworking. That might look like:
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"all done %.1f", CFAbsoluteTimeGetCurrent() - start);
NSLog(#"allImages=%#", self.allImages);
}];
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.responseSerializer = [AFImageResponseSerializer serializer];
manager.operationQueue.maxConcurrentOperationCount = 4;
[imageURLs enumerateObjectsUsingBlock:^(NSURL *url, NSUInteger idx, BOOL *stop) {
NSOperation *operation = [manager GET:[url absoluteString] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
[self.allImages replaceObjectAtIndex:idx withObject:responseObject];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"%s image request error: %#", __FUNCTION__, error);
}];
[completionOperation addDependency:operation];
}];
[[NSOperationQueue mainQueue] addOperation:completionOperation];
Because AFNetworking dispatches those completion blocks back to the main queue, that solves the synchronization issues, while still enjoying the concurrent network requests.
But the main take-home message here is that an NSOperation-based network request (or at least one that uses NSURLConnectionDataDelegatemethods) opens additional opportunities (e.g. you can cancel all of those network requests if you have to, you can get progress updates, etc.).
Frankly, having walked through two solutions that illustrate how to download the images efficiently up-front, I feel compelled to point out that this is an inherently inefficient process. I might suggest a "lazy" image loading process, that requests the images asynchronously as they're needed, in a just-in-time (a.k.a. "lazy") manner. The easiest solution for this is to use a UIImageView category, such as provided by AFNetworking or SDWebImage. (I'd use AFNetworking's if you're using AFNetworking already for other purposes, but I think that SDWebImage's UIImageView category is a little stronger.) These not only seamlessly load the images asynchronously, but offer a host of other advantages such as cacheing, more efficient memory usage, etc. And, it's as simple as:
[imageView setImageWithURL:url placeholder:[UIImage imageNamed:#"placeholder"]];
Just a few thoughts on efficiently performing network requests. I hope that helps.