Put on your detective's hat: It's time to track down those unruly app terminations. We'll outline the six major reasons apps terminate in the background, and show you how you can use MetricKit to to help you identify key statistics to drive down the rate of terminations. Learn how to prevent problems and recover gracefully from inevitable jetsams, identify any underlying issues, and take actionable measures to fix them. And discover the importance of implementing state restoration to make terminations less jarring — especially where text entry or playback is concerned.
MetricKit is a key partner in tracking down these issues. You can learn more about the API in WWDC19's “Improving Battery Life and Performance,” as well as explore the latest updates in “What's new in MetricKit.”
Hi. My name is Andy Aude, and I'm an iOS System Experience engineer.
Today, I'm going to shed light on the many reasons applications can be terminated in the background and what you can do about it.
Just as you wouldn't want an app to crash while it's in the foreground, it can be equally as disruptive to have it crash in the background.
For example, I have this to-do-list app that launches to a main screen with several lists.
I'll drill into the grocery list and I'll start making a reminder to buy a pineapple.
Now, say I have to leave the app for a moment.
At some point, I'll come back to it.
Oh, no. Why did the app launch from scratch? And where's my reminder to buy the pineapple? This is because the app was terminated in the background.
There are many reasons apps get terminated in the background. Here are the most common, which we'll cover today.
Prior to iOS 14, you only had a signal for if two of these were happening.
But now, in iOS 14, we have new API for you to find out how often these terminations happen broken out by the specific reason.
MXBackgroundExitData provides the counts of each termination type.
And we're even going to give you counts for normal exits where the user explicitly terminated your app in the app switcher.
It should no longer be a mystery why your app was killed.
Crashes are the most straightforward type of termination.
Crashes are typically due to segmentation faults, illegal instructions and asserts. These events all generate crash logs, which are surfaced in the Xcode Organizer for you automatically.
If you want to learn more about crashes and how to fix them, check out "Understanding Crashes and Crash Logs" from WWDC 2018.
In addition to the Xcode Organizer, new in MetricKit, we've added the ability to get crash info programmatically directly from the device.
The MXCrashDiagnostic object contains everything you need to track down crashes and other terminations.
Check out "What's New in Metric Kit" to learn how to obtain these and other diagnostic reports.
Another common termination is a watchdog event. This is a long hang during key app transitions, such as launch, going background and becoming foreground again.
These transitions have a time limit on the order of 20 seconds.
Note that these terminations do not happen while the Debugger is attached.
Watchdogs usually indicate something seriously wrong, like a deadlock, infinite loop or some other unending synchronous work on the main thread.
When these happen, you can get a diagnostic report via MXCrashDiagnostic.
Inside, the call graph is there, and it will show you the offending symbols where your app is getting stuck. Next, if your app relies on background execution, know there are strict CPU usage limits.
After a sustained high CPU load in the background, the system will generate an energy exception report.
If the sustained work goes on long enough, the system terminates the app.
CPU resource exception logs are available in the Xcode Organizer, as well as via MXCPUExceptionDiagnostic.
These reports contain call stacks to identify what exactly your app was doing at the time of the termination.
Perhaps there's a bug in your code causing excess CPU activity that you can simply fix.
But if you need to do truly intensive work in the background, consider moving the work into a background processing task.
The background processing task gives you several minutes of runtime while the device charges overnight, without CPU resource limits.
Similar to avoiding excessive CPU usage, it's important to keep memory usage under control as well.
If your app's memory, represented by this green square, grows way too large, the system will terminate it right as it crosses the footprint limit. The footprint limit is the same in the foreground and the background.
Instruments and Xcode's Memory Debugger help to track down the source of excess memory use in your app.
Note that the limit does vary from device to device. Older devices generally have lower limits. If your app targets devices before iPhone 6s, you'll want to aim to stay under 200 megabytes at all times.
This will give you more than enough safety margin.
Now, unlike the footprint exceeded terminations, we have another type of memory-related termination: the pressure exit, also known as jetsam. This is the most common background termination.
If these are happening, you're not necessarily doing anything wrong, it's just that the system needs to free up memory for the foreground application and other running applications like music and navigation apps.
If the foreground app grows too much, your app, the green block, will be terminated.
You can directly reduce the rate of terminations by shrinking your memory footprint to be as small as possible when you go background. When going into the background, aim for less than 50 megabytes. The smaller, the better.
Consider dropping caches and any resources you can just read back from disk. You can always set things back to the way they were when going foreground again.
But remember, you can cause other apps to jetsam, so try to be a good steward and minimize your foreground usage too.
And even if you manage to get under 50 megabytes, you can't eliminate the risk of jetsam entirely. Jetsam is inevitable. When it happens is very unpredictable too. It can happen in the mere seconds after backgrounding your app if the very next user action requires an extreme amount of memory. For example, launching camera and taking dozens of photos in a burst.
So, what should you do about this? You should absolutely save your state upon going background. Such that when your app is launched next, it goes back to the same place the user left off.
If they had been editing a text field, users expect that text to be there when they come back. If media playback is involved, be sure to restore the playback position.
You can do this with UIKit's built-in State Restoration APIs which do a lot of the heavy lifting for you. If you implement State Restoration throughout your app, many users won't even realize the app had been killed in the background and had to relaunch.
After jetsam, the second most common termination reason is a background task timeout.
When moving from the foreground to the background, you can get some extra runtime to finish up critical work by calling UIApplication.beginBackgroundTask. When you finish that work, you call endBackgroundTask.
What you may not realize is that if you don't explicitly call endBackgroundTask, the system will terminate the app when the time runs out. And that termination happens only 30 seconds after suspending your app, which can be massively frustrating if your app doesn't implement State Restoration.
Think of each task as a stick of dynamite with a 30-second fuse.
Once your app enters the background, the fuse is lit.
As long as you end all tasks within 30 seconds, your app will suspend gracefully instead of terminating.
Now, we don't surface crash logs when these terminations happen, but we do surface the statistics around their frequency via MXBackgroundExitData.
The good news is that these are preventable with very careful handling of your background tasks.
To prevent these terminations, the first thing you should do is switch to the named variant of the UIKit API.
The named variant is helpful because it lets you isolate which of the potentially many background tasks in your app are not being ended.
Note that these terminations don't reproduce in the Debugger.
So, to make things easier, starting in iOS 13.4, there's a new console message that prints when a task has been held for way too long.
These prints happen even if your app is in the foreground. So if you see these prints while debugging your application, you should do an audit of your calls to beginBackgroundTask, ensuring there's a matching call to endBackgroundTask.
Another thing that can help is the expiration handler. It's good practice for all calls to beginBackgroundTask to provide an expiration handler.
Note that it's safe to call endBackgroundTask inside the handler.
Be careful to not kick off any additional expensive work in this expiration handler as you only have a few seconds.
Think of the expiration handler as a safety net against terminations. They shouldn't be the only place you call endBackgroundTask. You should call endBackgroundTask when the work actually ends, which allows the device to go to sleep sooner and preserve battery life.
If you are seeing your app is encountering these terminations via MXBackgroundExitData, and you are not able to reproduce the issue in the Debugger, it can be useful to add telemetry to see which expiration handlers are being called.
To do this, first make a log handle.
Then, drop an event signpost to say you've entered the expiration handler.
Then, do the necessary cleanup work to make it safe to suspend your app.
And lastly, drop a signpost as you exit the handler.
To check how often the signposts are being emitted, you inspect the signpost counts inside your MXMetricsPayload.
It can also indicate if your app is not making it through the expiration handler. In this example, I have an imbalance.
Our entered signpost count is greater than the exited signpost count.
This means the DatabaseExpirationHandler is either hanging or crashing.
You should be extra careful in starting tasks while the application is already in the background, since the expiration handler won't be called if you start a task with fewer than five seconds remaining.
To guard against this, you can estimate how long the work will take to complete using five as a lower bound.
And then compare this to how much background time remains.
If enough background time remains, it's safe to call beginBackgroundTask.
If not enough time remains, you could enqueue the work as a background processing task to happen later, such as when the device is charging.
The next thing to be careful with is how you store your UIBackgroundTaskIdentifiers. If you're storing them in an instance variable, like so, it's easy to run into trouble.
In this example, I begin a background task when the user taps the beginDataExport button in my app.
After several seconds, the data finishes exporting and I get a completion handler.
If I tap the beginDataExport button a few times, I can end up with multiple outstanding background tasks.
Remember, our instance variable can only hold a single task at a time, and so we'll leak all the identifiers except the most recent one.
Fortunately, it's easy to avoid this kind of bug.
The easiest fix is to use a local variable instead of an instance variable to hold your UIBackgroundTaskIdentifier.
In Swift, this local variable is captured by closures, so you can access it in the completion block and even the expiration handler.
With this strategy, each invocation of beginDataExport will track the task identifier in a separate underlying memory location, preventing a leak.
If you carefully audit all uses of beginBackgroundTask and endBackgroundTask in your application, you should be able to eliminate background task timeout terminations.
Now you know how to investigate crashes, watchdogs, resource exceptions and background task timeouts. If you fix these, your app will appear to launch much faster since it'll resume instead of launch from scratch.
As for the most common termination, you can reduce the chance of jetsam by using less than 50 megabytes of memory when in the background. The smaller, the better.
But jetsam is inevitable. It's going to happen. Make sure your app can recover with State Restoration and launch smoothly, returning to where the user left off.
Doing these three things will ensure a truly seamless multitasking experience.
Thanks for watching and happy termination hunting.
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.