With iOS 7, Apple introduced new networking APIs to replace the traditional NSURLConnection
class for creating network requests and handling network responses. Built on top of NSURLConnection
is the new NSURLSession
API, which offers rich functionality for setting session-level configuration options, performing background processing (even if the application crashed), easier sending of network requests, and receiving responses using asynchronous blocks versus traditional delegate patterns. While these new APIs are easier to use and follow newer patterns that Apple incorporates within its other APIs, if developers follow Apple's documentation and other mainstream tutorials, they may find they are leaking memory, and eventually applications can crash due to memory pressure.
Application Example
Before I show where the memory leak occurs, let's create a sample application and use Apple's URL Loading System Programming Guide for instruction. Specifically, let's use the example code in the section "Fetching Resources Using System-Provided Delegates." I will modify the example code slightly to PUT
a photo to a site using a REST service.
With the example code, I still need to create a NSURLSessionConfiguration
object. NSURLSessionConfiguration
allows me to set up certain parameters about future requests that are made for a session. It can set HTTP header values, caching properties, permit cellular access, and more.
For the NSURLSessionConfiguration
object, I will use the defaultSessionConfiguration
that Apple provides. Apple's documentation on the default session configuration reads: "The default session configuration causes the session to behave similarly to an NSURLConnection
object in its standard configuration. If you are porting code from NSURLConnection
, use this method to obtain an initial configuration and then customize the returned object as needed."
Based on this description, it's a perfect fit, especially if you are porting over an iOS 6 application. You could add additional configuration details to the NSURLSession
, such as restricting cellular access, but I will just return the default configuration in this example:
-(NSURLSessionConfiguration *)sessionConfiguration { return [NSURLSessionConfiguration defaultSessionConfiguration]; }
Our session configuration is fairly simple. However, based on Apple's documentation, it should be sufficient.
For uploading the photo, I wrapped the example code provided by Apple in a method that takes image data and an identifier for the image. I made slight adjustments to the code as follows:
- Created a
NSURLRequest
as aPUT
request with a given URL - Changed the task type to
NSURLSessionUploadTask
, which subclasses the originalNSURLSessionDataTask
. - Invalidated the session after all tasks are finished
The last change I made (item 3 above) is calling finishTasksAndInvalidate
. Making this call is crucial; however, it is not emphasized enough in documentation. If you do not invalidate your session, it will leak memory. That being said, this is not the memory leak we are uncovering.
-(void)putPhoto:(NSData *)imageData usingIdentifier:(NSString *)identifier { // 1 - Setup Session NSURLSession *delegateFreeSession = [NSURLSession sessionWithConfiguration:[self sessionConfiguration] delegate:nil delegateQueue:[NSOperationQueue mainQueue]]; NSURL *syncPhotoUrl = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost/sync/photo/%@", identifier]]; // 2 - Setup Request NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:syncPhotoUrl]; [request setHTTPMethod:@"PUT"]; // 3 - Upload Photo [[delegateFreeSession uploadTaskWithRequest:request fromData:imageData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { NSLog(@"Got response %@ with error %@.\n", response, error); NSLog(@"DATA:\n%@\nEND DATA\n", [[NSString alloc] initWithData:data encoding: NSUTF8StringEncoding]); } ] resume]; // 4 - Need to invalidate session to ensure no memory is leaked [delegateFreeSession finishTasksAndInvalidate]; }
If the above code (which contains only slight modifications to the code provided by Apple) is run, it will eventually crash due to memory pressure. Additionally, if you look at other well-known NSURLSession
tutorials out there (Ray Wenderlich and tuts+, for example), you will find that this code does not deviate from the tutorials and recommendations posted by Apple (see NSURLSession Class Reference and Using NSURLSession). To show the leak, let's profile this test application in Instruments.
Discovering the Memory Leak
If I run the application in Instruments using the "Allocations" instrument, I see that memory is allocated and released: However, not all of the memory that allocated is released. You notice this by observing the peaks and valleys in memory consumption where it never gets back down to the original utilization levels after a photo is uploaded to the server (Figure 1).
Figure 1: Initial memory profile.
Also, if you look at the defaultSessionConfiguration
for NSURLSessionConfiguration
, you will find that the default disk capacity is 2MB and the default memory capacity is 4MB:
(lldb) posessionConfiguration.URLCache.diskCapacity 20971520 (lldb) posessionConfiguration.URLCache.memoryCapacity 4194304
These values are relatively small to suspect that the application would crash due to memory pressure. Every time you create a NSURLSessionConfiguration
and provide it to a new NSURLSession
, you would expect that when the NSURLSession
is destroyed, the part of it owning the session configuration object it would be destroyed as well. However by examining Instruments, we find the number of NSURLSessionConfiguration
objects continue to increase (Figure 2):
Figure 2: Examining Instruments.
From this capture, there should not be 42 active session configurations while this application is running. After each NSURLSession
is completed, these should be deallocated. Now, what Instruments shows as far as memory consumption is somewhat misleading; clearly, we can see that the allocated memory is increasing at a level that would end up crashing the application due to memory pressure. (Figure 3 shows the memory usage after little more than one minute of use.)
Figure 3: A more detailed look at the memory leak.
For this sample application, we have the benefit of being able to detect the leak because of the large amount of data that we are sending. However, applications that transmit a smaller amount of data may not know they are leaking memory because the leak is at a smaller scale and may go unnoticed. This may be why this issue is not well known today our application is transmitting data at a large scale, however over time, even a small application can crash if appropriate measures (which I'll outline shortly) aren't taken.
Continuing to dig into the issue, look at the top memory allocation calls (Malloc
). You can see that there is a large amount of memory being allocated (Figure 4):
Figure 4: Checking Malloc.
Here, we have 20 living memory allocations taking 108.36 MB, and none of these were released. Additionally, in the next row, we still have 18 living memory allocations taking 49.85 MB. In this example, there were 38 photos that were synced to the server. Discovering their owners of these is no easy task and there is no direct way of finding them in Instruments. However, considering that the memory climbs with network requests, if we dig around enough for what network-related objects are not being released, we find that there are cache entries that are still living and never released.
Figure 5: Cache entries still living.
Caching is done as part of the defaultSessionConfiguration
we get from NSURLSessionConfiguration
. Even though the cache on disk has a capacity of 2MB and the cache in memory has a capacity of 4MB, our memory consumption grows way beyond that. Unbeknown to the developer, these entries will continue to grow even when the NSURLSession
object that was created has been deallocated. The question to ask is, "why is there a cache held in memory for an object we don't have a reference to?" This is the key issue and it's where the memory leak occurs.
Who is Vulnerable?
An application might not crash due to memory pressure if it consistently sends requests to the same set of URLs and has a limited number of URLs in this set. The crash depends on the number of unique URLs that the application sends requests to and the size of data in that request and response pair. The reason for this is that for each URL that the application sends a request to, it stores a cached request and response. If it already has a cached entry, it does not create another entry for that pair. Because of this, applications that use RESTful services where data is uploaded to a server using a PUT
request with a unique path are at most danger, since that URL will constantly change. Even applications that do not use RESTful services are not in the clear. If they have a set of URLs that they post data to, their application might not crash, but the memory that they have available will be constrained by the amount of memory that the cache is consuming for those URLs.
Workaround the Memory Leak
There are several ways to work around the memory leak. The most straightforward way is to configure your defaultSessionConfiguration
with a NSURLCache
object that has a memory capacity of 0. The following code reflects this configuration update:
-(NSURLSessionConfiguration *)sessionConfiguration { NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; config.URLCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil]; return config; }
With this simple update, you will not cache request/response data for that session (which, if you are calling Web services that deal with dynamic data, should cause no disadvantage).
Warning: From the documentation on NSURLSessionConfiguration, you may decide to use an ephemeralSessionConfiguration
. Please note that this ensures only that cache is not written to disk; However, the cache entries will still be written to memory.
Conclusion
While Apple's new networking APIs are very powerful, if a developer doesn't understand how memory is being allocated and released, the application can leak memory and eventually crash due to memory pressure. I've identified this as a leak, since the developer does not have a reference to the consumed memory to release it. Also, due to the use of automatic reference counting (ARC) in Mac OS, the developer would assume that when the session is deallocated from memory, the corresponding cache in memory would be deallocated as well. As I have demonstrated, this is not the case. Fortunately, a work around does exist.
Brian Krupp is a software developer at Hyland Software and a Doctoral Candidate in the Department of Electrical and Computer Engineering at Cleveland State University's Washkewicz College of Engineering. His current research focuses on enhancing the security and privacy of mobile operating systems, specifically without requiring modification to the operating system.