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!
Related
I have been using NSURLSession to do background uploading to AWS S3. Something like this:
NSURLSessionConfiguration* configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#“some.identifier"];
NSURLSession* session = [NSURLSession sessionWithConfiguration:configuration delegate:someDelegate delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionUploadTask* task = [session uploadTaskWithRequest:request fromFile:[NSURL fileURLWithPath:httpBody]];
[task resume];
In someDelegate, I have implemented didSendBodyData, didCompleteWithError and handleEventsForBackgroundURLSession.
I have three questions:
I have noticed that if I close the app while uploading is in progress, transfer will continue and successfully finish. Is handleEventsForBackgroundURLSession called when the transfer is finished while the app is closed?
Assuming that the answer to the first question is yes, how can I delete httpBody in handleEventsForBackgroundURLSession? This is a temporary file that is not needed once transfer is complete.
I would appreciate it if someone explained, in detail, how background transfer works in iOS. That is when memory is created, which callbacks are called at which states and how the app is woken up once the transfer is completed. Thanks.
When the app delegate's handleEventsForBackgroundURLSession is called, you should:
save the completion handler;
instantiate your background NSURLSession;
let all of your delegate methods to be called;
in your URLSession:task:didCompleteWithError:, you can remove those temp files; and
in URLSessionDidFinishEventsForBackgroundURLSession:, you can call that saved completion handler.
A few additional notes:
There seems to be some confusion about what happens when an app is terminated.
If the app is terminated in the course of its normal lifecycle, the URLSession daemon will keep the background requests going, finishing your uploads, and then wake up your app when it's done.
But manually force-quitting the app (e.g., double tapping on home button, swiping up on the app to force it to quit) is a completely different thing (effectively, the user is saying "stop this app and everything associated with it"). That will stop background sessions. So, yes, background sessions will continue after the app is terminated, but, no, not if the user force-quit the app.
You talk about setting breakpoints and observing this in Xcode. You should be aware that the process of being attached to Xcode will interfere with the normal app life cycle (it keeps it running in background, preventing it from being suspended or, during the normal course of events, terminating).
But when testing background session related code, it's critical to be test the handleEventsForBackgroundURLSession workflow when your app was terminated, so to that end, I'd suggest not using Xcode debugger when testing this dimension of background sessions.
I use the new OSLog unified logging system, because the macOS Console can watch what is logged by the app, while not having Xcode running at all. Then I can write code that starts some download or upload, terminates app and then watch the logging statements I have inserted in order to observe the restarting of the app in background via the macOS console. See Unified Logging and Activity Tracing video for a tutorial of how to watch iOS logs from the macOS Console.
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.
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
I would like to know how it is possible to continue a async NSURLConnection, which has been started in the foreground, when the app will be terminated.
Currently I am starting a NSURLConnection when the app goes in the background. This works fine as long as the user is slower than the connection, when he wants to terminate the app. But when the user is quicker than it, the connection can't be established.
Here is my code:
// AppDelegate.m
- (void)applicationDidEnterBackground:(UIApplication *)application
{
AnObject *newObject = [[AnObject alloc] init];
[newObject InactiveApp];
}
// AnObject.m
- (void)InactiveApp
{
self.backgroundTaskID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:NULL];
// setting up the NSURLRequest
// [...]
dispatch_async(dispatch_get_main_queue(), ^
{
[NSURLConnection connectionWithRequest:request delegate:self];
});
}
// delegate functions, endBackgroundTask-closing, etc. is following
Unfortunately this is not working and I would like to know whether someone knows another way to fix it. There has to be a way which is similar like Snapchat or WhatsApp is doing it, because when you write an message and terminate the app right after pressing send, the message will be delivered.
The only way I could imagine is to do it with a background fetch but I think that is not the best solution, due to the fact that I just want to make one single connection when the App is send to the background.
I agree with Andy, that you should pursue NSURLSession and a background NSURLSessionConfiguration. See downloading content in the background section of the App Programming Guide for iOS: Background Execution.
By the way, the idea in your question will work fine (especially if you need support for iOS versions prior to 7.0, where NSURLSession and its background sessions are not available). Two observations regarding your code snippet:
The way you've written it, would appear that your AnObject would be prematurely deallocated when it falls out of scope and your app would therefore fail when it tried to call the delegate methods. Make sure to maintain a strong reference to AnObject.
Don't forget to call endBackgroundTask when the download is done. Likewise (and more subtly), the timeout handler should end the background task, too. See the Executing Finite Length Task section of the aforementioned App Programming Guide.
By the way, you mention requests continuing after the app is terminated. If a user manually terminates an app, that kills both background tasks contemplated in your question as well as background NSURLSession tasks. These are intended to gracefully handle continuing tasks if the app leaves foreground, not if the user manually terminates the app. The NSURLSession approach gracefully handles terminations due to memory pressure, but not manual termination.
When I force my device to go in sleep mode by pressing the power button, my background task stops by calling the delegate method didCompleteWithError with the error :
The operation couldn’t be completed. Operation not permitted
How can I configure my NSURLSession to continue the download even in sleep mode?
Is it even possible? If not, what options do I have? I need to download a file of 300Mb, so with a low connection the application will go in sleep mode before the end of the download.
Here is the creation of my session :
static NSURLSession *backgroundSession;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
backgroundSession = [NSURLSession sessionWithConfiguration:
[NSURLSessionConfiguration backgroundSessionConfiguration:
#"com.myapp.mytask"] delegate:self.
myDelegate delegateQueue:self.myQueue];
});
NSURLSessionDownloadTask *task = [backgroundSession downloadTaskWithRequest:
self.urlRequest];
[task resume];
The problem is that the Data Protection Capability is activated. With that enabled all files are stored with NSFileProtectionComplete by default, even the temporary file used to download by the NSURLSession:
The default level of protection is complete protection, in which files
are encrypted and inaccessible when the device is locked. You can
programmatically set the level of protection for files created by your
app, as described in “Protecting Data Using On-Disk Encryption” in iOS
App Programming Guide.
With NSFileProtectionComplete activated on that file you cannot access it when the device is locked.
I'm not sure if the temporary download file can be configured to not use data protection, it seems like that is not exposed by NSURLSession.
Source: App Distribution Guide