Articles

XPC-Dienste in macOS-App

Lê Điền Phúc
Lê Điền Phúc

Follow

Sep 4, 2020 – 9 min read

Vor XPC haben wir uns mit Sockets und Mach-Nachrichten (Mach Ports) beschäftigt.

Der XPC-Mechanismus bietet eine Alternative zu Sockets (oder Mach Services mit MIG) für IPC. Wir könnten zum Beispiel einen Prozess haben, der als „Server“ fungiert und darauf wartet, dass Clients auf seine API zugreifen und einen Dienst bereitstellen.

XPC-Dienste in Anwendungen

Wenn wir über XPC-Dienste (großes ‚S‘) sprechen, beziehen wir uns auf das Bundle namens XPC-Dienst. Bundles im Apple-Ökosystem beziehen sich auf Entitäten, die durch eine bestimmte Verzeichnisstruktur repräsentiert werden. Das am häufigsten anzutreffende Bundle sind Anwendungs-Bundles. Wenn Sie mit der rechten Maustaste auf eine beliebige Anwendung (z. B. Chess.app) klicken und Inhalt anzeigen wählen, finden Sie eine Verzeichnisstruktur. Zurück zu XPC, Anwendungen können mehrere XPC Service Bundles haben. Sie finden sie im Verzeichnis Contents/XPCServices/ innerhalb des Anwendungspakets. Du kannst in deinem /Applications Verzeichnis suchen und sehen, wie viele der Anwendungen auf XPC Services angewiesen sind.

Sie können auch XPC Services innerhalb von Frameworks haben (welche eine andere Art von Bundle sind).

Zusätzliche Vorteile von XPC Services

Die Verwendung von XPC Services in unseren Anwendungen ermöglicht es uns, einige Funktionen in separate Module aufzuteilen (Der XPC Service). Wir könnten einen XPC Service erstellen, der für die Ausführung einiger kostspieliger, aber seltener Aufgaben zuständig ist. Zum Beispiel eine Krypto-Aufgabe zur Generierung von Zufallszahlen.

Ein weiterer Vorteil ist, dass der XPC-Dienst in einem eigenen Prozess läuft. Wenn dieser Prozess abstürzt oder beendet wird, hat das keine Auswirkungen auf unsere Hauptanwendung. Stellen Sie sich vor, dass Ihre Anwendung benutzerdefinierte Plugins unterstützt. Und die Plugins werden mit XPC Services erstellt. Wenn sie schlecht kodiert sind und abstürzen, beeinträchtigen sie nicht die Integrität Ihrer Hauptanwendung.

Ein weiterer Vorteil der XPC Services ist, dass sie ihre eigenen Berechtigungen haben können. Die Anwendung benötigt die Berechtigung nur, wenn sie einen vom XPC Service bereitgestellten Dienst nutzt, der die Berechtigung benötigt. Stellen Sie sich vor, Sie haben eine Anwendung, die den Standort nutzt, aber nur für bestimmte Funktionen. Sie könnten diese Funktionen in einen XPC Service verschieben und die Standortberechtigung nur diesem XPC Service hinzufügen. Wenn Ihr Benutzer die Funktion, die den Standort verwendet, nie braucht, wird er nicht nach Berechtigungen gefragt, was die Nutzung Ihrer Anwendung vertrauenswürdiger macht.

XPC und unser Freund launchd

launchd ist der erste Prozess, der auf unserem System läuft. Er ist dafür zuständig, andere Prozesse, Dienste und Daemons zu starten und zu verwalten. launchd ist auch für die Planung von Aufgaben zuständig. Es macht also Sinn, dass launchd auch für die Verwaltung der XPC-Dienste zuständig ist.

XPC-Dienste können gestoppt werden, wenn sie lange Zeit inaktiv waren, oder sie können bei Bedarf gestartet werden. Die gesamte Verwaltung wird von launchd erledigt, und wir müssen nichts tun, damit es funktioniert.

launchd hat Informationen über die systemweite Ressourcenverfügbarkeit und Speicherbelastung, wer könnte besser als launchd Entscheidungen darüber treffen, wie die Ressourcen unseres Systems am effektivsten genutzt werden

Implementieren von XPC-Diensten

Ein XPC-Dienst ist ein Bündel im Verzeichnis Contents/XPCServices des Hauptanwendungsbündels; das XPC-Dienst-Bündel enthält eine Info.plist-Datei, eine ausführbare Datei und alle vom Dienst benötigten Ressourcen. Der XPC-Dienst gibt an, welche Funktion aufgerufen werden soll, wenn der Dienst Nachrichten empfängt, indem er xpc_main(3) Mac OS X Developer Tools Manual Page von seiner Hauptfunktion aus aufruft.

Um einen XPC-Dienst in Xcode zu erstellen, gehen Sie wie folgt vor:

  1. Fügen Sie Ihrem Projekt ein neues Ziel hinzu und verwenden Sie dabei die Vorlage XPC-Dienst.
  2. Fügen Sie den Build-Einstellungen Ihrer Anwendung eine Phase „Dateien kopieren“ hinzu, die den XPC-Dienst in das Verzeichnis „Contents/XPCServices“ des Hauptanwendungsbündels kopiert.
  3. Fügen Sie den Build-Einstellungen Ihrer Anwendung eine Abhängigkeit hinzu, um anzugeben, dass sie von dem XPC-Dienstbündel abhängt.
  4. Wenn Sie einen XPC-Dienst auf niedriger Ebene (C-basiert) schreiben, implementieren Sie eine minimale Hauptfunktion, um Ihren Event-Handler zu registrieren, wie im folgenden Code-Listing gezeigt. Ersetzen Sie my_event_handler durch den Namen Ihrer Event-Handler-Funktion.
int main(int argc, const char *argv) {
xpc_main(my_event_handler);
// The xpc_main() function never returns.
exit(EXIT_FAILURE);
}

Wenn Sie einen High-Level-Dienst (Objective-C-basiert) mit NSXPCConnection schreiben, erstellen Sie zunächst eine Verbindungsdelegate-Klasse, die dem NSXPCListenerDelegate-Protokoll entspricht. Implementieren Sie dann eine minimale Hauptfunktion, die ein Listener-Objekt erstellt und konfiguriert, wie im folgenden Code-Listing gezeigt.

int main(int argc, const char *argv) {
MyDelegateClass *myDelegate = ...
NSXPCListener *listener =
;
listener.delegate = myDelegate;
;
// The resume method never returns.
exit(EXIT_FAILURE);
}

Verwenden des Dienstes

Die Art und Weise, wie Sie einen XPC-Dienst verwenden, hängt davon ab, ob Sie mit der C-API (XPC-Dienste) oder der Objective-C-API (NSXPCConnection) arbeiten.

Verwendung der Objective-C NSXPCConnection API Die Objective-C NSXPCConnection API bietet eine High-Level-Remote-Procedure-Call-Schnittstelle, die es Ihnen ermöglicht, Methoden auf Objekten in einem Prozess von einem anderen Prozess aus aufzurufen (normalerweise eine Anwendung, die eine Methode in einem XPC-Dienst aufruft). Die NSXPCConnection-API serialisiert Datenstrukturen und Objekte automatisch für die Übertragung und deserialisiert sie am anderen Ende. Infolgedessen verhält sich der Aufruf einer Methode auf einem entfernten Objekt ähnlich wie der Aufruf einer Methode auf einem lokalen Objekt.

Um die NSXPCConnection API zu verwenden, müssen Sie Folgendes erstellen:

  • Eine Schnittstelle. Diese besteht hauptsächlich aus einem Protokoll, das beschreibt, welche Methoden vom entfernten Prozess aufrufbar sein sollen. Dies ist beschrieben in Designing an Interface
  • Ein Verbindungsobjekt auf beiden Seiten. Auf der Seite des Dienstes wurde dies bereits unter Erstellen des Dienstes beschrieben. Auf der Client-Seite wird dies in Verbindung mit einer Schnittstelle herstellen und verwenden
  • Ein Listener beschrieben. Dieser Code im XPC-Dienst nimmt Verbindungen an. Dies wird unter Akzeptieren einer Verbindung im Helper beschrieben. Messages.

Gesamtarchitektur

Gesamtarchitektur

Bei der Arbeit mit NSXPCConnection-basierten Helper-Apps verfügen sowohl die Hauptanwendung als auch der Helper über eine Instanz von NSXPCConnection. Die Hauptanwendung erstellt ihr Verbindungsobjekt selbst, woraufhin die Hilfsanwendung gestartet wird. Einer Delegatenmethode in der Hilfsanwendung wird ihr Verbindungsobjekt übergeben, wenn die Verbindung hergestellt wird. Dies ist in Abbildung 4-1 dargestellt.

Jedes NSXPCConnection-Objekt bietet drei Hauptmerkmale:

  • Eine exportedInterface-Eigenschaft, die die Methoden beschreibt, die der anderen Seite der Verbindung zur Verfügung gestellt werden sollen.
  • Eine exportedObject-Eigenschaft, die ein lokales Objekt enthält, um Methodenaufrufe zu behandeln, die von der anderen Seite der Verbindung kommen.
  • Die Fähigkeit, ein Proxy-Objekt für den Aufruf von Methoden auf der anderen Seite der Verbindung zu erhalten.

Wenn die Hauptanwendung eine Methode auf einem Proxy-Objekt aufruft, ruft das NSXPCConnection-Objekt des XPC-Dienstes diese Methode auf dem Objekt auf, das in seiner Eigenschaft exportedObject gespeichert ist.

Gleichermaßen, wenn der XPC-Dienst ein Proxy-Objekt erhält und eine Methode auf diesem Objekt aufruft, ruft das NSXPCConnection-Objekt der Hauptanwendung diese Methode auf dem Objekt auf, das in seiner exportedObject-Eigenschaft gespeichert ist

Designing an Interface

Die NSXPCConnection-API nutzt Objective-C-Protokolle, um die programmatische Schnittstelle zwischen der aufrufenden Anwendung und dem Dienst zu definieren. Jede Instanzmethode, die Sie von der anderen Seite einer Verbindung aufrufen möchten, muss explizit in einem formalen Protokoll definiert werden. Zum Beispiel:

@protocol FeedMeACookie
- (void)feedMeACookie: (Cookie *)cookie;
@end

Da die Kommunikation über XPC asynchron ist, müssen alle Methoden im Protokoll den Rückgabetyp void haben. Wenn Sie Daten zurückgeben müssen, können Sie einen Antwortblock wie folgt definieren:

@protocol FeedMeAWatermelon
- (void)feedMeAWatermelon: (Watermelon *)watermelon
reply:(void (^)(Rind *))reply;
@end

Eine Methode kann nur einen Antwortblock haben. Da die Verbindungen jedoch bidirektional sind, kann der XPC Service Helper auch antworten, indem er Methoden in der von der Hauptanwendung bereitgestellten Schnittstelle aufruft, falls gewünscht.

Jede Methode muss einen Rückgabetyp von void haben, und alle Parameter für Methoden oder Antwortblöcke müssen entweder sein:

  • Arithmetische Typen (int, char, float, double, uint64_t, NSUInteger, und so weiter)
  • BOOL
  • C-Strings
  • C-Strukturen und Arrays, die nur die oben aufgeführten Typen enthalten
  • Objective-C-Objekte, die das NSSecureCoding-Protokoll implementieren.

Wichtig: Wenn eine Methode (oder ihr Antwortblock) Parameter hat, die Objective-C-Auflistungsklassen (NSDictionary, NSArray usw.) sind, und wenn Sie Ihre eigenen benutzerdefinierten Objekte innerhalb einer Auflistung übergeben müssen, müssen Sie XPC explizit sagen, dass diese Klasse als Mitglied dieses Auflistungsparameters zulässig ist.

Verbinden mit und Verwenden einer Schnittstelle

Wenn Sie das Protokoll definiert haben, müssen Sie ein Schnittstellenobjekt erstellen, das es beschreibt. Rufen Sie dazu die Methode interfaceWithProtocol: in der Klasse NSXPCInterface auf. Beispiel:

NSXPCInterface *myCookieInterface =
;

Nachdem Sie das Schnittstellenobjekt erstellt haben, müssen Sie innerhalb der Hauptanwendung eine Verbindung mit diesem Objekt konfigurieren, indem Sie die Methode initWithServiceName: aufrufen. Zum Beispiel:

NSXPCConnection *myConnection = 
initWithServiceName:@"com.example.monster"];
myConnection.remoteObjectInterface = myCookieInterface;
;

Hinweis: Für die Kommunikation mit XPC-Diensten außerhalb Ihres App-Bundles können Sie eine XPC-Verbindung auch mit der Methode initWithMachServiceName: konfigurieren.

Zu diesem Zeitpunkt kann die Hauptanwendung die Methoden remoteObjectProxy oder remoteObjectProxyWithErrorHandler: auf dem myConnection-Objekt aufrufen, um ein Proxy-Objekt zu erhalten.

Dieses Objekt fungiert als Proxy für das Objekt, das der XPC-Dienst als sein exportiertes Objekt festgelegt hat (durch Setzen der Eigenschaft exportedObject). Dieses Objekt muss dem durch die Eigenschaft remoteObjectInterface definierten Protokoll entsprechen.

Wenn Ihre Anwendung eine Methode auf dem Proxy-Objekt aufruft, wird die entsprechende Methode auf dem exportierten Objekt innerhalb des XPC-Dienstes aufgerufen. Wenn die Methode des Dienstes den Antwortblock aufruft, werden die Parameterwerte serialisiert und an die Anwendung zurückgeschickt, wo die Parameterwerte deserialisiert und an den Antwortblock übergeben werden. (Der Antwortblock wird im Adressraum der Anwendung ausgeführt.)

Hinweis: Wenn Sie dem Hilfsprozess erlauben möchten, Methoden für ein Objekt in Ihrer Anwendung aufzurufen, müssen Sie die Eigenschaften exportedInterface und exportedObject vor dem Aufruf von resume festlegen. Diese Eigenschaften werden im nächsten Abschnitt näher beschrieben.

Annehmen einer Verbindung im Helfer

Wenn ein NSXPCConnection-basierter Helfer die erste Nachricht von einer Verbindung empfängt, wird die Methode listener:shouldAcceptNewConnection: des Listener-Delegaten mit einem Listener-Objekt und einem Verbindungsobjekt aufgerufen. Diese Methode lässt Sie entscheiden, ob Sie die Verbindung akzeptieren oder nicht; sie sollte YES zurückgeben, um die Verbindung zu akzeptieren, oder NO, um die Verbindung abzulehnen.

Hinweis: Der Helfer empfängt eine Verbindungsanforderung, wenn die erste aktuelle Nachricht gesendet wird. Die Resume-Methode des Verbindungsobjekts bewirkt nicht, dass eine Nachricht gesendet wird.

Zusätzlich zu den Richtlinienentscheidungen muss diese Methode das Verbindungsobjekt konfigurieren. Insbesondere, wenn der Helfer beschließt, die Verbindung zu akzeptieren, muss er die folgenden Eigenschaften für die Verbindung festlegen:

  • exportedInterface – ein Schnittstellenobjekt, das das Protokoll für das zu exportierende Objekt beschreibt. (Das Erstellen dieses Objekts wurde bereits in Verbindung mit einer Schnittstelle und deren Verwendung beschrieben.)
  • exportedObject – das lokale Objekt (normalerweise im Helper), an das die Methodenaufrufe des Remote-Clients übergeben werden sollen. Wann immer das andere Ende der Verbindung (normalerweise in der Anwendung) eine Methode auf dem Proxy-Objekt der Verbindung aufruft, wird die entsprechende Methode auf dem Objekt aufgerufen, das durch die Eigenschaft exportedObject angegeben ist.

