Recursive / Iterative NSURLSessionDataTask causing memory leak - ios

I am having a problem with memory leaking in my code, I have a need to GET many URL's in quick succession, each GET is influenced by the result of the previous GET. The purpose is to look for a specific piece of content within the response.
I found the cleanest way to implement this is recursively, as I can use the same method to identify if the desired value is present in the response. Functionally it works very well, but it leaks memory as described below. I have also implemented the same functionality in an iterative fashion, and this also leaks memory.
To my mind it seems that the NSURLSession API is responsible for leaking this memory, and it only occurs when multiple calls are made in very quick succession. However, I would appreciate if anyone can point out any obvious mistakes I am making.
Update 10/09/14:
Updated to add a recursion counter, demonstrating the leak still occurs even if the code isn't executed an infinite number of times. Also tidied up the implementation slightly, re-using the NSURLSession and NSURLSessionConfiguration as properties within the view controller.
Sample Code:
- (void)performURLCallRecursive {
recursionLimiter++;
if (recursionLimiter > 10) {
[self.session finishTasksAndInvalidate];
return;
}
NSURL * checkURL = [NSURL URLWithString:#"http://www.google.com"];
__block NSMutableURLRequest * urlRequest = [[NSMutableURLRequest alloc] initWithURL:checkURL
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:0.0f];
__weak typeof(self) weakSelf = self;
NSURLSessionDataTask * task = [self.session dataTaskWithRequest:urlRequest
completionHandler:^(NSData *data, NSURLResponse *response, NSError
*error) {
NSString * body = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"Body: %#", body);
[weakSelf performURLCallRecursive];
}];
[task resume];
}
#pragma mark - Getters
- (NSURLSessionConfiguration *)sessionConfiguration {
if (!_sessionConfiguration) {
_sessionConfiguration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
[_sessionConfiguration setAllowsCellularAccess:NO];
[_sessionConfiguration setTimeoutIntervalForRequest:10.0f];
[_sessionConfiguration setTimeoutIntervalForResource:10.0f];
[_sessionConfiguration setURLCache:[[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil]];
}
return _sessionConfiguration;
}
- (NSURLSession *)session {
if (_session == nil) {
_session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration
delegate:[SPRSessionDelegate new]
delegateQueue:nil];
}
return _session;
}
The memory leaks as reported by instruments. (NB: These vary slightly every time, but for the most part contain the same leaks, just more or less of the same leaks):
Further Update:
So, I actually implemented the same code iteratively, and the memory leak still occurs. For this example I included a loop limiter so it doesn't execute for ever. Can anyone help me figure out what on earth is going on here?
- (void)performURLCallIterative
{
int loopLimiter = 0;
do {
NSURLSessionConfiguration * defaultSession = [NSURLSessionConfiguration defaultSessionConfiguration];
[defaultSession setAllowsCellularAccess:NO];
[defaultSession setTimeoutIntervalForRequest:10.0f];
[defaultSession setTimeoutIntervalForResource:10.0f];
NSURLSession * session = [NSURLSession sessionWithConfiguration:defaultSession
delegate:self
delegateQueue:nil];
NSURL * checkURL = [NSURL URLWithString:#"http://google.com"];
NSMutableURLRequest * urlRequest = [[NSMutableURLRequest alloc] initWithURL:checkURL
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:0.0f];
__weak NSURLSession * weakSession = session;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSessionDataTask * task = [session dataTaskWithRequest:urlRequest
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSString * body = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"Body: %#", body);
dispatch_semaphore_signal(semaphore);
[weakSession invalidateAndCancel];
}];
[task resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
loopLimiter++;
} while (loopLimiter <= 6);
}
Update 10/09/14:
This is still occurring on iOS 8 for any Googlers who may have found their way here. As far as I am concerned this is a bug in iOS.

- Update 9/12/2014
Solution: wait for iOS8.
- Update 9/10/2014
Whoa, this is spiraling into some Nth dimension of complexity :P. I hope one way or another you get a break here quick.
I have a few other things for you to try.
1) Could you make sure NSZombies is turned off. In Xcode, Product->Scheme->Edit Scheme...->Enable Zombie Objects (NOT ticked).
2) Also try cachePolicy:NSURLCacheStorageNotAllowed for your NSMutableURLRequest.
3) Could you see if you are completing with an error? Just put this around your body string assignment...
if (error == nil)
{
//Enter data->string code here
}
4) Could you see if you are not getting status 200?
NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
5) It is hard to picture exactly how your project is set up. I would have an NSObject type class that houses the NSURLSession methods, which is separate from the UIViewController class from which it is being called. The timer or whatever recursion method you wish to choose would then call the url session associated methods from the UIViewController.
- Update 9/9/2014
You are correct about my question (2). The data task is resumed before completion and after the data task completes the session is invalidated. I haven't seen it done this way, but it makes sense. Just tested on my end, no leaks with regards to [session invalidateAndCancel]...
Could you check that your completion handler executes? Perhaps it doesn't and the session is never cancelled before a new task is started?
I am noticing that there are a few references to HTTP Headers in the Instruments Leaks report, maybe if you are not specifying either a [urlRequest setHTTPMethod:#"GET"] the request is missing some basic headers?
(I'll edit after we find the solution, so this doesn't look like a discussion).
- Original 9/8/2014
Interesting question! I have troubleshot leaks associated with NSURLSessions. Definitely #autoreleasepool{} and others are good suggestions to try so far... But!
I am afraid the thing you asked us to look past might be the culprit here.
Just a few observations first:
1) It is not clear to me why you would need to __weak the self here. What is the retain cycle you are trying to avoid? Perhaps this is more clear in the code you are actually using aside from your "sample".
2) What is the reason for the call to invalidate the session before the data task associated with that session even has a chance to complete, let alone resume. The data task is in the suspended state until resumed.
3) If you are recursively running a method like this, then I think it is crucial to specify or at least consider what delegate queue, otherwise having it set to nil defaults it to serial operation queue. What happens when the delegate calls before the completion handler finishes, in an infinite loop - most likely a huge pile up.
--
I believe that the main issue here is that you are starting a new or canceling the NSURLSessionDataTask before it has a chance to complete. Look at +sesssionWithConfiguration:
(sorry can't include pictures yet, hopefully after this answer)
https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSession_class/Introduction/Introduction.html#//apple_ref/occ/clm/NSURLSession/sessionWithConfiguration:
The point is here...
Important
The session object keeps a strong reference to the delegate
until your app explicitly invalidates the session. If you do not
invalidate the session by calling the invalidateAndCancel or
resetWithCompletionHandler: method, your app leaks memory.
My suggestion to try is...
//Your code above...
[task resume];
[session finishTasksAndInvalidate];
}
In theory this should prevent any new sessions from starting before completion, according to the description, "...new tasks cannot be created in the session, but existing tasks continue until completion. After the last task finishes and the session makes the last delegate call, references to the delegate and callback objects are broken..."
I am still not sure about invalidating the session before resuming it.
I hope this helps. Good luck.

A developer support request to Apple reveals this to be a bug within iOS 7. There is no fault with the code sample posted above (Either recursively or iteratively) and it has reportedly been fixed in the iOS 8 GM release.
Update:
This is still occurring in iOS 8.1

