NSURLSession Memory Leaks occur when using web services in IOS - ios

I am building an app that uses a web service and to get information from that web service I use NSURLSession and NSURLSessionDataTask.
I am now in the memory testing phase and I have found that NSURLSession is causing memory leaks.
This is not all of the leaks. It is all that I could fit in the picture.
Below is how I setup the NSURLSession and request the information from the web service.
#pragma mark - Getter Methods
- (NSURLSessionConfiguration *)sessionConfiguration
{
if (_sessionConfiguration == nil)
{
_sessionConfiguration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
[_sessionConfiguration setHTTPAdditionalHeaders:#{#"Accept": #"application/json"}];
_sessionConfiguration.timeoutIntervalForRequest = 60.0;
_sessionConfiguration.timeoutIntervalForResource = 120.0;
_sessionConfiguration.HTTPMaximumConnectionsPerHost = 1;
}
return _sessionConfiguration;
}
- (NSURLSession *)session
{
if (_session == nil)
{
_session = [NSURLSession
sessionWithConfiguration:self.sessionConfiguration
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
}
return _session;
}
#pragma mark -
#pragma mark - Data Task
- (void)photoDataTaskWithRequest:(NSURLRequest *)theRequest
{
#ifdef DEBUG
NSLog(#"[GPPhotoRequest] Photo Request Data Task Set");
#endif
// Remove existing data, if any
if (_photoData)
{
[self setPhotoData:nil];
}
self.photoDataTask = [self.session dataTaskWithRequest:theRequest];
[self.photoDataTask resume];
}
#pragma mark -
#pragma mark - Session
- (void)beginPhotoRequestWithReference:(NSString *)aReference
{
#ifdef DEBUG
NSLog(#"[GPPhotoRequest] Fetching Photo Data...");
#endif
_photoReference = aReference;
NSString * serviceURLString = [[NSString alloc] initWithFormat:#"%#/json?photoreference=%#", PhotoRequestBaseAPIURL, self.photoReference];
NSString * encodedServiceURLString = [serviceURLString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
serviceURLString = nil;
NSURL * serviceURL = [[NSURL alloc] initWithString:encodedServiceURLString];
encodedServiceURLString = nil;
NSURLRequest * request = [[NSURLRequest alloc] initWithURL:serviceURL];
[self photoDataTaskWithRequest:request];
serviceURL = nil;
request = nil;
}
- (void)cleanupSession
{
#ifdef DEBUG
NSLog(#"[GPPhotoRequest] Session Cleaned Up");
#endif
[self setPhotoData:nil];
[self setPhotoDataTask:nil];
[self setSession:nil];
}
- (void)endSessionAndCancelTasks
{
if (_session)
{
#ifdef DEBUG
NSLog(#"[GPPhotoRequest] Session Ended and Tasks Cancelled");
#endif
[self.session invalidateAndCancel];
[self cleanupSession];
}
}
#pragma mark -
#pragma mark - NSURLSession Delegate Methods
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
#ifdef DEBUG
NSLog(#"[GPPhotoRequest] Session Completed");
#endif
if (error)
{
#ifdef DEBUG
NSLog(#"[GPPhotoRequest] Photo Request Fetch: %#", [error description]);
#endif
[self endSessionAndCancelTasks];
switch (error.code)
{
case NSURLErrorTimedOut:
{
// Post notification
[[NSNotificationCenter defaultCenter] postNotificationName:#"RequestTimedOut" object:self];
}
break;
case NSURLErrorCancelled:
{
// Post notification
[[NSNotificationCenter defaultCenter] postNotificationName:#"RequestCancelled" object:self];
}
break;
case NSURLErrorNotConnectedToInternet:
{
// Post notification
[[NSNotificationCenter defaultCenter] postNotificationName:#"NotConnectedToInternet" object:self];
}
break;
case NSURLErrorNetworkConnectionLost:
{
// Post notification
[[NSNotificationCenter defaultCenter] postNotificationName:#"NetworkConnectionLost" object:self];
}
break;
default:
{
}
break;
}
}
else {
if ([task isEqual:_photoDataTask])
{
[self parseData:self.photoData fromTask:task];
}
}
}
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error
{
if (error)
{
#ifdef DEBUG
NSLog(#"[GPPhotoRequest] Session Invalidation: %#", [error description]);
#endif
}
if ([session isEqual:_session])
{
[self endSessionAndCancelTasks];
}
}
#pragma mark -
#pragma mark - NSURLSessionDataTask Delegate Methods
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
#ifdef DEBUG
NSLog(#"[GPPhotoRequest] Received Data");
#endif
if ([dataTask isEqual:_photoDataTask])
{
[self.photoData appendData:data];
}
}
#pragma mark -
Question:
Why is NSURLSession causing these memory leaks? I am invalidating the NSURLSession when I am finished with it. I am also releasing any properties that I do not need and setting the session to nil (refer to - (void)cleanupSession & - (void) endSessionAndCancelTasks).
Other Information:
The memory leaks occur after the session has completed and "cleaned up". Sometimes, they also occur after I have popped the UIViewController. But, all of my custom (GPPhotoRequest and GPSearch) objects and UIViewController are being dealloced (I've made sure by adding an NSLog).
I tried not to post to much code, but I felt like most of it needed to be seen.
Please let me know if you need any more information. Help is greatly appreciated.

I had this same "leaky", memory management issue when I switched to NSURLSession. For me, after creating a session and resuming/starting a dataTask, I was never telling the session to clean itself up (i.e. release the memory allocated to it).
// Ending my request method with only the following line causes memory leaks
[dataTask resume];
// Adding this line fixed my memory management issues
[session finishTasksAndInvalidate];
From the docs:
After the last task finishes and the session makes the last delegate call, references to the delegate and callback objects are broken.
Cleaning up my sessions fixed the memory leaks being reported via Instruments.

After rereading the URL Loading System Programming Guide it turns that I was setting the NSURLSession property to nil too early.
Instead, I need to set the NSURLSession property to nil AFTER I receive the delegate message URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error, which makes sense. Luckily, it was a minor mistake.
E.g.
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error
{
if (error)
{
#ifdef DEBUG
NSLog(#"[GPPhotoRequest] Session Invalidation: %#", [error description]);
#endif
}
if ([session isEqual:_session])
{
[self cleanupSession];
}
}

Had the same issue. The #Jonathan's answer didn't make a sense - my app still leaked memory. I found out that setting the session property to nil in URLSession:didBecomeInvalidWithError: delegate method is causing the issue. Tried to look deeper into the URL Loading System Programming Guide. It says
After invalidating the session, when all outstanding tasks have been canceled or have finished, the session sends the delegate a URLSession:didBecomeInvalidWithError: message. When that delegate method returns, the session disposes of its strong reference to the delegate.
I left the delegate method blank. But the invalidated session property still have a pointer, when should I set it nil? I just set this property weak
// .h-file
#interface MyClass : NSObject <NSURLSessionDelegate>
{
__weak NSURLSession *_session;
}
// .m-file
- (NSURLSessionTask *)taskForRequest:(NSURLRequest *)request withCompletionHandler:(void(^)(NSData *,NSURLResponse *,NSError *))handler
{
if(!_session)
[self initSession];
//...
}
The app stopped leaking memory.

Please see my answer here: https://stackoverflow.com/a/53428913/4437636
I believe this leak is the same one I was seeing, and only happens when running network traffic through a proxy. My code was fine, but it turned out that an internal bug in the Apple API was causing the leak.

Related

Pass a delegate as a parameter for NSURLSession

I have a class named RestService which I use all over my app to perform several synchronous requests to a web service. I added a new method to this class to perform an asynchronous request which again I want to reuse all over my app. This is the code for that new method:
- (void)backgroundExecutionOfService:(NSString *)serviceName
withParameters:(NSDictionary *)parameters
inView:(UIView *)view
withDelegate:(UIViewController *)delegate
{
NSString *serviceUrl = #"http://MyWebServer/public/api/clients/5";
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.allowsCellularAccess = YES;
sessionConfig.timeoutIntervalForRequest = 10;
sessionConfig.timeoutIntervalForResource = 10;
sessionConfig.HTTPMaximumConnectionsPerHost = 1;
NSURLSession *session;
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:delegate
delegateQueue:nil];
NSURLSessionDownloadTask *getFileTask;
getFileTask = [session downloadTaskWithURL:[NSURL URLWithString:serviceUrl]];
[getFileTask resume];
}
But XCode is giving me a warning about using that parameter as a delegate (Sending UIViewController * __strong' to parameter of incompatible type 'id< NSURLSessionDelegate > _Nullable'). I made sure that the view controller I'm sending as a parameter has declared < NSURLSessionDelegate > in .h and I created the delegate methods in the ViewControllers's implementation file.
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error{
NSLog(#"Became invalid with error: %#", [error localizedDescription]);
}
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler{
NSLog(#"Received challenge");
}
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session{
NSLog(#"Did finish events for session: %#", session);
}
The app doesn't crash but the delegate methods are never called. The synchronous web services work as expected.
That happened because a UIViewController class don't conform a NSURLSessionDelegate protocol.
To resolve that discrepancy just change the method's signature to this like:
- (void)backgroundExecutionOfService:(NSString *)serviceName withParameters:(NSDictionary *)parameters inView:(UIView *)view withDelegate:(id<NSURLSessionDelegate>)delegate{
//... your code
}
And "read up on the basics of delegates."

How to use iOS delegate and callback methods from NMSSH library?

I'm trying to use delegate methods from NMSSH library in iOS but could not get it working. Let's take an example.
CustomViewController.h
#import <UIKit/UIKit.h>
#import <NMSSH/NMSSH.h>
#interface CustomViewController : UIViewController<NMSSHSessionDelegate, NMSSHChannelDelegate>
- (IBAction)connectButton:(UIButton *)sender;
#end
CustomViewController.m
#import "CustomViewController.h"
#implementation CustomViewController
-(void)viewDidLoad{
[super viewDidLoad];
}
- (IBAction)connectButton:(UIButton *)sender {
[self serverConnect:#"10.0.0.1"];
}
-(void)serverConnect:(NSString *)address{
NMSSHSession *session = [NMSSHSession connectToHost:address withUsername:#"username"];
NMSSHChannel *myChannel = [[NMSSHChannel alloc]init];
if (session.isConnected) {
[session authenticateByPassword:#"password"];
if (session.isAuthorized) {
NSLog(#"Authentication succeeded");
[session setDelegate:self];
[myChannel setDelegate:self];
}
}
NSError *error = nil;
//session.channel.requestPty = YES; (tried and later ignored)
NSString *response = [session.channel execute:#"mkdir" error:&error];
NSLog(#"Response from device: %#", response);
}
- (void)session:(NMSSHSession *)session didDisconnectWithError:(NSError *)error{
NSLog(#"log if session disconnects...Delegate method");
}
- (void)channel:(NMSSHChannel *)channel didReadError:(NSString *)error{
NSLog(#"Error received...Delegate method");
}
- (void)channel:(NMSSHChannel *)channel didReadRawData:(NSData *)data{
NSLog(#"Read Raw Data...Delegate method");
}
Connection to the server, sending a single line command and acknowledgement back from the server in Console is OK.
I have decent idea how to pass values from one View Controller to another using delegate (went through few tutorials with practical implementation).
With the same knowledge I am attempting to get response from delegate methods parts of NMSSH library but it's driving me round and round. I've found http://cocoadocs.org/docsets/NMSSH/2.2.1/ pretty nice API of this library but with my limited knowledge of iOS, I'm bit stuck.
Please help me.
My search finally came to an end with NMSSH AsyncAPI (branch) which supports multithreading.

Why doesn't NSOperationQueue execute a block submitted with addOperationWithBlock?

Here is the thing, I got some code, that does not execute (the compiler run the code but does not do anything)... Here is the code... Using NSURLSessionDelegate and [NSOperationQueue mainQueue] addOperationWithBlock
#interface tablaPosViewController : UIViewController <NSURLSessionDelegate>
#end
#implementation tablaPosViewController ()
- (void)viewDidLoad
{
//some code to set the view, labels and stuff
[self downloadTheHTMLdata] // this code download some data from WWW
}
- (void)downloadTheHMTLdata
{
//some code to set the session using an object
[object.downloadTask resume]; //Begins the download
}
- (void)toFixTablaPosiciones
{
//some code to do with the downloaded data from WWW (and HTML sheet)
//here, parse the HTML Sheet, and put some data into an Arrays, and another public vars
//call another method
[self PutTheDataInLabel];
}
- (void)PutTheDataInLabel
{
//some code to put all the data in every label
//take the public vars that was set in the previous method, and do some code with it
//and put the info into the labels
//call another method
[self MoreStuff];
}
- (void)MoreStuff
{
//some code..
}
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
//When the download finish this method fire up!
//this is to copy file from tmp folder to Docs folder
if ([fileManager fileExistsAtPath:[destinationURL path]])
{
[fileManager removeItemAtURL:destinationURL error:nil];
}
BOOL success = [fileManager copyItemAtURL:location //TMP download folder
toURL:destinationURL //Documents folder
error:&error];
//HERES COMES THE TROUBLE!!!
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self toFixTablaPosiciones]; //Call this method, that has other method calls!
}];
}
#end
UPDATE
This another code put methods in the queue...
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session{
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
if ([downloadTasks count] == 0) {
if (appDelegate.backgroundTransferCompletionHandler != nil) {
void(^completionHandler)() = appDelegate.backgroundTransferCompletionHandler;
appDelegate.backgroundTransferCompletionHandler = nil;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
completionHandler();
}];
}
}
}];
}
The issue is when the download file ends, calls -(void)URLSession:(NSURLSession *)session downloadTask:... method and I wait to [[NSOperationQueue mainQueue] addOperationWithBlock:^{... runs everything... but it does not execute anythig of [self toFixTablaPosiciones]!!.
I ran the code step by step, and I see how the compiler runs all the code, method over method... but the view never updates, runs but not executes, simply does do anything, and I have an Activity Indicator, and want to stop it, but the lablels stills with de dummie data and the Activity Indicator never stops and never disappears.
In a previous view, I download another file using a similiar class, and downloads very quickly. Going to this view/class try to perform the download and this is the thing...
Hoping any body can help me and send me any advice. Thanks!
One technique I use, as do most experienced developers, is to use asserts and also NSLog (which you can comment on and off) to verify that assumptions you are making about your code are in fact true. Try cutting and pasting the below code and see what happens - it should help. In the future, don't bang your head on a wall - start adding asserts and logs. At some point you will find that some assumption is untrue.
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
NSLog(#"Received URLSessionDidFinishEventsForBackgroundURLSession: application state is %d", [UIApplication sharedApplication] applicationState];
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
NSLog(#"Download task count %d", [downloadTasks count]);
#warning "This looks like incorrect logic, but I don't know this background code. Wouldn't you see "1", or more? Won't you get an array of the tasks you submitted???
if ([downloadTasks count] == 0) {
assert(appDelegate.backgroundTransferCompletionHandler); // if this is nil, your app will be stuck in some odd state forever, so treat this like a fatal error
void(^completionHandler)() = appDelegate.backgroundTransferCompletionHandler;
appDelegate.backgroundTransferCompletionHandler = nil;
assert([NSOperationQueue mainQueue]); // why not?
assert(![NSOperationQueue mainQueue].isSuspended); // I looked at the class description, the queue **could** be suspended
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(#"Completion Operation called!");
assert(completionHandler); // perhaps an ARC bug, unlikely, but then you cannot get this code to work, right?
completionHandler();
}];
}
}];
}

download task is null for the first request when when in backgroundfetch IOS

I was wondering if you have seen this or might have some ideas as to why I see the following behavior in my code: I have an NSURLsession with background config. I initiate periodic download task when the program runs in the Foreground, and everything works. WhenI simulate backgroundfetch (in xcode), my task gets a null value (eventhough the request and the session are not null). of course in this case, my session delegate never gets fired to do completionhandler. if I simulate subsequent background fetches, they all work afterward. at this point, if I bring the app to the foreground in the simulator, and I simulate another backgroundfetch, the symptoms star all over. I am using this code in my appdelegate class.
your help is greatly appreciated.
- (NSURLSession *)FlickrSession
{
if(!_FlickrSession)
{
NSLog(#"setting new FlickrSession");
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:FLICKR_SESSION];
configuration.allowsCellularAccess = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_FlickrSession = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
_FlickrSession.sessionDescription = FLICKR_SESSION;
NSLog(#" new self is %#", _FlickrSession);
NSLog(#"queue in session %#", dispatch_get_current_queue());
});
}
return _FlickrSession;
}
-(void) startFlickrFetch
{
// initialize session config and the background session
[self.FlickrSession getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks)
{
if(![downloadTasks count])
{
NSLog(#"new downloadtask session %#", self.FlickrSession.sessionDescription);
NSURLRequest *request = [NSURLRequest requestWithURL:[FlickrFetcher URLforRecentGeoreferencedPhotos]];
// NSURLSessionDownloadTask *task = [self.FlickrSession downloadTaskWithURL:[FlickrFetcher URLforRecentGeoreferencedPhotos]];
NSURLSessionDownloadTask *task = [self.FlickrSession downloadTaskWithRequest:request];
task.taskDescription = FLICKR_DOWNLOAD_TASK;
NSLog(#"new request %#", request);
NSLog(#"new downloadtask %#", task);
[task resume];
//task?[task resume]:[self fireBackgroungFetchCompletionHandler];;
//[self fireBackgroungFetchCompletionHandler];
NSLog(#"queue in task %#", dispatch_get_current_queue());
}
else
{
NSLog(#"resuming old downloadtask %d", [downloadTasks count]);
for(NSURLSessionDownloadTask *task in dataTasks) [task resume];
}
}];
NSLog(#"queue outside the block %#", dispatch_get_current_queue());
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions: (NSDictionary *)launchOptions
{
// Override point for customization after application launch.
// open the file, if there is no managedContext. this is the case wjere the application was launched directly by user
// it did not come from the bckground state;
//need to enable background fetch
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
//[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(documentChangedState) name:UIDocumentStateChangedNotification object: self.document];
NSLog(#"in application didfinishlaunching");
[self openDatabaseFile];
[self startFlickrFetch];
return YES;
}
-(void) application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
self.backgroundFetchCompletionHandler = completionHandler;
if(self.document.documentState == UIDocumentStateNormal)
{
//[self openDatabaseFile];
NSLog(#"in performFetchWithCompletionHandler");
[self startFlickrFetch];
}
}
- (void) application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
{
self.completionhandler = completionHandler;
NSLog(#"handle event for backgroundURLsession***********");
}
This could have something to do with a strange interaction of the background fetch and a background session, in which case I have no advice.
However, in the background, iOS doesn't know to wait for async calls, e.g. getTasksWithCompletionHandler. You can solve this by wrapping those calls with a UIBackgroundTaskIdentifier†-based [UIApplication] begin/end task (in this case, with the "end (app) task" inside the "get (session) tasks completion handler" block).
But if all you need is a count, here's what I did, which I think is simpler:
Create an ivar:
NSMutableSet *activeTaskIDs;
When you create a task, add it to the set:
[activeTaskIDs addObject:#(task.taskIdentifier)];
When the task completes, remove it.
You can get your count from there, no async.
† Confusingly, a different kind of task. I differentiate with the terms "app tasks" vs. "session tasks".

Run multiple instances of NSOperation with NSURLConnection?

We have a large project that needs to sync large files from a server into a 'Library' in the background. I read subclassing NSOperation is the most flexible way of multithreading iOS tasks, and attempted that. So the function receives a list of URLs to download & save, initialises an instance of the same NSOperation class and adds each to an NSOperation queue (which should download only 1 file at a time).
-(void) LibSyncOperation {
// Initialize download list. Download the homepage of some popular websites
downloadArray = [[NSArray alloc] initWithObjects:#"www.google.com",
#"www.stackoverflow.com",
#"www.reddit.com",
#"www.facebook.com", nil];
operationQueue = [[[NSOperationQueue alloc]init]autorelease];
[operationQueue setMaxConcurrentOperationCount:1]; // Only download 1 file at a time
[operationQueue waitUntilAllOperationsAreFinished];
for (int i = 0; i < [downloadArray count]; i++) {
LibSyncOperation *libSyncOperation = [[[LibSyncOperation alloc] initWithURL:[downloadArray objectAtIndex:i]]autorelease];
[operationQueue addOperation:libSyncOperation];
}
}
Now, those class instances all get created fine, and are all added to the NSOperationQueue and begin executing. BUT the issue is when it's time to start downloading, the first file never begins downloading (using an NSURLConnection with delegate methods). I've used the runLoop trick I saw in another thread which should allow the operation to keep running until the download is finished. The NSURLConnection is established, but it never starts appending data to the NSMutableData object!
#synthesize downloadURL, downloadData, downloadPath;
#synthesize downloadDone, executing, finished;
/* Function to initialize the NSOperation with the URL to download */
- (id)initWithURL:(NSString *)downloadString {
if (![super init]) return nil;
// Construct the URL to be downloaded
downloadURL = [[[NSURL alloc]initWithString:downloadString]autorelease];
downloadData = [[[NSMutableData alloc] init] autorelease];
NSLog(#"downloadURL: %#",[downloadURL path]);
// Create the download path
downloadPath = [NSString stringWithFormat:#"%#.txt",downloadString];
return self;
}
-(void)dealloc {
[super dealloc];
}
-(void)main {
// Create ARC pool instance for this thread.
// NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init]; //--> COMMENTED OUT, MAY BE PART OF ISSUE
if (![self isCancelled]) {
[self willChangeValueForKey:#"isExecuting"];
executing = YES;
NSURLRequest *downloadRequest = [NSURLRequest requestWithURL:downloadURL];
NSLog(#"%s: downloadRequest: %#",__FUNCTION__,downloadURL);
NSURLConnection *downloadConnection = [[NSURLConnection alloc] initWithRequest:downloadRequest delegate:self startImmediately:NO];
// This block SHOULD keep the NSOperation from releasing before the download has been finished
if (downloadConnection) {
NSLog(#"connection established!");
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
} while (!downloadDone);
} else {
NSLog(#"couldn't establish connection for: %#", downloadURL);
// Cleanup Operation so next one (if any) can run
[self terminateOperation];
}
}
else { // Operation has been cancelled, clean up
[self terminateOperation];
}
// Release the ARC pool to clean out this thread
//[pool release]; //--> COMMENTED OUT, MAY BE PART OF ISSUE
}
#pragma mark -
#pragma mark NSURLConnection Delegate methods
// NSURLConnectionDelegate method: handle the initial connection
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSHTTPURLResponse*)response {
NSLog(#"%s: Received response!", __FUNCTION__);
}
// NSURLConnectionDelegate method: handle data being received during connection
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[downloadData appendData:data];
NSLog(#"downloaded %d bytes", [data length]);
}
// NSURLConnectionDelegate method: What to do once request is completed
-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(#"%s: Download finished! File: %#", __FUNCTION__, downloadURL);
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *docDir = [paths objectAtIndex:0];
NSString *targetPath = [docDir stringByAppendingPathComponent:downloadPath];
BOOL isDir;
// If target folder path doesn't exist, create it
if (![fileManager fileExistsAtPath:[targetPath stringByDeletingLastPathComponent] isDirectory:&isDir]) {
NSError *makeDirError = nil;
[fileManager createDirectoryAtPath:[targetPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:&makeDirError];
if (makeDirError != nil) {
NSLog(#"MAKE DIR ERROR: %#", [makeDirError description]);
[self terminateOperation];
}
}
NSError *saveError = nil;
//NSLog(#"downloadData: %#",downloadData);
[downloadData writeToFile:targetPath options:NSDataWritingAtomic error:&saveError];
if (saveError != nil) {
NSLog(#"Download save failed! Error: %#", [saveError description]);
[self terminateOperation];
}
else {
NSLog(#"file has been saved!: %#", targetPath);
}
downloadDone = true;
}
// NSURLConnectionDelegate method: Handle the connection failing
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(#"%s: File download failed! Error: %#", __FUNCTION__, [error description]);
[self terminateOperation];
}
// Function to clean up the variables and mark Operation as finished
-(void) terminateOperation {
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"];
finished = YES;
executing = NO;
downloadDone = YES;
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
#pragma mark -
#pragma mark NSOperation state Delegate methods
// NSOperation state methods
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isExecuting {
return executing;
}
- (BOOL)isFinished {
return finished;
}
NOTE: If that was too unreadable, I set up a QUICK GITHUB PROJECT HERE you can look through. Please note I'm not expecting anyone to do my work for me, simply looking for an answer to my problem!
I suspect it has something to do with retaining/releasing class variables, but I can't be sure of that since I thought instantiating a class would give each instance its own set of class variables. I've tried everything and I can't find the answer, any help/suggestions would be much appreciated!
UPDATE: As per my answer below, I solved this problem a while ago and updated the GitHub project with the working code. Hopefully if you've come here looking for the same thing it helps!
In the interests of good community practice and helping anyone else who might end up here with the same problem, I did end up solving this issue and have updated the GitHub sample project here that now works correctly, even for multiple concurrent NSOperations!
It's best to look through the GitHub code since I made a large amount of changes, but the key fix I had to make to get it working was:
[downloadConnection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
This is called after the NSURLConnection is initialized, and just before it is started. It attaches the execution of the connection to the current main run loop so that the NSOperation won't prematurely terminate before the download is finished. I'd love to give credit to wherever first posted this clever fix, but it's been so long I've forgotten where, apologies. Hope this helps someone!

Resources