Thank you for the clarification about the 24 MB memory limit and jetsam-based termination for Notification Service Extensions. I wanted to share two specific behaviors I'm observing in our extension related to heavy CPU work, because both scenarios produce different outcomes.
Scenario 1 — Heavy CPU loop on a background thread
DispatchQueue.global(qos: .userInitiated).async {
var sum: Int = 0
for i in 0..<5_000_000_000 {
sum += i
if i % 100_000_000 == 0 {
Log(String(format: "Scenario: Progress i=%d", i))
}
if !NotificationService.shoudLoop { break }
}
NotificationService.notificationContent!.title = "[Scenario Finished]"
NotificationService.notificationContent!.body = "CPU-intensive work done"
NotificationService.notificationContentHandler!(NotificationService.notificationContent!)
}
Observation:
In this case, the Notification Service Extension stays alive long enough for didReceive(_:withContentHandler:) to complete. The notification text is successfully decrypted and displayed. As for the logs, of the Progress of the loop on the background thread, it gets executed till around i=100_000_000 and after that no logs are printed for the loop, as process gets terminated.
Scenario 2 — The same CPU loop but on the main thread
Log("Scenario 2A: Blocking main thread in didReceive")
var sum = 0
for i in 0..<5_000_000_000 {
sum += i
if i % 100_000_000 == 0 {
Log(String(format: "Scenario 2A: Progress i=%d", i))
}
if !NotificationService.shoudLoop { break }
}
NotificationService.notificationContent!.title = "[Scenario 2 on main thread Finished]"
NotificationService.notificationContent!.body = "CPU-intensive work done"
NotificationService.notificationContentHandler!(NotificationService.notificationContent!)
Observation:
When this same long-running loop runs directly on the main thread, the extension becomes unresponsive. The system logs show messages such as:
tearing down context in extension due to invalidation [xpcservice...]
[xpcservice<world.tally.IOSPushNotifications.NotificationServiceExtension([osservice<com.apple.SpringBoard>:34])>{vt hash: 0}:1962] Set jetsam priority to 0 [0] flag[1]
The extension is terminated immediately, and the notification appears with the original encrypted payload — meaning didReceive never got a chance to complete. The expiry callback (serviceExtensionTimeWillExpire()) is also never invoked.
Could you help explain the reason behind the different behaviours when the work runs on the main thread versus a background thread? Are there any recommended best practices for what type of work should (or should not) run on the main thread inside a Notification Service Extension?
Also, if an extension needs to perform some initialisation work, are there guidelines on where that work should be placed to avoid premature termination?
Topic:
App & System Services
SubTopic:
Notifications
Tags: