Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recursive / Iterative NSURLSessionDataTask causing memory leak

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):

Image showing instruments reporting memory 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.

like image 241
Sammio2 Avatar asked Sep 08 '14 10:09

Sammio2


People also ask

Can recursion cause memory leak?

Now that we know the cause, let's see how we can solve this problem. From the previous example, we can see that recursive calls in co or async functions may delay the memory deallocation. This delay leads to memory pileups and memory pressure.

What is the main cause of memory leaks?

DEFINITION A memory leak is the gradual deterioration of system performance that occurs over time as the result of the fragmentation of a computer's RAM due to poorly designed or programmed applications that fail to free up memory segments when they are no longer needed.

What causes memory leaks Swift?

Memory leaks in Swift are often the product of a retain cycle, when one object will hold a strong reference to an object that also strongly references the original object. So A retains B and B retains A . These kinds of issues can sometimes be tricky to debug and lead to hard to reproduce crashes.

What is a memory leak and how do you prevent it?

Memory leak occurs when programmers create a memory in heap and forget to delete it. The consequences of memory leak is that it reduces the performance of the computer by reducing the amount of available memory.


1 Answers

- 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.

like image 181
serge-k Avatar answered Oct 05 '22 23:10

serge-k