AFNetworking: interrupt background requests for higher priority requests - objective-c

Using AFNetworking, I need to download ~100 images in the background and store them to disk while ensuring any other network connectivity in my app takes precedence.
~~~~~~~
I've got an application that has 4 tabs. Each tab basically does the same thing: Pulls down a JSON response from a server, and displays a thumbnail grid of images, pulling down each image on demand (using AF's ImageView category). Tapping on a thumbnail takes you to a detail view controller where you see a larger image. The response and images are different for each tab.
There's a new requirement to fetch all of the images for the 4th tab ahead of time, so that theoretically by the time the user taps on the 4th tab, the JSON data and images are being read from disk.
I've got this working more or less right now, with the 4th tab pre-fetching and saving to disk being performed on a background thread so the main thread doesn't lock up. However, the network requests being kicked off when a user is on the 1st, 2nd or 3rd tab are being blocked by the pre-fetching network requests.
I'm using AFNetworking, and here is the code I'm using when Tab 1, 2 or 3 loads:
// this network request ends up getting blocked by the network request that
// is fired upon the application becoming active
- (void)getAllObjectDataWithBlock:(AFCompletionBlockWrapper)block
{
[[[MyAPIClient] sharedClient] getPath:#"" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
block(operation, responseObject, nil);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
block(operation, nil, error);
}];
}
And here is the code I'm using when my application becomes active, to pre-fetch content for the 4th tab:
// this network request runs in the background, but still blocks requests
// that should have a higher priority
- (void)applicationDidBecomeActive:(UIApplication *)application
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
NSOperationQueue *imageQueue = [[NSOperationQueue alloc] init];
[imageQueue setMaxConcurrentOperationCount:8];
for (NSString *imageURL in self.images) {
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL imageURL]];
AFImageRequestOperation *operation = [[AFImageRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"success");
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"fail");
}];
[imageQueue addOperation:smallOperation];
}
});
}
How can I structure things so any network requests kicked off from the main thread interrupt those that were kicked off in the background thread?

I don't know that you can easily interrupt running operations, unless you want to send them a cancel--but you'd have to look at whether AFImageRequestOperation pays attention to isCancelled.
Have you tried using setQueuePriority? You could start all the pre-fetching requests with a low priority, then add current tab requests with a higher priority. I believe running operations would complete, but once they complete, your higher-priority operations would get scheduled ahead of queued low-priority ones.

Related

Incrementing a Variable from an Asynchronous Block in Objective-C

