'NSData dataWithContentsOfURL:' memory leak iOS 9.x? - ios

My code is the following:
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *theURLString = #"http://website.com/musicFile";
NSData *theData = [NSData dataWithContentsOfURL:[NSURL URLWithString:theURLString]];
}
There is nothing special at all. I am not even using the background thread.
Here is the behavior I get on iOS 8.x (and the behavior that I expect to get):
So, NSData is fully released and all of the occupied memory is back.
However, iOS 9.x surprised me a lot:
My questions are:
Approximately 100 MB are gone for nothing in iOS 9.x. How can I get them back? Are there any workarounds?
iOS 8.x has occupied 136.2 MB at max, while iOS 9.x used 225.9 MB at max. Why is this happening?
What is going on in iOS 9.x?
UPDATE #1:
I have also tried using NSURLSession 'dataTaskWithURL:completionHandler:' (thanks to #CouchDeveloper). This reduces the leak, but doesn't fully solve the problem (this time both iOS 8.x and iOS 9.x).
I used the code below:
NSURLSession *theURLSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSURLSessionDataTask *theURLSessionDataTask = [theURLSession dataTaskWithURL:[NSURL URLWithString:theURLString]
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error)
{
NSLog(#"done");
}];
[theURLSessionDataTask resume];
As you can see, 30 MB are still lost.
UPDATE #2:
The above tests where done using Xcode simulator.
However, I have also decided to test on actual iOS 9.2 iPhone 4S (as recommended by #Sohil R. Memon).
The results of 'NSData dataWithContentsOfURL:' are below:
The results of using 'NSURLSession dataTaskWithURL:completionHandler:' are below:
It looks like 'NSData dataWithContentsOfURL:' works perfectly on actual device, while 'NSURLSession dataTaskWithURL:completionHandler:' -- doesn't.
However, does anyone know solutions which show identical information on BOTH actual device AND Xcode simulator?

Approximately 100 MB are gone for nothing in iOS 9.x. How can I get them back? Are there any workarounds?
For a couple of reasons, we should use NSURLSession to download data from a web service. So, this is not a workaround, but the correct approach.
What is going on in iOS 9.x?
I have no idea - possibly cached data, network buffers, or some other issues. But this is irrelevant - you should try the correct approach with NSURLSession.
From the docs:
IMPORTANT
Do not use this synchronous method to request network-based URLs. For network-based URLs, this method can block the current thread for tens of seconds on a slow network, resulting in a poor user experience, and in iOS, may cause your app to be terminated.
Instead, for non-file URLs, consider using the dataTaskWithURL:completionHandler: method of the NSURLSession class. See URL Session Programming Guide for details.
Edit:
Those "reasons" are:
NSURLSession is specifically designed to load remote resources.
NSURLSession methods are asynchronous which is crucial for methods which complete only after a perceivable duration (it will not block the calling thread).
A session can handle authentication by means of a default method or with a custom delegate.
Session tasks can be cancelled.

Here is also an answer which helped me. The answer states to use [NSData dataWithContentsOfURL:url options:0 error:&error]; instead.
Hope this helps

Related

Will performFetchWithCompletionHandler be called if the app has been terminated

It's surprisingly difficult to find a definitive answer to this; couldn't find it mentioned in the Apple documentation and couldn't find a definite yes/no after searching past questions.
The question is simple - if the app requests a background fetch to be performed after N time, then the user terminates the app. Will the OS still launch the app into the background to perform the background fetch?
Okay, once again background modes cause confusion. No offense to the other people trying to help, but this is more complicated than it seems.
First of all:
This is out of date, as Sausage guessed in the comments. I know this for a fact, because the section about VoIP apps is still explaining the "old way" to do this, with a handler that gets called periodically. I investigated this a bit for this answer, so I suggest you go and read that. The important lesson for this case here is that iOS makes a distinction between an app being terminated by the user or by the system, plus it also plays a role whether the phone was rebooted or not.
So to sum this (and your question) up you basically want to know whether this part of the above, outdated documentation is still correct word for word:
In most cases, the system does not relaunch apps after they are force quit by the user. One exception is location apps, which in iOS 8 and later are relaunched after being force quit by the user. In other cases, though, the user must launch the app explicitly or reboot the device before the app can be launched automatically into the background by the system. When password protection is enabled on the device, the system does not launch an app in the background before the user first unlocks the device.
Apple: Understanding When Your App Gets Launched into the Background
I thoroughly investigated the rest of the docs, but did not find any definite answer, so it unfortunately boils down to what dan already suggested: Test it. My gut feeling is that the documentation is still correct in that regard, though (as said what's not is the VoIP stuff). I say that because the UI in the Settings app calls the feature "Background App Refresh", so users are probably supposed to understand that an app having this permission won't refresh when they "push" them out of background (i.e. home button -> swipe it out). For regular users, apps are either quit (not in the task manager at all), in the foreground (using them) or in background (they're in the task manager and another app is in foreground and/or the phone is locked).
To really test this you'd have to write an app and actually carry it around a bit (I assume at least two days) in each condition. First while it is in background (the OS should periodically let it fetch, as you probably know this can also be triggered in Xcode) and then while it is force-quit. The problem is to verify that it fetched stuff. I'd go with a logfile that can be shared via iTunes. I have typed up some code for this:
-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
NSLog(#"We're awake! Booyah!");
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config
delegate:nil
delegateQueue:[NSOperationQueue mainQueue]];
NSMutableURLRequest *request = [NSMutableURLRequest new];
request.HTTPMethod = #"GET";
request.URL = [NSURL URLWithString:#"https://www.google.com"];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
completionHandler:^(NSData * _Nullable data,
NSURLResponse * _Nullable response,
NSError * _Nullable error) {
NSDate *now = [NSDate date];
NSString *toLog = [NSString stringWithFormat:#"%# - fetched\n",
[now description]];
[self updateTestDocumentWithString:toLog];
NSLog(#"Yay, done!");
completionHandler(UIBackgroundFetchResultNewData);
}];
[task resume];
}
- (void)updateTestDocumentWithString:(NSString *)toAppend {
NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *filePath = [[docDir stringByAppendingPathComponent:#"logfile.txt"] copy];
if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
if (![[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]) {
NSLog(#"We're effed...");
return;
}
}
NSFileHandle *file = [NSFileHandle fileHandleForUpdatingAtPath:filePath];
if (!file) {
NSLog(#"We're effed again...");
return;
}
[file seekToEndOfFile];
// ensure this is never nil
[file writeData:[toAppend dataUsingEncoding:NSUTF8StringEncoding]];
[file closeFile];
}
This would go into the app delegate, and don't forget to add the Application supports iTunes file sharing boolean setting in your app's plist. I will leave this running on my development device for a bit and check the logfile, eventually reporting back here. Feel free to test it yourself, too.
EDIT:
https://devforums.apple.com/message/873265#873265 (login required)
Also keep in mind that if you kill your app from the app switcher
(i.e. swiping up to kill the app) then the OS will never relaunch the
app regardless of push notification or background fetch. In this case
the user has to manually relaunch the app once and then from that
point forward the background activities will be invoked. -pmarcos
That post was by an Apple employee so I think i can trust that this information is correct.
OLD answer:
According to this answer wrote by a top user: iOS background fetch: your app won't be woken up again.
Make sure you're not killing the app (i.e. by double tapping on the
home button and swiping up on your app for force the app to
terminate). If the app is killed, it will prevent background fetch
from working correctly.
It really doesn't make sense for it to be woken up...it kinda invalidates the user killing the app.
Having that said there are different ways a terminated/force quit app can be launched again:
Tapping on a notification.
Tapping on the app icon.
Using openUrl to open your app from another app.
If you use PushKit...then your app would be launched. Imagine if had a VOIP app e.g. Skype, WhatsApp and a friend was calling you but you had have force-quit the app, you wouldn't receive calls. For more see here.
Location updates either through use of region monitoring or the significant-change location service. See this answer and make sure to read this entire page from Apple docs.
Rebooting the device would also undo anything blocked through force-quit
Reading the Apple documentation here I found this text snippet which should explain your question:
The techniques offered by iOS fall into three categories:
Apps that start a short task in the foreground can ask for time to finish that task when the app moves to the background.
**Apps that initiate downloads in the foreground can hand off management of those downloads to the system, thereby allowing the app to be suspended or terminated while the download continues.**
Apps that need to run in the background to support specific types of tasks can declare their support for one or more background execution modes.
The second option is exactly about downloading the data, which can be delegated to the system even if the can be terminated.

Prevent caching in NSURLSession for iOS Simulator

I'm trying to find a way to prevent NSURLSession from caching responses (in Simulator) by using approach from these questions:
Prevent NSURLSession from caching responses
NSURLCache does not clear stored responses in iOS8
I don't want to use approach from this question:
Clear NSURLSession Cache
which is using ephemeralSessionConfiguration call to NSURLSessionConfiguration when setting up a new NSURLSession instance.
I just want to not cache requests, in worst case clear the cache when application resigns active.
With call [NSURLCache sharedURLCache] removeAllCachedResponses] in applicationWillResignActive execution in app delegate i still get Cache.db present and full of cached responses/requests in respective iOS Simulator cache on disk.
Before that, I changed all my NSURLSessionConfiguration cache policy to NSURLRequestReloadIgnoringCacheData NSURLRequestReloadIgnoringLocalCacheData
NSURLRequestReloadIgnoringLocalAndRemoteCacheData
(tested all three cases, the last one is probably not implemented) instances (and effectively ALL cache policies). Isn't changing cache policy supposed to result in not caching server responses and requests on simulator? Does it depends on storage policy in response headers?
I'm using iOS 9.3 and iOS 10.1 Simulators for iPhone and v. 10 of iOS SDK. Project is in Objective C, maybe full Swift project would behave differently?
Is this behavior different for simulators and with devices? Why is this happening, is there a solution different from using dependency (can't fall onto one right now for some reason).
I had a similar issue and I was able to fix this by setting the properties- URLCache and requestCachePolicy on NSURLSessionConfiguration to nil and NSURLRequestReloadIgnoringCacheData respectively.
Also, you can try setting the cachePolicy property on NSMutableURLRequest to NSURLRequestReloadIgnoringCacheData.
So the rude solution is ... drop the Cache.db file on app resigning. I assume on the simulator you should be able to?!
But, did you try the ephemeralSessionConfiguration? Because Apples header doc says:
* An ephemeral session has no persistent disk storage for cookies,
* cache or credentials.

NSURLConnection randomly fails until device reboot

I have been struggling with an issue where NSURLConnection calls instantly fail. The device needs to be rebooted entirely or Flight Mode needs to be toggled on/off to resolve the problem. Restarting the app (swipe up) alone does not help.
Some facts:
-All URLs are HTTPS, TLS 1.2 compatible with Forward Secrecy. There are no issues with ATS and iOS 9. The error has been present since iOS 7 and remains on 9.2.
-No third party frameworks are used by the app. I use only native NSURLConnection calls that always work, except for when this odd situation occurs.
-No infrastructure/network issues - other devices on same networks (same WiFi for instance) work in the same app at the same time. Going to/from 3G/Wifi makes no difference.
-I always implement willCacheResponse to return nil.
-The service is hosted on AWS Elastic Beanstalk, so some suggested that it might be a DNS caching issue in case of IP address changes - this seems unlikely to me and should trigger multiple errors at once on different devices, which I have never seen.
-The method called is didFailWithError, instantaneously, as if there were no Internet connection on the device at all - all other apps work, however.
-The website that hosts the API used by the app can be browsed with no problems at all times. The website actually makes the same requests to fetch data.
The error code returned is -1003, kCFURLErrorCannotFindHost. I've been following a thread on Git dealing with the same issue to no avail. https://github.com/AFNetworking/AFNetworking/issues/967
I tried using NSURLRequestReloadIgnoringCacheData for all my requests, but that did not help.
With this information, will anyone care to venture a guess what I might be doing wrong? I added the bounty because I have no idea how to approach this problem - especially because it's so inconsistent. And it is definitely not a legitimate error (that is, that the domain could not be found), as the service is operating fine while this happens on random clients.
I create my request with a static method that looks like this. It's been stripped of some non-public info, but basically it just performs a POST request with JSON data. [Controller getSQLHost] just returns a URL - the base domain.
+(NSURLConnection*)initiatePOSTwithJSONDictionary:(NSDictionary*)dictionary toURL:(NSString*)urllocation withDelegate:delegate {
NSMutableDictionary *connectionDictionary = [[NSMutableDictionary alloc] init];
if (dictionary) {
[connectionDictionary setObject:dictionary forKey:#"input"];
}
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:connectionDictionary options:kNilOptions error:nil];
NSURL *url = [NSURL URLWithString:[[Controller getSQLHost] stringByAppendingString:urllocation]];
NSString *postLength = [NSString stringWithFormat:#"%i", (int)[jsonData length]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:30.0];
[request setHTTPMethod:#"POST"];
[request setValue:postLength forHTTPHeaderField:#"Content-Length"];
[request setHTTPBody:jsonData];
return [[NSURLConnection alloc] initWithRequest:request delegate:delegate];
}
Does you delegate implement connectionShouldUseCredentialStorage ? (or respond with YES)
I think the device's keychain is used when this method returns yes which may explain the persisting failure beyond the life time of the running application and why rebooting or otherwise resetting network connectivity "fixes" it. If an authentication failure has been recognized once, it may linger in the key chain for a little while which would then respond immediately without actually going to the server.
What would cause the authentication to register as a failure in the keychain in the first place may depend on a variety of factors. It could be as simple as a typo in the saved password or more convoluted such as some certificate expiration preventing the SSL layer from establishing a secure link.
You're creating NSURLConnections on the current runloop. How many are active at any one time? Have you considered using an NSOperationQueue so that you don't get bitten by load bugs?
Is your delegate thread-safe? If not, that could explain the sporadic-ness of the bug.
You say you don't see the problem often, but others do. Can you borrow their devices and maybe even them and their usage patterns and thus get to see the problem more often?

NSURLSession with a performSelector:

I found out that iOS can run processes even when your application is in background mode (minimized) in at most 10 minutes, and to work around that and make it run forever, it would be necessary:
Apps that need to download and process new content regularly
And that's what using NSURLSession API to make requests to my server:
-(void)SendFiles{
...
NSURLSessionDataTask * dataTask =[defaultSession dataTaskWithRequest:urlRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if(error == nil){
[self performSelector:#selector(SendFiles) withObject:nil afterDelay:3600.0];
}
}];
[dataTask resume];
}
Some of us know that to run processes even when the app is minimized, we should use some tags, but if you are using the API NSURLSession is no longer needed this kind of thing, right?
In the case of my project, he needs to make such a request every 1 hour on my server, and for this I added a code within the block completionHandler: to be called every 1 hour the same method that executes the send command files to the server.
My question is: The time of 10 minutes can affect the selector with delay of 3600 seconds? causing my app is fully down, and I can not send files to the server?
A couple of thoughts:
The ten minute window was reduced to three minutes in iOS 7.
You only get that window if you request it with beginBackgroundTaskWithName (or, prior to iOS 7, beginBackgroundTaskWithExpirationHandler). See the Executing Finite-Length Tasks section of the App Programming Guide for iOS.
Note, I believe that this background state behavior changes whether you run the app connected to the Xcode debugger, so make sure to test this on an actual device, and run the app directly from the device, not through Xcode.
Yes, this limited amount of time will affect your ability to call performSelector:afterDelay: with a delay of 3600.0.
If you want to have the app periodically ask the server for data, you would nowadays use "background fetch". See the Fetching Small Amounts of Content Opportunistically in the App Programming Guide. This is designed to poll a server to see if there is data to be downloaded.
You only have 30 seconds for this request to be performed. You also have no control over the timing of these requests (though whether you report whether there was data available or not may affect the timing of the subsequent request).

NSURLSession download task grows memory in debug with default session config

I've got an app that downloads files using NSURLSession on iOS 7 and later. I start the task like this:
//property declared: #property (retain) NSURLSession *session;
NSString *addr = #"http://www.example.com/path/to/nontrivial/file.dat";
NSURL *url = [NSURL URLWithString:addr];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
[[self.session downloadTaskWithRequest:request] resume];
In the completion handler (URLSession:downloadTask:didFinishDownloadingToURL:), I move the file to my application data folder and call invalidateAndCancel on the session. In the progress handler (didWriteData:totalBytesWritten:totalBytesExpectedToWrite:), I simply log the progress.
As this runs in debug, memory usage increases quickly, to the tune of a few times the amount of data that has been downloaded. If I do it in a background task, though, memory usage is constant.
Is it bad to use the default session configuration with a download task? Is there some kind of cache property that I should be setting? What's going on here?
Update: As far as I can tell, this only happens when the debugger is attached. When I launch it in the simulator without the debugger, I don't see any memory usage increase in Activity Monitor.
Update 2: This appears to have been fixed in iOS 8.3. Yay!
This same behavior was happening to me. It was manifesting itself via hard crashes when memory became depleted. For anyone else who's curious if they're seeing this same problem, follow these steps...
Open activity monitor alongside the iOS simulator, go to the memory tab and enter your app's name in the search results.
Run your app in the simulator via xcode. Wait until you see the process listed in activity monitor, and then queue up a download or two. Activity monitor will show the memory usage for your app ballooning out of control.
Stop execution of your app in xcode. Restart the app in the simulator without the aid of xcode. Wait for your app to appear in activity monitor again, and queue up a few more downloads. When you do it this time, the memory usage for your app should basically remain the same.
This was a really great observation on your part. I never would have thought to investigate this path. Thanks!

Resources