All about handling Background tasks in iOS 13 and below
UIApplication background task mechanism allows you to prevent your app from being suspended for short periods of time. While the API involved is quite small, there’s still a bunch of things to watch out for:
- The name background task is somewhat misappropriate. Specifically,
beginBackgroundTask(expirationHandler:)doesn’t actually start any sort of background task, but rather it tells the system that you have started some ongoing work that you want to continue even if your app is in the background. You still have to write the code to create and manage that work. So it’s best to think of the background task API as raising a “don’t suspend me” assertion.
- You must end every background task that you begin. Failure to do so will result in your app being killed by the watchdog. For this reason I recommend that you attach a name to each background task you start (by calling
beginBackgroundTask(expirationHandler:)). A good name is critical for tracking down problems when things go wrong.
WARNING Failing to end a background task is the #1 cause of background task problems on iOS. This usually involves some easy-to-overlook error in bookkeeping that results in the app beginning a background task and not ending it. For example, you might have a property that stores your current background task identifier (of type
UIBackgroundTaskIdentifier). If you accidentally creates a second background task and store it in that property without calling
endBackgroundTaskon the identifier that’s currently stored there, the app will ‘leak’ a background task, something that will get it killed by the watchdog.
- Background tasks can end in one of two ways:
a. When your app has finished doing whatever it set out to do.
b. When the system calls the task’s expiry handler.
Your code is responsible for calling
endBackgroundTask(_:)in both cases.
- All background tasks must have an expiry handler that the system can use to ‘call in’ the task. The background task API allows the system to do that at any time.
- Your expiry handler is your opportunity to clean things up. It should not return until everything is actually cleaned up. It must run quickly, that is, in less than a second or so. If it takes too long, your app will be killed by the watchdog.
- Your expiry handler is called on the main thread.
- It is legal to begin and end background tasks on any thread, but doing this from a secondary thread can be tricky because you have to coordinate that work with the expiry handler, which is always called on the main thread.
- The system puts strict limits on the total amount of time that you can prevent suspension using background tasks. iOS 13 beta has reduced the from-the-foreground value to 30 seconds. On current systems you can expect values like:
a. 3 minutes, when your app has moved from the foreground to the background
b. 30 seconds, when your app was resumed in the background
WARNING: I’m quoting these numbers just to give you a rough idea of what to expect. The target values have changed in the past and may well change in the future, and the amount of time you actually get depends on the state of the system. The thing to remember here is that the exact value doesn’t matter as long as your background tasks have a functional expiry handler.
- You can get a rough estimate of the amount of time available to you by looking at
IMPORTANT: The value returned by
backgroundTimeRemainingis an estimate and can change at any time. You must design your app to function correctly regardless of the value returned. It’s reasonable to use this property for debugging but we strongly recommend that you avoid using as part of your app’s logic.
WARNING: Basing app behaviour on the value returned by
backgroundTimeRemainingis the #2 cause of background task problems on iOS.
- The system does not guarantee any background task execution time. It’s possible (albeit unlikely, as covered in the next point) that you’ll be unable to create a background task. And even if you do manage to create one, its expiry handler can be called at any time.
beginBackgroundTask(expirationHandler:)can fail, returning
UIBackgroundTaskInvalid, to indicate that you the system is unable to create a background task. While this was a real possibility back when background tasks were first introduced, where some devices did not support multitasking, you’re unlikely to see this on modern systems.
- The background time ‘clock’ only starts to tick when the background task becomes effective. For example, if you start a background task while the app is in the foreground and then stay in the foreground, the background task remains dormant until your app moves to the background. This can help simplify your background task tracking logic.
- The amount of background execution time you get is a property of your app, not a property of the background tasks themselves. For example, starting two background task in a row won’t give you 6 minutes of background execution time.
- Notwithstanding the previous point, it can still make sense to create multiple background tasks, just to help with your tracking logic. For example, it’s common to create a background task for each job being done by your app, ending the task when the job is done.
- Do not create too many background tasks. How many is too many? It’s absolutely fine to create tens of background tasks but creating thousands is not a good idea.
IMPORTANT: iOS 11 introduced a hard limit on the number of background task assertions a process can have (currently about 1000, but the specific value may change in the future). If you see a crash report with the exception code 0xbada5e47, you’ve hit that limit.
NOTE: The practical limit that you’re most likely to see here is the time taken to call your expiry handlers. The watchdog has a strict limit (a few seconds) on the total amount of time taken to run background task expiry handlers. If you have thousands of handlers, you may well run into this limit.
- If you’re working in a context where you don’t have access to
UIApplication(an app extension or on watchOS) you can achieve a similar effect using
- If your app ‘leaks’ a background task, it may end up being killed by the watchdog. This results in a crash report with the exception code 0x8badf00d (“ate bad food”).
IMPORTANT: A leaked background task is not the only reason for an 0x8badf00d crash. You should look at the backtrace of the main thread to see if the main thread is stuck in your code, for example, in a synchronous networking request. If, however, the main thread is happily blocked in the run loop, a leaked background task should be your primary suspect.
- Prior to iOS 11 information about any outstanding background tasks would appear in the resulting crash report (look for the text
BKProcessAssertion). This information is not included by iOS 11 and later, but you can find equivalent information in the system log.
- You can monitor your device’s system log interactively using the Console app on macOS. The system log is also included in a sysdiagnose log, so if you have a problem that only shows up in the field you can ask the user to send you that. For more information about sysdiagnose logs, see the Bug Reporting > Profiles and Logs page.
- The system log is very noisy so it’s important that you give each of your background tasks an easy-to-find name.
- Tasks started in the background have a much shorter time limit. While the exact limit is not documented — it has changed in the past and it may well change again in the future — the current value is about 30 seconds as mentioned above. So here’s what happens:
- The system resumes (or relaunches) your app in the background because the user enters your region.
- You schedule a timer for 3 minutes and start a background task to prevent the app from being suspended.
- After 30 seconds the background task expires. At this point one of two things happens:
a. If you have an expiry handler, that will be called. It must end the background task, at which point your app will suspend (A).
b. If you have no expiry handler, or it fails to end the background task promptly, the watchdog will kill your app (B).
- This means that the timer will not run after 3 minutes. Either your app is suspended (which will prevent the timer from running) or your app is terminated.
- This state continues until the user leaves your region. At that point one of two things happens:
a. In case A, the system will resume your app in the background. Your timer is long overdue, so it’ll fire now.
b. In case B, the system will relaunch your app in the background. As this is a new process, the timer no longer exists.
IMPORTANT: If you want to test this, make sure to run your app from the Home screen rather than from Xcode. The Xcode debugger disables the watchdog, prevents your app from suspending, and so on.
Do read and share !!
Thanks to the Quinn “The Eskimo!” from Apple Developer Relations for his extensive and updated comments.