I have run into a bit of a conundrum with a service I am working on in objective-c. The purpose of the service is to parse through a list of core-data entities and download a corresponding image file for each object. The original design of the service was choking my web-server with too many simultaneous download requests. To get around that, I moved the code responsible for executing the download request into a recursive method. The completion handler for each download request will call the method again, thus ensuring that each download will wait for the previous one to complete before dispatching.
Where things get tricky is the code responsible for actually updating my core-data model and the progress indicator view. In the completion handler for the download, before the method recurses, I make an asynchronous call the a block that is responsible for updating the core data and then updating the view to show the progress. That block needs to have a variable to track how many times the block has been executed. In the original code, I could simply have a method-level variable with block scope that would get incremented inside the block. Since the method is recursive now, that strategy no longer works. The method level variable would simply get reset on each recursion. I can't simply pass the variable to the next level either thanks to the async nature of the block calls.
I'm at a total loss here. Can anyone suggest an approach for dealing with this?
Update:
As matt pointed out below, the core issue here is how to control the timing of the requests. After doing some more research, I found out why my original code was not working. As it turns out, the timeout interval starts running as soon as the first task is initiated, and once the time is up, any additional requests would fail. If you know exactly how much time all your requests will take, it is possible to simply increase the timeout on your requests. The better approach however is to use an NSOperationQueue to control when the requests are dispatched. For a great example of how to do this see: https://code-examples.net/en/q/19c5248
If you take this approach, keep in mind that you will have to call the completeOperation() method of each operation you create on the completion handler of the downloadTask.
Some sample code:
-(void) downloadSkuImages:(NSArray *) imagesToDownload onComplete:(void (^)(BOOL update,NSError *error))onComplete
{
[self runSerializedRequests:imagesToDownload progress:weakProgress downloaded:0 index:0 onComplete:onComplete ];
}
-(void)runSerializedRequests:(NSArray *) skuImages progress:(NSProgress *) progress downloaded:(int) totalDownloaded index:(NSUInteger) index onComplete:(void (^)(BOOL update,NSError *error))onComplete
{
int __block downloaded = totalDownloaded;
TotalDownloadProgressBlock totalDownloadProgressBlock = ^BOOL (SkuImageID *skuImageId, NSString *imageFilePath, NSError *error) {
if(error==nil) {
downloaded++;
weakProgress.completedUnitCount = downloaded;
//save change to core-data here
}
else {
downloaded++;
weakProgress.completedUnitCount = downloaded;
[weakSelf setSyncOperationDetail:[NSString stringWithFormat:#"Problem downloading sku image %#",error.localizedDescription]];
}
if(weakProgress.totalUnitCount==weakProgress.completedUnitCount) {
[weakSelf setSyncOperationIndicator:SYNC_INDICATOR_WORKING];
[weakSelf setSyncOperationDetail:#"All product images up to date"];
[weakSelf setSyncOperationStatus:SYNC_STATUS_SUCCESS];
weakProgress.totalUnitCount = 1;
weakProgress.completedUnitCount = 1;
onComplete(false,nil);
return true;
}
return false;
};
NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:nil destination:nil
completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
NSLog(#"finished download %u of %lu", index +1, (unsigned long)skuImages.count);
if(error != nil)
{
NSLog(#"Download failed for URL: %# with error: %#",skuImage.url, error.localizedDescription);
}
else
{
NSLog(#"Download succeeded for URL: %#", skuImage.url);
}
dispatch_async(dispatch_get_main_queue(), ^(void){
totalDownloadProgressBlock(skuImageId, imageFilePath, error);
});
[self runSerializedRequests:manager skuImages:skuImages progress:progress downloaded:downloaded index:index+1 onComplete:onComplete ];
}];
NSLog(#"Starting download %u of %lu", index +1, (unsigned long)skuImages.count);
[downloadTask resume];
}
The original design of the service was choking my web-server with too many simultaneous download requests. To get around that, I moved the code responsible for executing the download request into a recursive method.
But that was never the right way to solve the problem. Use a single persistent custom NSURLSession with your own configuration, and set the configuration's httpMaximumConnectionsPerHost.

Method calling itself with identical params

I have several "proxy" classes, which inherit from a "base proxy". These classes connect to my server and pass data to their delegates. In event of 0 status code, I want to handle these different requests in the same way.
For 0 status codes, I want to retry the method in 5 seconds, hoping the user's internet connection has improved.
SomethingProxy.m
- (void)fetchSomething {
NSString *fullPath = [NSString stringWithFormat:#"%#/route/index.json",MY_BASE_URL];
NSURL *url = [NSURL URLWithString:fullPath];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [[AFJSONRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSDictionary *d = (NSDictionary *)responseObject;
[self.delegate fetchedPolicy:d[#"keypath"]];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[self handleOperationFailed:operation action:^{
[self fetchSomething];
}];
}];
NSOperationQueue *q = [[NSOperationQueue alloc] init]; [q addOperation:operation];
}
MyBaseProxy.m
- (bool)shouldRetryOperation:(AFHTTPRequestOperation *)o {
return self.retries < [self maxRetries];
}
- (void)handleOperationFailed:(AFHTTPRequestOperation *)o action:(ActionBlock)block {
NSInteger statusCode = o.response.statusCode;
if (statusCode == 0) {
if ([self shouldRetryOperation:o]) {
double delayInSeconds = 5.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
self.retries++;
block();
});
} else {
self.retries = 0;
[SVProgressHUD showErrorWithStatus:#"Please check your internet connection and try again"];
return;
}
}
self.retries = 0;
What's a better way to handle the request failure? Should I subclass AFHTTPRequestOperation?
EDIT: Removed confusing text. When I meant "same way", I meant per request Eg. handle all 500s the same, handle all 403s the same. I'm specifically asking for handling status code 0 - no internet connection.
The key situation you should be concerned with, IMHO, is when the network, itself, is not available to the device. In that case, you'd generally use Reachability to have a notification posted to your app when the network becomes available. No point in repeatedly retrying when (a) you know the network is unavailable; and (b) you easily can be notified when the network becomes available again.
If you're concerned about server-specific issues, I would be careful about send a request in 5 seconds. Let's say your server is overwhelmed and cannot respond to all the client requests. Having large numbers of clients proceeding to retry every 5 seconds might not improve the situation. It sort of depends upon why your server is not responding. You just want to make sure that the attempts to connect again will never make the situation worse.
(As an aside, the description for 403 says, "The server understood the request, but is refusing to fulfill it. Authorization will not help and the request SHOULD NOT be repeated.")
If you're going to employ a retry process, I'd specify some reasonable maximum number of retries (which, judging by your revised answer, you are doing). And perhaps you'd want something that when the app returns to foreground, you reinitiate your "retry three times, once every 15 seconds" logic (or whatever you settle upon). The scenario I'm thinking about is "user started app, it failed after exceeding max retries, and hit the home button on their device, and a few minutes/hours later, they tap on the app again (which might still be running)."

NSURLConnection sendSynchronousRequest/sendAsynchronousRequest fails after app killed by iOS

All the server connections in my app goes the same way:
dispatch_async to a 2nd thread.
sendSynchronousRequest
fail/success blocks on main thread.
The following is not the full code, just the concept:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSURL *url = [NSURL URLWithString:#"someURL"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
NSURLResponse *response = nil;
NSError *error;
NSData *data = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
if (error)
{
dispatch_async(dispatch_get_main_queue(), ^{ _failureBlock(error); });
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{ _successBlock(data); });
});
Well the app works great! I can use it for thousands of server connections, and it always works when I run a fresh load of the app.
The issue starts when the following occurs:
Send the app to BG.
Wait some time...
The app get killed by the iOS.
Open again, the iOS loads the whole app from scratch.
(see splash screen followed by the first screen, not the one I left to BG..)
In that scenario I get NSURLConnectionErrors!! All kind of them! Code 1003, Code 1009, Code 9... Just name it!
I get errors for all server connections that starts right when the app loads. Any other connections after that works fine! including the refresh of the ones that got failed!
It almost feel like i'm trying to reach my server too quick or too early, BUT I do check and pass a reachabillity test before intiating a connection.
Could really use some help here. Thanks in advance.
UPDATE: As suggested below, tried to use sendAsynchronousRequest - gave me the exact same behaviour. (err: 1001 this time).
OK got it! The appDelegate built the tabBar wich loaded all viewControllers wich sent some NSURLConnections... So for the iOS thought it's too long to wait for the responses and failed them all! (No idea way the first launch is ok and only after the app killed its not)
Anyway, I changed all server loads to perform with selector with 0.1 delay. That way the appDelegate could finish running and all is GOOD! :)
You should look at using this method and send the data Asynchronously
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
the NSOpertationQueue mainQueue uses the mainthread however allowing you to update UI also on the main thread i.e an activityindicator for example..

UITableView Refresh Data

I have a UITableViewController that when opened displays a table of the following object:
class {
NSString *stringVal;
int value;
}
However, whenever this controller opens, I want it to download the data from the internet and display "Connecting..." in the status bar and refresh the stringVal and value of all of the objects. I do this by refreshing the array in the UITableViewController. However, to do this the UI hangs sometimes or even displays "blank" table cells until the operation has ended. I'm doing this in an NSOperationQueue to download the data, but I'm wondering if there's a better way to refresh the data without those weird UI bugs.
EDIT:
the UI no longer displays blank cells. This was because cellForRowAtIndexPath was setting nil values for my cellText. However, it still seems somewhat laggy when tableView.reloadData is called even though I'm using NSOperationQueue.
EDIT2:
Moreover, I have two problems: 1. the scrolling prevents the UI from being updated and 2. when the scrolling does stop and the UI starts to update, it hangs a little bit. A perfect example of what I'm trying to do can be found in the native Mail app when you view a list of folders with their unread count. If you constantly scroll the tableview, the folders unread count will be updated without any hanging at all.
Based on your response in the question comments, it sounds like you are calling [tableView reloadData] from a background thread.
Do not do this. UIKit methods, unless otherwise specified, always need to be called from the main thread. Failing to do so can cause no end of problems, and you are probably seeing one of them.
EDIT: I misread your comment. It sounds like you are not updating the UI from a background thread. But my comments about the architecture (i.e. why are you updating in a background thread AFTER the download has finished?).
You state that "when the data comes back from the server, I call a background operation..." This sounds backwards. Normally you would have your NSURLConnection (or whatever you are using for the download) run on the background thread so as not to block to UI, then call out to the main thread to update the data model and refresh the UI. Alternatively, use an asynchronous NSURLConnection (which manages its own background thread/queue), e.g.:
[NSURLConnection sendAsynchronousRequest:(NSURLRequest *)
requestqueue:(NSOperationQueue *)queue
completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler];
And just make sure to use [NSOperationQueue mainQueue] for the queue.
You can also use GCD, i.e., nested dispatch_async() calls (the outer to a background queue for handling a synchronous connection, the inner on the main queue to handle the connection response).
Finally, I will note that you in principle can update your data model on the background thread and just refresh the UI from the main thread. But this means that you need to take care to make your model code thread-safe, which you are likely to mess up at least a couple times. Since updating the model is probably not a time consuming step, I would just do it on the main thread too.
EDIT:
I am adding an example of how one might use GCD and synchronous requests to accomplish this. Clearly there are many ways to accomplish non-blocking URL requests, and I do not assert that this is the best one. It does, in my opinion, have the virtue of keeping all the code for processing a request in one place, making it easier to read.
The code has plenty of rough edges. For example, creating a custom dispatch queue is not generally necessary. It blindly assumes UTF-8 encoding of the returned web page. And none of the content (save the HTTP error description) is localized. But it does demonstrate how to run non-blocking requests and detect errors (both at the network and HTTP layers). Hope this is helpful.
NSURL *url = [NSURL URLWithString:#"http://www.google.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
dispatch_queue_t netQueue = dispatch_queue_create("com.mycompany.netqueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(netQueue,
^{
// We are on a background thread, so we won't block UI events (or, generally, the main run loop)
NSHTTPURLResponse *response;
NSError *error;
NSData *data = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
dispatch_async(dispatch_get_main_queue(),
^{
// We are now back on the main thread
UIAlertView *alertView = [[UIAlertView alloc] init];
[alertView addButtonWithTitle:#"OK"];
if (data) {
if ([response statusCode] == 200) {
NSMutableString *body = [[NSMutableString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
[alertView setTitle:#"Success"];
[alertView setMessage:body];
}
else {
[alertView setTitle:#"HTTP Error"];
NSString *status = [NSHTTPURLResponse localizedStringForStatusCode:[response statusCode]];
[alertView setMessage:status];
}
}
else {
[alertView setTitle:#"Error"];
[alertView setMessage:#"Unable to load URL"];
}
[alertView show];
[alertView release];
});
});
dispatch_release(netQueue);
EDIT:
Oh, one more big rough edge. The above code assumes that any HTTP status code != 200 is an error. This is not necessarily the case, but handling this is beyond the scope of this question.

Simultaneous NSURLDownloads

I have a Cocoa Mac application set up to download files to a specific folder using NSURLDownload. This works great with a single download at a time. However, if I attempt to start multiple downloads, all but the last will fail immediately.
Is there any way to use NSURLDownload for multiple simultaneous downloads? Or what would be a good way to queue up multiple URLs to be downloaded in order? Or is there a more appropriate way to accomplish this (NSURLConnection seemed possible but I was unsure if I could set the download location and filename as I can with NSURLDownload)?
Each NSURLDownload represents a single downloading instance. You're probably trying to reuse the same one multiple times. It's an inherently asynchronous system that already used background threads. Here's an example based on Apple's sample code:
- (void)startDownloadingURL:sender
{
// Create a couple requests.
NSURLRequest *requestOne = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.apple.com"]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
NSURLRequest *requestTwo = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://stackoverflow.com"]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
// Create two download instances
NSURLDownload *downloadOne = [[NSURLDownload alloc] initWithRequest:requestOne delegate:self];
NSURLDownload *downloadTwo = [[NSURLDownload alloc] initWithRequest:requestTwo delegate:self];
if (downloadOne) {
// Set the destination file.
[downloadOne setDestination:#"/tmp" allowOverwrite:YES];
} else {
// inform the user that the download failed.
}
if (downloadTwo) {
// Set the destination file.
[downloadTwo setDestination:#"/tmp" allowOverwrite:YES];
} else {
// inform the user that the download failed.
}
}
- (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error
{
// Release the connection.
[download release];
// Inform the user.
NSLog(#"Download failed! Error - %# %#",
[error localizedDescription],
[[error userInfo] objectForKey:NSErrorFailingURLStringErrorKey]);
}
- (void)downloadDidFinish:(NSURLDownload *)download
{
NSLog(#"The download %# has finished.", download)
// Release the download connection.
[download release];
}
If you attempt to use the same NSURLDownload for both NSURLRequests, then it will kill the previous connection.
I'd second using NSOperation if your on 10.5+ or greater. You could just throw 1 operation onto a queue for each download. Or you could even just use sendSynchronous request and use it with NSOperationQUeue's addOperationWithBlock (10.6+) method and then in your block you are throwing onto the queue you can just use [[NSOperationQueue mainQueue] addOperationWithBlock:^{ when you are doe with the code you need to execute or just periodically need to refresh the UI on the main thread, like so...
[myQueue addOperationWithBlock:^{
//download stuff here...
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
//update main thread UI
}];
}];
you would just need to do this for each download.
If you're targeting 10.5+, you should look at NSOperation. It should allow you to build a generic solution for a single download, and then use the built-in queue facilities to manage dependencies if you require certain operations finish downloading before others begin.
Keep in mind that these APIs often expect delegate methods involved to be run on the main thread, so you'll need ensure that occurs if you're working with asynchronous APIs that function via delegate methods. (You can do this pretty simply by using performSelectorOnMainThread: and friends)