Nach dem Einstellen dieser Eigenschaften sollte es die Resume-Methode des Verbindungsobjekts aufrufen, bevor es YES zurückgibt. Obwohl der Delegat den Aufruf von resume aufschieben kann, empfängt die Verbindung keine Nachrichten, bis er dies tut.

Nachrichten senden

Das Senden von Nachrichten mit NSXPC ist so einfach wie ein Methodenaufruf. Wenn Sie beispielsweise die Schnittstelle myCookieInterface (beschrieben in den vorherigen Abschnitten) auf dem XPC-Verbindungsobjekt myConnection haben, können Sie die Methode feedMeACookie wie folgt aufrufen:

Cookie *myCookie = ...
feedMeACookie: myCookie];

Wenn Sie diese Methode aufrufen, wird die entsprechende Methode im XPC-Helper automatisch aufgerufen. Diese Methode wiederum könnte das Verbindungsobjekt des XPC-Helpers auf ähnliche Weise verwenden, um eine Methode auf dem von der Hauptanwendung exportierten Objekt aufzurufen.

Fehlerbehandlung

Zusätzlich zu allen Fehlerbehandlungsmethoden, die für die Aufgabe eines bestimmten Helfers spezifisch sind, sollten sowohl der XPC-Dienst als auch die Hauptanwendung auch die folgenden XPC-Fehlerbehandlungsblöcke bereitstellen:

  • Unterbrechungshandler – wird aufgerufen, wenn der Prozess am anderen Ende der Verbindung abgestürzt ist oder seine Verbindung anderweitig geschlossen hat. Das lokale Verbindungsobjekt ist in der Regel noch gültig – jeder zukünftige Aufruf wird automatisch eine neue Helferinstanz erzeugen, es sei denn, es ist unmöglich, dies zu tun – aber Sie müssen möglicherweise jeden Zustand zurücksetzen, den der Helfer sonst behalten hätte.

Der Handler wird in derselben Warteschlange wie Antwortnachrichten und andere Handler aufgerufen und wird immer nach allen anderen Nachrichten oder Antwortblock-Handlern ausgeführt (außer dem Invalidierungs-Handler). Es ist sicher, von einem Unterbrechungs-Handler aus neue Anforderungen an die Verbindung zu stellen.

  • Invalidierungs-Handler – wird aufgerufen, wenn die invalidate-Methode aufgerufen wird oder wenn ein XPC-Helper nicht gestartet werden konnte. Wenn dieser Handler aufgerufen wird, ist das lokale Verbindungsobjekt nicht mehr gültig und muss neu erstellt werden. Dies ist immer der letzte Handler, der für ein Verbindungsobjekt aufgerufen wird. Wenn dieser Block aufgerufen wird, wurde das Verbindungsobjekt abgebaut. Es ist nicht möglich, zu diesem Zeitpunkt weitere Nachrichten über die Verbindung zu senden, weder innerhalb des Handlers noch an anderer Stelle in Ihrem Code.

In beiden Fällen sollten Sie Block-scoped-Variablen verwenden, um genügend Kontextinformationen bereitzustellen – vielleicht eine Warteschlange für ausstehende Operationen und das Verbindungsobjekt selbst -, damit Ihr Handler-Code etwas Sinnvolles tun kann, wie z.B. ausstehende Operationen erneut zu versuchen, die Verbindung abzubauen, einen Fehlerdialog anzuzeigen oder was auch immer für andere Aktionen in Ihrer speziellen Anwendung sinnvoll sind.