Top causes of app termination:
Crashes
CPU Resources limit
Watchdog
Memory limit exceeded
Memory pressure exit
Background task timeout
New MetricKit API
MXBackgroundExitData helps us understand why our app is getting killed by providing the exit count every time app is getting terminated:
- cumulativeBadAccessExitCount
- cumulativeIllegalInstructionExitCount
- cumulativeAbnormalExitCount
- cumulativeCPUResourceLimitExitCount
- cumulativeMemoryResourceLimitExitCount
- cumulativeMemoryPressureExitCount
- cumulativeSuspendedwithLockedFileExitCount
- cumulativeAppWatchdogExitCount
- cumulativeBackgroundTaskAssertionTimeoutExitCount
- cumulativeNormalExitCount Crashes
Crash is the most straightforward type of terminations. Crash may happen because of these 3 reasons:
Segmentation fault
Illegal Instruction
Asserts and uncaught exceptions
These event will be generated on crashlog and the crash will report to us automatically. In addition of the Xcode organizer, MetricKit add more APIs for diagnostic on a per device basis which is MXCrashDiagnostic
MXCrashDiagnostic will provide:
StackTrace
Signal
Exception Code
Termination Reason
Watchdog
Another termination happen because of Watchdog event that happen because of timeout during some key transitions.
Timeout during key transitions: long hang during app key transition such as launch, going background or foreground again. It has time limit around 20 seconds during transition.
Disabled in Simulator and in the debugger
Fixing watchdog event will help to eliminate deadlocks, infinite loop, and unending synchronous work on the main thread
Report available in
MXCrashDiagnostic
CPU Resource limit
CPU Resource limit is indicating the high sustained CPU load in background Addition in Xcode 12 there will be solution:
Energy Exception Report through Xcode organizer and
MXCPUExceptionDiagnosticCall stack point out hotspots in code
Consider moving work into
BGProcessingTask
Memory footprint exceeded
App using too much memory. Some solutions:
Same limit for foreground and background
Use instruments and memory debugger
Keep in mind limitation for older devices
Jetsam (Memory pressure exit)
This is not a bug in our app, and it is most common termination.
System freeing up memory for active applications
How to reduce jetsam rate?
Aim for less than 50 MB in the background
Upon backgrounding:
Flush state to disk
Clear out image view
Drop caches
There are also suggestion for recovering from jetsam:
Save state upon entering background such as view controller stack, draft input in text fields, media playback position and many more depend on your use case
Adopt UIKit State Restoration
Remember users should not realize that the app was terminated
Background task timeout
We can use beginBackgroundTask and endBackgroundTask for executing background task when going to background (Remember we have 30 seconds before app is suspended)
UIApplication.beginBackgroundTask(expirationHandler:)
UIApplication.endBackgroundTask(_:)The problem is when we don’t call
endBackgroundTask, it will lead to failure to end task explicitly result in termination. However it is preventableCounts exposed via
MXBackgroundExitData
We should use the name variant of UIKit API to prevent the termination happen
UIApplication.beginBackgroundTask(withName:expirationHandler:)Why?
The terminations do not occur in debugger
After that we can do console message and do an audit of our call to matching background and end task process
Another solution is using expirationHandler
Implement an
expirationHandleras safety net and do not rely it exclusivelyCall
endBackgroundTaskinside handlerDo not begin new work inside handler
Add telemetry at the start and each of each expiration handler
let handle = MXMetricManager.makeLogHandle(category: "DatabaseExpHandler")
mxSignpost(.event, log: handle, name: "Entered")
cancelOperations()
closeDatabase()
mxSignpost(.event, log: handle, name: "Exited")
UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)Now let us inspect MXMetricPayload to see the signpost count and check any imbalance of the signpost count
"signpostMetrics": [
{
"signpostCategory": "DatabaseExpirationHandler",
"signpostName": "Entered",
"totalSignpostCount": 2
},
{
"signpostCategory": "DatabaseExpirationHandler",
"signpostName": "Exited",
"totalSignpostCount": 1
}
]Another solutions to improve debugging of app terminations is checking backgroundTimeRemaining before doing some background work
Only start work if plenty of time remains
Unsafe to begin task with < 5 seconds remaining
Example code:
let minimumTimeRemaining = min(5, estimateProcessingTime(inputData))
if UIApplication.shared.backgroundTimeRemaining > minimumTimeRemaining {
// Enough time remains, call begin background task
return UIApplication.shared.beginBackgroundTask { ... }
} else {
// Not enough time remains, defer this work until later
registerProcessingTask(inputData)
return .invalid
}The next thing we need to do also is avoiding leaking UIBackgroundTaskIdentifier. Use local variable instead of an instance variable to hold UIBackgroundTaskIdentifier and it will preventing the leak since it will allocated on different memory
Example code:
@IBAction func beginDataExport(sender: UIButton) {
var taskId: UIBackgroundTaskIdentifier = .invalid
taskId = UIApplication.shared.beginBackgroundTask {...}
// End the background task after archiving, which takes several seconds
ArchiveUtility.exportUserData(completion: ()->()) {
UIApplication.shared.endBackgroundTask(taskId)
}
}Finally we learn about some solutions to reduce termination:
Identify and fix termination
Reduce memory usage
Implement state restoration
