I found that the application can be launched in the background, but the launchOptions in application:didFinishLaunchingWithOptions: in AppDelegate is empty. I would like to know under what circumstances the application can be launched in the background, given that background fetch is not enabled.
In what circumstances will the application be launched in the background?
I found that the application can be launched in the background, but the launchOptions in application:didFinishLaunchingWithOptions: in AppDelegate is empty. I would like to know under what circumstances the application can be launched in the background, given that background fetch is not enabled.
This is a much harder question to answer than you'd think. Over time we've accumulated more and more API that can wake an app in the background and the combination of those APIs and optimizations like launching app ahead of their expected time of use or app prewarming make it very difficult to provide a straightforward list. If you can tell me more about exactly what your app does and what APIs it uses then I can probably make a pretty good guess at why it was launched, but there isn't really a formal "list" anymore.
What problem are you actually having?
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware
Our application has an SDK that detects whether the user last exited abnormally. Simply put, if the app is quit from the foreground and does not receive the willTerminate callback, and there is no crash stack, it is deemed an abnormal exit. In May of this year, the reports of abnormal exits suddenly surged. Upon investigation, we found that the application was being launched in the background, and during the SDK initialization, it was set to a foreground state, which caused users who were launched in the background to be counted as having exited abnormally in the foreground.
We want to clarify the reasons for our application's background launches. I found in the documentation that a few scenarios can cause background launches:
- Background fetch
- Remote notifications - Silent notifications
- App prewarm
Analysis:
- Our application's background modes only enable Audio, AirPlay, Picture in Picture, and Remote notifications, so the possibility of background fetch can be ruled out.
- As for silent notifications, the code does not have any use cases, but we cannot rule out the possibility that the server sent a silent notification causing the launch. However, in the abnormal exit logs, we do print the launchOptions in application:didFinishLaunchingWithOptions:, but the value is empty. The documentation states that if it is a silent notification, applicationDidEnterBackground: will be called after application:didFinishLaunchingWithOptions:. We can confirm through the logs that this method was not called.
- Regarding app prewarm, I see that this was introduced after iOS 15. However, the surge in abnormal exits started in May of this year and occurred only after the application was updated to a certain version. The percentage of abnormal exits for older versions did not increase, which suggests it might not be strongly correlated with the iOS system. Therefore, we cannot determine whether the app prewarm caused the application to launch in the background.
I would like to ask if there is any method to ascertain if the recent launch was due to prewarm? Or is there any way to log specific API calls to determine the reason for the background launch?
Splitting in two because I write to much...
Our application has an SDK that detects whether the user last exited abnormally.
Have you looked at MetricKit? Frankly, the best way to monitor an apps activity is from out side the process, which is exactly what MetricKit does.
Simply put, if the app is quit from the foreground and does not receive the willTerminate callback, and there is no crash stack, it is deemed an abnormal exit.
Can you better define "quit from the foreground"? Do you mean "app ceased to execute without having received applicationDidEnterBackground"? Or something different? One side note here, if you're going to try and track something like this, I'd probably register an "atexit()" function as well as watching for "willTerminate". It's never really been documented, but I'm aware of at least one code path where UIApplication intentionally exits*, bypassing "willTerminate" and I won't promise there aren't others.
*The code path is part of state restoration and exist to help ensure that background launch don't invalidate previously saved state. More to the point, while it might seem like this would cause major problems, the reality is that the system has been doing this since ~iOS 3 and, as far as I've been able to tell, very few people (including myself) ever even noticed it was happening.
Finally, how are you determining that exit occurred at all? Having looked at issue like this in the past, I've seen many cases where the issue wasn't caused by what iOS itself was doing, but by how the data being collected was stored. Similarly,
As a slightly contrived example, if you start a background task and "didEnterBackground" and try to write data when the task expires, there are app configuration where that write will fail. If the target file is stored at NSDataProtectionComplete, it's possible for the device to completely lock before task expiration, at which point the file either cannot be written to or will be overwritten by the write attempt (depending on the API your using).
In May of this year, the reports of abnormal exits suddenly surged. Upon investigation, we found that the application was being launched in the background, and during the SDK initialization, it was set to a foreground state,
Note that the value returned returned by UIApplication.applicationState doesn't not necessarily match the ACTUAL state of the app and never has. As the simplest example of this, an app that enters the background is sent "applicationWillResignActive" and then "didEnterBackground". The value of applicationState matches that cycle, so state can't be "UIApplicationStateBackground" until your app was told it had "entered the background". However, from an interface perspective, your app was sent directly to the background and was never really "inactive". That could be a significant period of time, depending on how long your app spend in applicationWillResignActive.
As an aside, these issue are one of the major reasons the scene lifecycle flow was introduced. The scene lifecycle lets the system draw a clearer line between "your app is running" and "what parts of your app are visible".
Anyway, in practice UIApplication.applicationState is basically used to tell your app what state the system THINKS your app should "believe" itself to be, regardless of what it's real state actually is. Apps launched into the background start with UIApplicationStateActive because that's the best way to ensure they properly initialize their interface.
Our application's background modes only enable Audio, AirPlay, Picture in Picture, and Remote notifications, so the possibility of background fetch can be ruled out.
Do you become the "Now Playing" app? Now playing apps can also be launched into the background and (I think?) PiP support requires "Now Playing".
However, in the abnormal exit logs, we do print the launchOptions in application:didFinishLaunchingWithOptions:, but the value is empty. The documentation states that if it is a silent notification, applicationDidEnterBackground: will be called after application:didFinishLaunchingWithOptions:. We can confirm through the logs that this method was not called.
I'm sorry, but I don't think the documentation of this area is reliable. At one point (basically, when multi-tasking was introduced in iOS 4) it was fairly accurate but the system has become vastly more complicated over time. More specifically:
-
The number of background launching APIs has DRAMATICALLY increased over time, many of which don't require any specific permission or authorization (for example, background downloads, the Background Task framework, and responding to notifications).
-
The system has multiple mechanisms that can cause an app to be launched into the background. Prewarming is one variety of this but, for example, we also launch apps based on expected usage (for example, if you always use a app at 7am, we might launch the app at 6:50am so that it's "ready" for you) and that isn't necessarily the same as a "Prewarm" launch.
-
The evolution of the app lifecycle mean that the "correct" behavior isn't obvious. case in point, the "applicationDidEnterBackground" is part of the application delegate lifecycle, but the scene delegate system was introduced as it's "replacement" 5 years ago (iOS 13).
-
How EXACTLY the system behaves across ALL of these services varies between system versions and basically "always" has. Most developers never notice this variance because they either weren't using the feature that changed or the variation didn't actually alter how their app behaved (often both).
-
All of these factors combined together make saying "this is EXACTLY how the system does/should work"... very difficult.
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware
Part 2...
Regarding app prewarm, I see that this was introduced after iOS 15.
No, or at least not exactly.
The core functionality was implemented in iOS 13 (possibly earlier) and was being actively applied to 3rd party apps in iOS 14. What changed in iOS 15 was that the system started using prewarming more "broadly" and that shift the exposed problems for a number of apps.
The word "expose" is important there because I think it's important to understand that all of the major issues that have been tied to prewarming were actually caused by bug in other components. In terms of developer code, the primary factors/issues were:
-
Apps assuming that protected data would "always" be available and failing catastrophically if it wasn't.
-
Apps performing VERY large scale initialization at framework load time instead of waiting until app initialization had occurred.
That second issue in particular was (and still is) quite common. I was and remain stunned by the idea that ANY framework would create threads or initialize complex view objects (like "WKWebView") before main() has started executing, but that exactly the sort of thing that was interfering with Prewarming.
However, the surge in abnormal exits started in May of this year and occurred only after the application was updated to a certain version.
OK. What did you change in or near that version?
The percentage of abnormal exits for older versions did not increase, which suggests it might not be strongly correlated with the iOS system. Therefore, we cannot determine whether the app prewarm caused the application to launch in the background.
I would like to ask if there is any method to ascertain if the recent launch was due to prewarm?
Yes, but it won't really be all that useful. Prewarm adds the environment variable "ActivePrewarm" whenever it triggers a launch but that variable is then cleared later (basically, once the application is actually "working"). However, checking for that variable in main() will tell you whether prewarm is what originally created your process.
However, that tells you a lot less than you might think. Since late iOS 15, the Prewarm process suspends your app very early in the dyld load process, specifically before ANY 3rd party library (and most of our libraries) can be loaded. By design, your application cannot have ANY effect on that process. Indeed it's entirely possible for prewarm to have created your process minutes or even hours before ANY of your apps code actually executed. Prewarming is only what created your apps process, NOT the reason why your app is actually executing code.
Or is there any way to log specific API calls to determine the reason for the background launch?
With experience it's generally possible to determine why an app has been woken from the console log, but there isn't any specific API that will tell you "why".
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware
Thank you very much for your response!
Let me first describe the overall startup process of my application, how we determine abnormal exits, and how we identify background launches.
In application:didFinishLaunchingWithOptions:, we conduct the overall startup process of the application. We log every key path within this process, and we print the UIApplication.appState to determine the application’s foreground and background states at each key point. Additionally, we log many key paths during the user's interactions, which also include UIApplication.appState.
In various lifecycle APIs of AppDelegate, we also print logs to confirm the overall lifecycle of the application. Due to historical reasons, our application does not use SceneDelegate.
To determine abnormal exits, when application:didFinishLaunchingWithOptions: is called, we start listening for foreground and background notifications, UIApplicationWillTerminateNotification, and atexit. We only write a file named "normalExitFile" when receive UIApplicationDidEnterBackgroundNotification, UIApplicationWillTerminateNotification, or atexit.
When the application starts, we first check whether the "normalExitFile" exists and whether a crash occurred last time (we also write a file when we received crash signals). If both are absent, we conclude that the user had an abnormal exit last time. At this point, we report the user logs to the server, delete the "normalExitFile" to reset the application to its initial state, and then begin listening for the next lifecycle processes (foreground and background notifications, UIApplicationWillTerminateNotification, atexit).
Starting from May, the quantity of these abnormal log reports surged. We noticed that a small portion of the logs had appState aligning with our expectations, transitioning from UIApplicationStateInactive to UIApplicationStateActive, and the final logs were all UIApplicationStateActive, with no logs indicating a background transition (logs indicating transitions to the background also had corresponding logs for returning to the foreground). We consider these logs to be normal, indicating that the user experienced an abnormal exit.
However, for the majority of the other logs, the appState remained UIApplicationStateBackground throughout, with no UIApplicationStateInactive printed, or any other lifecycle log prints. This suggests that from the moment application:didFinishLaunchingWithOptions: was called, the UIApplication.appState has consistently been UIApplicationStateBackground.
From the logs, we can confirm that application:didFinishLaunchingWithOptions: was invoked. Particularly while determining whether it was a background launch, I added logs of user actions (whether user had clicked somewhere last time) and periodic logging (To check if the UIApplication.appState has change). For these background launch logs, it's clear that no user click events occurred at any point, and most periodic logs ceased around the 30-second mark. This led me to initially suspect silent notifications, as I read in the documentation that silent notifications provide the application with 30 seconds to process.
However, the logs printed in userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler: did not appear in the background launch logs, so can I rule out silent notifications as the cause?
Do you become the "Now Playing" app? Now playing apps can also be launched into the background and (I think?) PiP support requires "Now Playing".
We do use the Now Playing feature, but that was introduced in January last year, which doesn't align with the timing this May. Of course, it could be that there is increased usage of Now Playing in some business flows. Is there a way to determine if Now Playing triggered the background launch?
Or with more information I provided, do you have any other suspects? Thanks for your time.
And now, a tale in 4 parts...
Part 1, Being Suspended
To determine abnormal exits, when application:didFinishLaunchingWithOptions: is called, we start listening for foreground and background notifications, UIApplicationWillTerminateNotification, and atexit. We only write a file named "normalExitFile" when receive UIApplicationDidEnterBackgroundNotification, UIApplicationWillTerminateNotification, or atexit.
Three suggestions:
-
What we really need here is an "applicationWillSuspend" API/delegate, that would actually tell your app "we're going to suspend your app" without artificially extending your apps lifecycle (like a background task would). I'd appreciate it if you would file a enhancement request asking for this, then post the bug number back here.
-
It's not ideal, but beginBackgroundTask can basically be used to replicate the same behavior, with the main downside being that it can keep your app awake longer than it otherwise would. All you need to do is call beginBackgroundTask and you know your app will be suspending when the expiration handler is called. I'm not sure it's something I would recommend as a "general" technique under all circumstances, but I don't really see any issue with calling beginBackgroundTask when your app launches as a way to investigate whatever is going on here.
As a side comment on #2, many apps that operate in the background end up "accidentally" relying on the larger systems internal implementation instead of explicitly managing their own background operation. As a concrete example, many voip apps don't call beginBackgroundTask when the receive a voip push. They report a new call to CallKit and assume they're stay awake. This HAPPENS to work because the voip background category is very old (iOS 4/8) and it HAPPENS to keep the app awake for a pretty long time, long enough that the CallKit call itself is able to "take over" keeping the app awake. The EXACT same approach with our PushProvider API fails completely. PushProvider is much newer and does the right thing, which is to end it's own assertion as soon as it's finished it's work.
In any case, the right approach here (and this applies to basically ANY time your app runs in the background) is to call beginBackgroundTask in whatever API the system "notified" your app of activity in, then end that task when your app is done. This puts your app in control of it's lifecycle, instead of relying on whatever the system "happens" to do.
This also applies to app launches as well. I think it's reasonable for an app to decide "this is the work I do at startup" and then use beginBackgroundTask to insure that it's given an consistent amount of time to complete that work. Note that "unexpected early suspension" can be an issue even if you're dealing with a standard launch- it's pretty easy to launch and then background an app in less than a second.
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware
Part 2, On Crash Reporters
When the application starts, we first check whether the "normalExitFile" exists and whether a crash occurred last time (we also write a file when we received crash signals).
As a side note here, in process crash reporting in an inherently bad idea, as "Implementing Your Own Crash Reporter" tries to explain in a fair amount of detail. What is less obvious from that article is that all of the technical complexity that article describes ISN'T "What it takes to make a great crash reporter". That article is "What it takes to make a crash reporter that isn't fundamentally broken".
What makes these issues (and why I'm spending so many words on this...) so dangerous is that a crash reporter can actually ignore basically "all" of those requirements and still "work", in the sense that it is capable of collecting a significant subset of common crashes. The problem is that it will also fail quite badly on other kinds of crashes and, even worse, it's virtually CERTAIN to fail in a way that will prevent it from capturing any data about that failure. Odds are good , they'll (and you) may never know that anything was missed.
There are well known an highly visible crash reporters that do NOT meet the standard described by that article. I've seen crash logs with 10+ threads blocked in signal handling because the crash reporter hasn't properly suspended threads and is also crashing due to issue in the ObjC runtime... which it should never have been using at all. At least one crash reporter was going NETWORKING in their mach exception handler (something I hope they've since corrected). Note that the worst case here ISN'T "my crash log isn't helpful", it's that "I don't have any crash log at all".
Having worked on far to many of these issues, I've basically come to the conclusion that that these services tend to make "easy crashes easier to fix and really hard crashes impossible to fix". My perspective is admittedly skewed (I don't get asked to help with easy crashes), but that does not feel like a good tradeoff.
My recommendations here:
- Use MetricKit or a service* that's built on it. Whatever disadvantages it may have, the simple fact that it's crash reporter will NEVER break your app is an advantage that cannot be overstated.
*I'm not aware of any such service, but if one exists I'd love to hear about it.
- If you choose to use or write an in process crash reporter, it is essential that you test and validate it very, very well. Having looked at the crash data from many services, it's entirely possible for a well known and respected crash service to have a beautiful interface chock full of graphs and pretty analytics, but who's actual crash collection process is unreliable junk. This is not a complete list, but here are the things I would always look at:
-
No crash reporter is ever allowed to call "exit()" or "_exit". Ever. The only thing worse than a bad crash log is no crash log at all.
-
The crash data it presents should show it's own activity inside your process. Many services hide them because they "aren't useful" and that's entirely true... until it isn't.
-
You need to validate the crash reporters own implementation, particularly around using a high level language like ObjC or Swift. Those simply cannot be used in during the crash collection process under any circumstances.
-
Be afraid of "better" and don't assume that a useful feature is a good idea. For example, most crash reporters work by writing the crash data to a files that was prepared in advance, then uploading that data the next time the app runs. It's entirely possible to write a crash reporter that "immediately" reports data, just write a bunch of network code that runs in your mach exception/signal handler. I hope it's obvious what a truly terrible idea that is.
-
Only have one. Including multiple crash reporters means that you're risking ALL of the issues above, PLUS the additional complexity created by their own interactions with each other. Again, this is a case where they'll often work "fine"... except when they don't.
- Look at Apple's crash logs. Crash reporters should always ensure that they allow our reporter to generate crashes. The reports for the same crash should be "broadly" similar. If they're not, that's a problem. More to the point, make sure you look at the crash logs we've collected for your shipping app. A lot of the time I've spent looking at 3rd party crash reporters started with the question "What are all these extra crashes Xcode is showing?". As you might expect, Xcode is never the issue here. If we're showing crashes that they don't have (or vice versa), then that's an issues that needs to be looked at VERY closely.
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware
Part 3, Getting things done with daemons
If both are absent, we conclude that the user had an abnormal exit last time.
In concrete terms, I can only think of three cases which would match what you're describing:
-
The app launched into the background, performed some work, and then suspended normally. This is entirely possible and not necessarily unusual.
-
The app actually crashed and your collector failed. Also possible, with the likelihood depending on the quality of the collection process.
-
Something called _exit and I don't think it was us. Strictly speaking, UIKit does call "_exit", but that's specifically at the end of the same method that delivers applicationWillTerminate (which you're also checking for).
Moving to here:
This suggests that from the moment application:didFinishLaunchingWithOptions: was called, the UIApplication.appState has consistently been UIApplicationStateBackground.
To state this clear, this means your app was in the background. There are cases where the system may/has returned "foreground" for an app what wasn't visible, but I'm not aware of any case where a foreground app would see "background".
For these background launch logs, it's clear that no user click events occurred at any point, and most periodic logs ceased around the 30-second mark. This led me to initially suspect silent notifications, as I read in the documentation that silent notifications provide the application with 30 seconds to process.
Note that 30s is basically the "default" that was used by most our original background APIs, so you'd actually see very similar behavior if there was an issue with many of our background APIs, even if that isn't behavior isn't actually documented in the API (see below for more detail on why that is).
However, the logs printed in userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler: did not appear in the background launch logs, so can I rule out silent notifications as the cause?
Maybe...
Does your application call "registerForRemoteNotifications" in didFinishLaunchingWithOptions? Did you also set up your UNUserNotificationCenterDelegate? And did you receive "didRegisterForRemoteNotificationsWithDeviceToken" (or didFail...)? Architecturally, when a background API launches an app, the cycle works like this:
-
Daemon launches app and takes background assertion which will keep it awake.
-
Daemon waits for XPC connection from app.
-
App launches and calls whatever API establishes the relevant XPC connection (see questions above).
-
Daemon delivers data through the XPC connection, which the framework then calls your delegate methods with.
The standard assertion timeout on #1 is 30 seconds, so if an app doesn't handle #3 correctly, it will automatically be suspended ~30s after it was launched/woken.
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware
Part 4, Now Playing, A New Hope
Is there a way to determine if Now Playing triggered the background launch?
First, a disclaimer. I'm only involved with audio in a relatively indirect way (through voip/CallKit). Because of that, I know a fair amount about how it's internals work but I've actually spent very little time working directly with our actual audio APIs
Returning to the question, no, I'm not aware of any "official" way to do this. Similarly, I'm not exactly sure of the exactly how NowPlaying apps are expected to "restore" themselves or even if/where it was documented*. If you want to follow up on the details of that process, please post a new forum post that's specifically focused on that piece.
*And, yes, you should file a bug about that.
Having said all that, you can actually confirm and test the basic behavior fairly easily. Here is what I did, starting with our "Becoming a now playable app" sample app:
- Modify the sample to automatically activate NowPlaying when launched by modifying
updateConfig
inIOSConfigViewController.m:
func updateConfig() {
guard let tableViewController = children.first as? IOSConfigTableViewController else { return }
tableViewController.updateConfig()
self.optIn(nil)
}
-
Load the app on to your phone with Xcode, then stop the app (so you'll start with new launch the next time you run).
-
Go to
Settings.app-> Developer-> STATE RESTORATION TESTING
and switch"Fast App Termination"
to "On". Switching this setting will make the system terminate apps that are sent to the background at the point they would normally terminate. In this case, it's an easy way to replicate a "normal" system termination without using extra code. -
Launch the app, listen to the fine music for a bit, then lock your device.
-
Hit Pause on the lock screen
-
Watch the Now Playing information on the lock screen and you'll the see information reset when the system exits the app. After that point, hit play.
-
Continue enjoying the music.
Note that the relaunch process here can be a lot more robust than it might appear. For example, the Now Playing UI in #6 goes away pretty quickly after playback but that specific UI isn't what specifically triggers the launch. Hitting play from control center or from and external command source (like headphones) trigger exactly the same process.
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware
Thanks again~
What we really need here is an "applicationWillSuspend" API/delegate, that would actually tell your app "we're going to suspend your app" without artificially extending your apps lifecycle (like a background task would). I'd appreciate it if you would file a enhancement request asking for this, then post the bug number back here.
I have file a feedback regarding the suggestion for an "applicationWillSuspend" API, and please let me know if there are any inaccuracies in my feedback.
Use MetricKit or a service* that's built on it. Whatever disadvantages it may have, the simple fact that it's crash reporter will NEVER break your app is an advantage that cannot be overstated.
Currently, our application utilizes MetricKit, including diagnostics for disk, CPU, crashes, etc., and we do upload the corresponding information to our server.
Today I looked into MetricKit and discovered that starting from iOS 16, it added MXCallStackTree in MXAppLaunchDiagnostic. Could it possibly help in understanding the application's launch reason?
A breakthrough discovery!
When examining the logs for background launches, I found that some logs contain launchOptions though just a few, which include UIApplicationLaunchOptionsURLSessionKey. However, I couldn't find the meaning of this key in UIApplication.h. The only clue I found was in this forum thread. If you could provide more information on this, it would be greatly appreciated.
Part 4, Now Playing, A New Hope
Regarding the Now Playing suggestion, I found that switching "Fast App Termination" to "On" didn't work. When I put the app into the background and then reopen it, it does not restart, so I couldn't really test that case.