I had a lot of problems with memory from NSURLSession and I finally fixed it by not using a new session for each request. Sessions are generally defined on Wikipedia as:
a semi-permanent interactive information interchange
As such, Apple's convenience class method [NSURLSession sharedSession] gives us a clue of how NSURLSession objects are intended to be used: as semi-permanent objects, not one-off objects created fresh for each request, like you are doing.
You are making a new session object per request for a ton of requests that, from the server's perspective, are all part of a single session with a single client.
I was doing the same thing until I realized this was the source of my woes. I did not find Apple's documentation on this very clear, but after I realized the error of my ways, it made certain things in the documentation suddenly make more sense, like why there is a sharedSession singleton convenience method of NSURLSession, why the word "tasks" is plural in finishTasksAndInvalidate, why they called it a "session", why it has a cache, etc. (If it was just for one request, why would it be a "session" and what good would a "cache" be?)
It helps to know how a browser like Safari looks at a session. A new session starts the first time you make a connection to a given server. Setting up the session involves creating a cache of SSL certificates, establishing authentication, handshaking, etc. It would be extraordinarily inefficient to do all of that every time some JavaScript on a page makes a new request to the same server, especially since modern web apps constantly make requests with callbacks etc. That is why a single session is established for a whole huge set of requests and responses -- a conversation, if you will, between the client and server. Eventually, a session expires, but usually this happens after several minutes, not after one request!
The point is, how you should be using NSURLSession objects is to make a singleton with a strongly referenced NSURLSession object as a property. Do this if you need to customize the session's configuration, (like turning caching off, etc.). However if you do not need to customize it, just use Apple's sharedSession.
If you use a singleton on a custom class, then, if you never need to set the session property to nil, then you never need to invalidateAndCancel or finishTasksAndInvalidate. Instead, just resetWithCompletionBlock or flushWithCompletionBlock to clear out connection caches periodically.
If you hate singletons you can still use a session as a property, just make sure to invalidateAndCancel or finishTasksAndInvalidate the session before its last owner gets deallocated by the ARC runtime.
Also note that setting your NSURLSession object's URLCache property to nil is the proper way to shut off caching. That's what Apple says they do for backgroundSessionConfiguration.
See my other answers on this topic here and here.

The only suggestions I have are perhaps using an #autoreleasepool{} and converting the __weak id self to __block id self. I don't think the __block vs. __weak will do anything differently, but give it a shot.
I'm not sure what one should expect with ARC when running something asynchronously AND recursively. Looking at other questions with asynchronous recursive calls and ARC, there isn't any consistent solution. Take a look here, for example.

Related

NSURLSession: method dataTaskWithRequest never reach completion callback on lengthy responses

The following code that used to create communication session with a remote server and send/receive HTTP requests/responses.
However, when a large file is attached to the response, the callback block never reached.
Only when explicitly invoke the cancel method after some timeout from the NSURLSession task (_dataTask), this callback is being called.
notice that using tcpdump it can easily observed that the response was properly received on the client side.
NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1;
NSURLSession* session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:queue];
_dataTask = [session dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if ([error code] == NSURLErrorCancelled) {
writeLog(LOG_ERROR, "NSURLErrorCancelled");
} else {
...
}
}];
[_dataTask resume]
// after timeout, the operation is cancelled.
sleep(100)
[_dataTask cancel];
I'd like to know if using dataTask has response length limit (because it's working for small files on response body) and if there is such a limit, so which other method should I use in order to overcome it.
I saw that there's an alternative method in NSUrlsession dedicated for downloading files called downloadTaskWithRequest but it doesn't have an async completion block.
Thanks !
When fetching large resources, you should use download task. A data task will attempt to load the entire response in a single NSData object. Loading a large asset in memory at the same time is not only inefficient, but if it is extraordinarily large, can cause problems.
A download task is well suited for these tasks, because it will stream the asset to a temporary file for you, reducing the peak memory usage. (Admittedly, you can manually achieve the same with data task with delegate pattern, but download tasks do this for you.)
You said:
I saw that there's an alternative method in NSURLSession dedicated for downloading files called downloadTaskWithRequest but it doesn't have an async completion block.
Two observations:
There is a rendition, dataTaskWithRequest:completionHandler:, that has a completion block:
NSURLSession* session = [NSURLSession sharedSession];
NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:request completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
...
}];
[task resume];
Note, I would advise using sharedSession if you are not setting a delegate, or otherwise customizing your NSURLSession. You do not want to instantiate NSURLSession objects unnecessarily. And if you really must instantiate a NSURLSession, re-use it for subsequent tasks and/or make sure to call finishTasksAndInvalidate after submitting the last task for that session, or else the NSURLSession will leak. And, if you instantiate your own NSURLSession, you do not have to instantiate your own operation queue, as it will, by default, create a serial queue for you if you do not supply an operation queue.
The rendition without a block parameter, downloadTaskWithURL:, works, too. All you need to do is to specify a delegate for your NSURLSession and then and implement URLSession:downloadTask:didFinishDownloadingToURL:.
The reason I suggest this is that, often, when we are downloading very large assets (especially over cellular), we realize that the users may want to leave our app and let the download complete in the background. In those situations, we would use a background NSURLSessionConfiguration. And when using background sessions, you must use this delegate-based approach. So, if you think you might eventually adopt background sessions for long downloads, then adopting a delegate-based approach now is not a bad idea.
For more information, see Downloading Files in the Background.

