1

Notification service extension (NSE) allows to process notifications in iOS app before showing them to the user (e.g., for message decryption):

class NotificationService: UNNotificationServiceExtension {
    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        // call content handler here when ready
    }

    override func serviceExtensionTimeWillExpire() {
        // call content handler here with the best attempt
    }
}

As I understand, for each notification a new instance of NotificationService is created, but all these instances can be in the same process that may share access to the database and other resources - iOS doesn't start a new NSE process for each new notification.

At some point iOS kills NSE process, and if database connection is not properly released, it results in the exception with 0xdead10cc exit code ("dead lock") that is visible to the users as an alert about the crash of the app in the background.

Is there a way to determine when iOS is about to kill NSE process to release the resources?

esp
  • 7,314
  • 6
  • 49
  • 79

1 Answers1

1

From your description, you make it sound as if you are thinking there can be several instances of the NSE active at once? That's not what happens, if the handset receives multiple pushes simultaneously, the OS keeps them in a queue and launches then closes, launches then closes, etc. the extension.

The extension gets terminated when you call contentHandler() to display the notification, or if you don't call than then after about 30 seconds the OS calls serviceExtensionTimeWillExpire(). In both cases the extension will be terminated (and if while it was running, the phone had received another push, then it'll be launched again)

Gruntcakes
  • 37,738
  • 44
  • 184
  • 378
  • Thank you! Not sure if there can be concurrent instances of NotificationService class - you may be right that they are created sequentially, I didn't say either. I am certain that they are created in the same system process. serviceExtensionTimeWillExpire is only called If I don't call content handler in time. if it is called, it doesn't mean NSE process is terminated, as the next notification can be processed in the same system process (so it won't have to reconnect to database, network, etc.). So the question is if it's possible to determine when this system process is about to terminate. – esp Jul 20 '23 at 18:20
  • If you establish db or network connections in one invocation of the extension, they won't be their next invocation, so you need to establish them each time at the start of didReceive. Calling the content hander or serviceExtensionTimeWillExpire is the answer to your question. Very very shortly after those two events the extension will have gone and your db connections and outstanding network calls etc. no longer valid/active. You could add some logging to an extension then fire several pushes to your app and observe the logging in the phone's console, you'll be able to observe its lifetime. – Gruntcakes Jul 20 '23 at 18:32
  • Running in the same system process doesn't mean you won't have to reconnect to a database etc. If you have a process then create an object within that process, then delete the object, then create it again, then there's no persistence of things used within that object from one invocation to the next. – Gruntcakes Jul 20 '23 at 18:35
  • > If you establish db or network connections in one invocation of the extension, they won't be their next invocation, so you need to establish them each time at the start of didReceive. This is not true, and it would be very inefficient. You certainly can re-use the same connections between invocations - this is what both Signal and our app does (https://simplex.chat). You just need to keep handles to these connections in the global state of the process, outside of NotificationService class instance, and they will be available. – esp Jul 20 '23 at 22:05
  • "there's no persistence of things used within that object from one invocation to the next." Yes, there is, if you put the handles in the global process state. – esp Jul 20 '23 at 22:08
  • "if you put the handles in the global process state" How would you do that? – Gruntcakes Jul 21 '23 at 13:21
  • This is our NSE: https://github.com/simplex-chat/simplex-chat/blob/stable/apps/ios/SimpleX%20NSE/NotificationService.swift It depends on SimpleXChat framework (that is shared between NSE and app) which puts the handle to the global state when chat core is started: https://github.com/simplex-chat/simplex-chat/blob/stable/apps/ios/SimpleXChat/API.swift#L11 – esp Jul 21 '23 at 17:13
  • In general, you can always put something in a global lazily (or non-lazily) initialised variable, it doesn't have to be inside the class. Like this variable, for example: https://github.com/simplex-chat/simplex-chat/blob/stable/apps/ios/SimpleX%20NSE/NotificationService.swift#L15 This logger is not created on every notification, it persists between invocations as long as they happen in the same process. – esp Jul 21 '23 at 17:16
  • So the question is how to detect when this process is about to be terminated, to clean up the resources. – esp Jul 21 '23 at 17:18