3

Even after invalidate a NSURLSession, running a profile using Instruments, some classes (probably privates) called TubeManager, HTTPConnectionCache and HTTPConnectionCacheDictionary still alive in memory.

Code snippet to reproduce:

NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession* session = [NSURLSession sessionWithConfiguration:config];
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.google.com"]];
NSURLSessionDataTask* sessionDataTask = [session dataTaskWithRequest:request
                                               completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
    [session finishTasksAndInvalidate];
}];
[sessionDataTask resume];

Instruments screenshot

Victor Barros
  • 39
  • 1
  • 3
  • I'm not sure how you're measuring the memory growth, but if I look at total allocations, do 100 downloads, and check total allocations again, and repeat that five times, I'm seeing peak allocations leveling off. And if I do a memory warning, even some of that is recovered. Now, if I look at allocations changes over a range or use "generations", it looks like it's growing, but net-net, it levels off. Also, make sure you have zombies turned _off._ – Rob May 08 '15 at 20:03
  • Have you tried run Instruments and look for the classes that I mentioned? – Victor Barros May 11 '15 at 17:45
  • Yes, but they do not continue to grow (i.e. my app keeps falling back to a steady state). – Rob May 11 '15 at 18:22
  • Please take a look in this screenshot: https://www.evernote.com/shard/s70/sh/14637ee5-11ea-4744-889d-56ca50b647da/3a0e9e1c7e59e516d21f27b3da7d04da Are you not getting a similar result? Which version of iOS are you running? – Victor Barros May 11 '15 at 18:32
  • iOS 8.3.1. I notice that I have only two instances each of those three objects. Coincidentally, I only instantiated two sessions. You're not instantiating new sessions for each of your requests, are you? – Rob May 11 '15 at 18:41
  • Yes, in the sample project that I've created to reproduce the problem I'm creating a new session for each request, but I'm invalidating the session once the request has finished. And this is just to reproduce the problem. In a real situation, I don't need to create a new session every time, but there's no way to keep the memory consumption low if I need to create a bunch of them and release when I don't need anymore. – Victor Barros May 11 '15 at 18:47
  • No, I don't think you can do anything to mitigate the memory behavior. I think the solution is to design approach that mitigates the need for hundreds/thousands of session objects. But if you want to bring this to Apple's attention, feel free to post to http://bugreport.apple.com. – Rob May 11 '15 at 19:07
  • See my answer below about how to avoid lots of session objects @VictorBarros – CommaToast Mar 05 '16 at 20:08
  • Possible duplicate of [NSURLSession HTTP/2 memory leak](https://stackoverflow.com/questions/39409357/nsurlsession-http-2-memory-leak) – Tomasz Czyżak Oct 08 '17 at 05:56

3 Answers3

1

finishTasksAndInvalidate called in wrong place... completionHandler is for handling response it has nothing to do with session

Here is correct code:

NSURLSessionConfiguration* config = [NSURLSessionConfigurationdefaultSessionConfiguration];
NSURLSession* session = [NSURLSession sessionWithConfiguration:config];
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.google.com"]];
NSURLSessionDataTask* sessionDataTask = [session dataTaskWithRequest:request
                                           completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
   // handle response...
}];
[sessionDataTask resume];
[session finishTasksAndInvalidate];
Tomasz Czyżak
  • 1,118
  • 12
  • 13
0

So, what is the question? Do you want to turn off caching? Network responses are routinely cached in NSURLCache in both memory and persistent storage.

If this cache usage is problematic, change the requestCachePolicy for the session configuration accordingly. Or change the cachePolicy of a NSMutableURLRequest, itself. You can also configure the maximum size of the URLCache that the session configuration uses, to constrain both the amount of RAM usage as well as the amount of persistent memory usage.

Even if you turn off caching, as a general rule one should not be surprised that API calls increase memory consumption, through no fault of your own. It's not uncommon for an app to experience some modest memory consumption the first time it performs some task. But one should not see the same growth in subsequent iterations while the app runs. When tracking memory usage, it's generally advisable to repeat the task multiple times and see if the app returns back to some steady state (which is desirable), or whether it continues to grow (which requires further investigation to make sure your code is not the source of the problem). But we rarely worry about the initial memory consumption unless it is dramatic.

Looking at your code snippet, there's nothing obviously wrong. I'd be inclined to suspect routine memory consumption by iOS. Assuming the issue is broader than the general cache behavior, if the memory consumption is dramatic and/or continues to grow each time the app performs this code, then provide more details and we can help you diagnose this further.


This is what my memory profile looks like after four batches of 100 requests each; plus a final flag after I issued a memory warning:

enter image description here

(Note, that's a composite image so I could show you memory before the first batch, before the third batch, after the last batch and after I manually posted the memory warning. I combined these together to make it easier to see what the total allocations were at those four points in time.)

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Even changing the `requestCachePolicy` or the `URLCache` like this `config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;` `config.URLCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];` the memory consumption still growing for each request... so after perform 800 requests the memory consumption grows from 4mb to around 6mb, so 2mb that shouldn't be there – Victor Barros May 08 '15 at 18:35
  • See my answer above: the Security Framework's heap SSL cache is private, then each new session object increases it by 3.5k for 10 minutes. That's consistent with 2.7MB of growth after 10 mins, but then you will not see it go up from there unless there is some other problem. So run instruments for 25 mins and have a generation from 15 min to 25 min mark, and see how much growth is there. Note that if the frequency of requests increases or decreases during that time, that will change the 'high water level' of the SSL cache's memory footprint. – CommaToast Mar 05 '16 at 20:15
0

Note that on iOS 9, Security framework allocates appx. 4k of SSL cache data and charges it to your app the first time you resume a task for a new NSURLSession object. Apple Technical Q&A QA1727 tells us this SSL cache persists for 10 minutes, no matter what, since it's private and entirely managed by the system (because security!).

In your code example, you are creating a new NSURLSession object each time you make a request. But you are just using defaultSessionConfiguration and not specifying a delegate to which a strong reference might exist, then what you should instead do is just use the singleton [NSURLSession sharedSession] and use resetWithCompletionHandler to clear up non-security allocations. Or make a custom singleton if you want to customize the configuration.

  • (NSURLSession *)sharedSession Discussion

For basic requests, the URL session class provides a shared singleton session object that gives you a reasonable default behavior. By using the shared session, you can fetch the contents of a URL to memory with just a few lines of code.

Unlike the other session types, you do not create the shared session; you merely request it by calling [NSURLSession sharedSession]. As a result, you don’t provide a delegate or a configuration object. Therefore, with the shared session:

   - You cannot obtain data incrementally as it arrives from the server.
   - You cannot significantly customize the default connection behavior.
   - Your ability to perform authentication is limited.
   - You cannot perform background downloads or uploads while your app is    
     not running.

The shared session uses the shared NSURLCache, NSHTTPCookieStorage, and NSURLCredentialStorage objects, uses a shared custom networking protocol list (configured with registerClass: and unregisterClass:), and is based on a default configuration.

When working with a shared session, you should generally avoid customizing the cache, cookie storage, or credential storage (unless you are already doing so with NSURLConnection), because there’s a very good chance that you’ll eventually outgrow the capabilities of the default session, at which point you’ll have to rewrite all of those customizations in a manner that works with your custom URL sessions.

In other words, if you’re doing anything with caches, cookies, authentication, or custom networking protocols, you should probably be using a default session instead of the default session.

(From NSURLSession Class Reference... Italics mine ;P)

If you are not using the sharedSession Apple-provided singleton, then you should at least take a cue from Apple and roll your own singleton with a session property. The point of a session is that it is intended to live longer than just one request. Despite their unclear documentation, the fact that Apple provides a singleton, and calls it a "session", indicates session objects were meant to live longer than just a single request.

Yeah, you are supposed to invalidateAndCancel at some point, but not after every single request, and not even if each request is going to a different server entirely (which is almost never the case). You only need to invalidate and cancel if you are going to break your reference to a particular session; otherwise, you can just call flushWithCompletionHandler or resetWithCompletionHandler on the session to flush your session's heap allocations to VM, or reset to clear both heap and VM storage. (Also see my answer here.)

Community
  • 1
  • 1
CommaToast
  • 11,370
  • 7
  • 54
  • 69