Task created in a session that has been invalidated

I am getting the following crash on crashlytics which I can not reproduce on my device
Fatal Exception: NSGenericException
Task created in a session that has been invalidated
at the following line
NSURLSessionTask *task = [self.session uploadTaskWithRequest:request fromFile:filePathURL];
[task resume];
[self.session finishTasksAndInvalidate];
I handled session invalidation at the delegate method
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error {
// Crashlytics logging
[CrashlyticsKit setBoolValue:true forKey:#"URLSession_didBecomeInvalid"];
self.session = [self createNewSession];
}
- (NSURLSession *)CreateSession {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:SERVER_URL];
if (#available(iOS 11.0, *)) {
sessionConfig.waitsForConnectivity = YES;
}
return [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
}
after uploading a new build, I still have the same crash and no Crashlytics logs at "didBecomeInvalidWithError" at all!
any idea how to solve this crash?
If you invalidate a session, you cannot ever use it again. If you attempt to use a previously invalidated session, you’ll get that error you shared with us.
That leaves you with two options:
If you must invalidate a session, I’d recommend setting your session reference to nil so you can’t accidentally use it again. If you have to do another request later, you would instantiate a new session object.
If you might ever need to use the session again, you can simply refrain from invalidating it. The memory impact of keeping a single session object around is largely inconsequential. It only really an issue if you’re creating many sessions.
Frankly, background sessions are complicated enough that I’d need a pretty compelling case for dealing with multiple ones (e.g. each with its own completion handler passed to my app delegate). I’d lean towards the single background session pattern if possible.

AWS SDK - Implementing a network queue for CloudFront downloads

I'm currently working on an iOS project that utilises the AWS SDK to download large media files to the device. I am using CloudFront to distribute the content and the downloads are working well, however I am having problems implementing a network queue for these operations. No matter what I try, all the files want to download at once.
I am using the AWSContent downloadWithDownloadType: method to initiate and monitor progress on the actual downloads.
I have tried using an NSOperationQueue and setting setMaxConcurrentOperationCount, and all the code blocks execute at once. :(
I have a feeling it might be configurable with AWSServiceConfiguration in the AppDelegate, but the documentation is extremely vague on what variables you can pass into that object... http://docs.aws.amazon.com/AWSiOSSDK/latest/Classes/AWSServiceConfiguration.html
Has anyone had any experience with this?
TIA
Your problem is most likely that you misunderstand an approach of asynchronous operations.
I have tried using an NSOperationQueue and setting
setMaxConcurrentOperationCount, and all the code blocks execute at
once. :(
It's difficult to say what's definitely wrong without seeing an actual code, however most likely it's tied to the following steps:
You create NSOperationQueue
You set maxConcurrentOperationsCount to 2 for example
You add 4 blocks to it with AWSContent downloadWithDownloadType:
You expect no more 2 downloads to be run simultaneously
What do you probably do wrong
The key is inside point 3. What exactly the block does? My guess is that it completes before actual download completes. So if you have something like:
NSOperationQueue *queue = [NSOperationQueue new];
queue.maxConcurrentOperationsCount = 2;
for (AWSContent *content in contentArray) { // Assume you already do have this array
[queue addOperationWithBlock:^() {
[content downloadWithDownloadType:AWSContentDownloadTypeIfNotCached
pinOnCompletion:YES
progressBlock:nil
completionHandler:^(AWSContent *content, NSData *data, NSError *error) {
// do some stuff here on completion
}];
}];
}
Your block exits before your download is finished, allowing next blocks to run on queue and starting further downloads.
What to try
You should simply add some synchronization mechanism to your block to let operation complete only on completion block. Say:
NSOperationQueue *queue = [NSOperationQueue new];
queue.maxConcurrentOperationsCount = 2;
for (AWSContent *content in contentArray) { // Assume you already do have this array
[queue addOperationWithBlock:^() {
dispatch_semaphore_t dsema = dispatch_semaphore_create(0);
[content downloadWithDownloadType:AWSContentDownloadTypeIfNotCached
pinOnCompletion:YES
progressBlock:nil
completionHandler:^(AWSContent *content, NSData *data, NSError *error) {
// do some stuff here on completion
// ...
dispatch_semaphore_signal(dsema); // it's important to call this function in both error and success cases of download to free the block from queue
}];
dispatch_semaphore_wait(dsema, DISPATCH_TIME_FOREVER); // or another dispatch_time if you want your custom timeout instead of AWS
}];
}
Effectively your answer is https://stackoverflow.com/a/4326754/2392973
You just schedule plenty of such blocks to your operation queue.
More reading
https://developer.apple.com/reference/dispatch

Using NSUrlConnection inside either NSThread or NSOperation

I am developing a static library that needs to do some stuff in the background, without interacting with the main thread. To give you an idea, think of just logging some user events. The library must keep doing this stuff until the user exits the app or sends it to the background (pushes the home button) - in other words it needs to keep doing stuff inside a loop.
The only interaction between the main app thread and the spawned thread is that occasionally the main app thread will put some stuff (an event object) into a queue that the spawned thread can read/consume. Other than that, the spawned thread just keeps going until the app exists or backgrounds.
Part of what the spawned thread needs to do (though not all of it) involves sending data to an HTTP server. I would have thought that it would be easy to subclass NSThread, override its main method, and just make a synchronous call to NSUrlConnection with some sort of timeout on that connection so the thread doesn't hang forever. For example, in Java/Android, we just subclass Thread, override the start() method and call a synchronous HTTP GET method (say from Apache's HttpClient class). This is very easy and works fine. But from what I have seen here and elsewhere, apparently on iOS it is much more complicated than this and I'm more than a bit confused as to what the best approach is that actually works.
So should I subclass NSThread and somehow use NSUrlConnection? It seems the asynchronous NSUrlConnection does not work inside NSThread because delegate methods don't get called but what about the synchronous method? Do I somehow need to use and configure the RunLoop and set up an autorelease pool? Or should I use an NSOperation? It seems to me that what I am trying to do is pretty common - does anyone have a working example of how to do this properly?
As I understand it, to use NSURLConnection asynchronously you need a runloop. Even if you use an NSOperation you still need a runloop.
All the examples I have seen use the Main Thread to start NSURLConnection which has a runloop. The examples using NSOperation are set up so the operation is Concurrent which tells NSOperationQueue not to provide it's own thread, they then make sure that NSURLConnection is started on the main thread, for example via a call to performSelectorOnMainThread:
Here is an example:
Pulse Engineering Blog: Concurrent Downloads using NSOperationQueues
You can also search the Apple documentation for QRunLoopOperation in the LinkedImageFetcher sample which is an example class showing some ins and outs of this kind of thing.
(Although I'm not sure I actually saw any code that example showing how to run your own runloop, again this example relies on the main thread.)
I've used the grand central dispatch (GCD) methods to achieve this. Here is an example that worked for me in a simple test app (I'm not sure if it applies in a static library, but may be worth a look). I'm using ARC.
In the example, I am kicking off some background work from my viewDidLoad method, but you can kick it off from anywhere. The key is that "dispatch_async(dispatch_get_global_queue…" runs the block in a background thread. See this answer for a good explanation of that method: https://stackoverflow.com/a/12693409/215821
Here is my viewDidLoad:
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL),
^(void) {
[self doStuffInBackground];
});
}
The doStuffInBackground method is running in the background at this point, so you can just use NSURLConnection synchronously. In my example here, the method loops making network calls until presumably some other code sets backgroundStuffShouldRun = false. A network call is made with a 10 second timeout. After the call, I'm updating a UI label just to show progress. Note that the UI update is performed with "dispatch_async(dispatch_get_main_queue()…". This runs the UI update on the UI thread, as required.
One potential issue with this background work: there isn't a way to cancel the http request itself. But, with a 10 second timeout, you'd be waiting a max of 10 seconds for the thread to abort itself after an outsider (likely some event in your UI) sets backgroundStuffShouldRun = false.
- (void)doStuffInBackground
{
while (backgroundStuffShouldRun) {
// prepare for network call...
NSURL* url = [[NSURL alloc] initWithString:#"http://maps.google.com/maps/geo"];
// set a 10 second timeout on the request
NSURLRequest* request = [[NSURLRequest alloc] initWithURL:url cachePolicy:NSURLCacheStorageAllowed timeoutInterval:10];
NSError* error = nil;
NSURLResponse *response = nil;
// make the request
NSData* data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
// were we asked to stop the background processing?
if (!backgroundStuffShouldRun) {
return;
}
// process response...
NSString* status = #"Success";
if (error) {
if (error.code == NSURLErrorTimedOut) {
// handle timeout...
status = #"Timed out";
}
else {
// handle other errors...
status = #"Other error";
}
}
else {
// success, handle the response body
NSString *dataAsString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"%#", dataAsString);
}
// update the UI with our status
dispatch_async(dispatch_get_main_queue(), ^{
[statusLabel setText:[NSString stringWithFormat:#"completed network call %d, status = %#", callCount, status]];
});
callCount++;
sleep(1); // 1 second breather. not necessary, but good idea for testing
}
}

Stopping an NSOperationQueue

I have an NSOperationQueue that handles importing data from a web server on a loop. It accomplishes this with the following design.
NSURLConnect is wrapped in an NSOperation and added to the Queue
On successful completion of the download (using a block), the data from the request is wrapped in another NSOperation that adds the relevant data to Core Data. This operation is added to the queue.
On successful completion (using another block), (and after a specified delay) I call the method that started it all and return to step 1. Thus, i make another server call x seconds later.
This works great. I'm able to get data from the server and handle everything on the background. And because these are just NSOperations I'm able to put everything in the background, and perform multiple requests at a time. This works really well.
The ONLY problem that I currently have is that I'm unable to successfully cancel the operations once they are going.
I've tried something like the following :
- (void)flushQueue
{
self.isFlushingQueue = YES;
[self.operationQueue cancelAllOperations];
[self.operationQueue waitUntilAllOperationsAreFinished];
self.isFlushingQueue = NO;
NSLog(#"successfully flushed Queue");
}
where self.isFlushingQueue is a BOOL that I use to check before adding any new operations to the queue. This seems like it should work, but in fact it does not. Any ideas on stopping my Frankenstein creation?
Edit (Solved problem, but from a different perspective)
I'm still baffled about why exactly I was unable to cancel these operations (i'd be happy to keep trying possible solutions), but I had a moment of insight on how to solve this problem in a slightly different way. Instead of dealing at all with canceling operations, and waiting til queue is finished, I decided to just have a data structure (NSMutableDictionary) that had a list of all active connections. Something like this :
self.activeConnections = [NSMutableDictionary dictionaryWithDictionary:#{
#"UpdateContacts": #YES,
#"UpdateGroups" : #YES}];
And then before I add any operation to the queue, I simply ask if that particular call is On or Off. I've tested this, and I successfully have finite control over each individual server request that I want to be looping. To turn everything off I can just set all connections to #NO.
There are a couple downsides to this solution (Have to manually manage an additional data structure, and every operation has to start again to see if it's on or off before it terminates).
Edit -- In pursuit of a more accurate solution
I stripped out all code that isn't relevant (notice there is no error handling). I posted two methods. The first is an example of how the request NSOperation is created, and the second is the convenience method for generating the completion block.
Note the completion block generator is called by dozens of different requests similar to the first method.
- (void)updateContactsWithOptions:(NSDictionary*)options
{
//Hard coded for ease of understanding
NSString *contactsURL = #"api/url";
NSDictionary *params = #{#"sortBy" : #"LastName"};
NSMutableURLRequest *request = [self createRequestUsingURLString:contactsURL andParameters:params];
ConnectionCompleteBlock processBlock = [self blockForImportingDataToEntity:#"Contact"
usingSelector:#selector(updateContactsWithOptions:)
withOptions:options andParsingSelector:#selector(requestUsesRowsFromData:)];
BBYConnectionOperation *op = [[BBYConnectionOperation alloc] initWithURLRequest:request
andDelegate:self
andCompletionBlock:processBlock];
//This used to check using self.isFlushingQueue
if ([[self.activeConnections objectForKey:#"UpdateContacts"] isEqualToNumber:#YES]){
[self.operationQueue addOperation:op];
}
}
- (ConnectionCompleteBlock) blockForImportingDataToEntity:(NSString*)entityName usingSelector:(SEL)loopSelector withOptions:(NSDictionary*)options andParsingSelector:(SEL)parseSelector
{
return ^(BOOL success, NSData *connectionData, NSError *error){
//Pull out variables from options
BOOL doesLoop = [[options valueForKey:#"doesLoop"] boolValue];
NSTimeInterval timeInterval = [[options valueForKey:#"interval"] integerValue];
//Data processed before importing to core data
NSData *dataToImport = [self performSelector:parseSelector withObject:connectionData];
BBYImportToCoreDataOperation *importOperation = [[BBYImportToCoreDataOperation alloc] initWithData:dataToImport
andContext:self.managedObjectContext
andNameOfEntityToImport:entityName];
[importOperation setCompletionBlock:^ (BOOL success, NSError *error){
if(success){
NSLog(#"Import %#s was successful",entityName);
if(doesLoop == YES){
dispatch_async(dispatch_get_main_queue(), ^{
[self performSelector:loopSelector withObject:options afterDelay:timeInterval];
});
}
}
}];
[self.operationQueue addOperation:importOperation];
};
}
Cancellation of an NSOperation is just a request, a flag that is set in NSOperation. It's up to your NSOperation subclass to actually action that request and cancel it's work. You then need to ensure you have set the correct flags for isExecuting and isFinished etc. You will also need to do this in a KVO compliant manner. Only once these flags are set is the operation finished.
There is an example in the documentation Concurrency Programming Guide -> Configuring Operations for Concurrent Execution. Although I understand that this example may not correctly account for all multi-threaded edge cases. Another more complex example is provided in the sample code LinkedImageFetcher : QRunLoopOperation
If you think you are responding to the cancellation request correctly then you really need to post your NSOperation subclass code to examine the problem any further.
Instead of using your own flag for when it is ok to add more operations, you could try the
- (void)setSuspended:(BOOL)suspend
method on NSOperationQueue? And before adding a new operation, check if the queue is suspended with isSuspended?

Resources