Zurück zu Googles Android

Manchmal ist es sinnvoll, Prozesse auf einem Gerät dauerhaft laufen zu lassen und unterschiedlichen Anwendungen einen Zugriff darauf zu erlauben. Solche Prozesse mögen Anwendungen sein, die regelmäßig Daten von einer externen Quelle beziehen und lokal auf dem Android-Gerät speichern. Es mögen lang laufende Berechnungen sein oder Dienste, die die Werte der Sensoren des Handy regelmäßig auslesen und verarbeiten. Es können aber auch Dienste sein, die keine Werte für Anwendungen berechnen und danach ihren Dienst einstellen.

In Android existiert das Konzept der Services. Services laufen parallel zu Activities und können von unterschiedlichen Aktivitäten genutzt werden. Mit Services wird mittels RPC kommuniziert. RPC steht für Remote Procedure Call und wird Ihnen gegebenenfalls bereits in der Variante RMI begegnet sein.

Das Konzept der RPC ist schon recht alt. Es geht auf einen Artikel von Nelson und Birrell aus dem Jahre 1984 zurück; zu einigen wenigen Details der Geschichte siehe auch T. Schwotzer.[1] RPC ähneln lokalen Prozeduraufrufen. Wird innerhalb eines Prozesses eine Prozedur/Funktion/Methode aufgerufen, so werden die Eingabewerte auf den Stack des Prozesses (konkret des Threads, wenn es mehrere gibt) gelegt. Die aufgerufene Funktion entnimmt am Anfang die Parameter, danach wird die Prozedur ausgeführt. Die Rückgabewerte werden wiederum auf den Stack gelegt und die Funktion wird beendet. Die rufende Funktion entnimmt nun die Rückgabewerte und arbeitet weiter.

Sollen Prozeduren über Prozessgrenzen aufgerufen werden, so müssen die Eingabewerte von rufenden Prozess zum aufgerufenen Prozess übertragen werden. Umgekehrt müssen die Ergebnisse vom gerufenen Prozess zum Aufrufer zurückgeschickt werden. Funktionen, die diese Parameter in ein Datenpaket packen, versenden und entpacken können anhand einer Interfacebeschreibung generiert werden. überlegen Sie einmal, wie Sie das machen würden!

In Android dient dazu eine Interfacebeschreibung und das Tool AIDL.[2] Lesen Sie dort Details zur Nutzung von AIDL und der Interfacebeschreibung nach. Hier wird nur das grobe Prinzip erklärt.

IDL steht für Interface Definition Language. Es gibt eine Fülle von IDL-Sprachen. Viele sind an Programmiersprachen angelehnt. Das erscheint auch logisch. Hier ist ein Beispiel.

interface Test {
	int increment(int i);
}

Eine solche Beschreibung muss in Android in einer Datei Test.aidl gespeichert werden. Das Tool aidl erzeugt daraus eine einzige Datei: Test.java. Diese Datei enthält Javacode und konkret Folgendes:

  • die Benutzeroberfläche „Test“. Diese enthält
  • Stub
  • Proxy

Wenn Sie Eclipse nutzen, so wird aidl automatisch gestartet, sobald Sie irgendeine Änderung in der aidl-Datei durchführen. Sollten Sie also das Beispielprogramm importieren, so müssen Sie einmal eine Leerzeile einfügen oder Ähnliches und aidl generiert ein (neues) Java-File.

Es soll zunächst die Serverseite eines Services diskutiert werden. Auf der Serverseite wird der Dienst erbracht. Dazu sind folgende Dinge notwendig:

  • Es muss eine Implementierung existieren.
  • Es muss Code existieren, der die ankommenden Daten deserialisiert und der die Rückgabewerte serialisiert und zurücksendet.
  • Es muss ein Prozess laufen, der auf Dienstaufrufe reagiert und sie an die Implementierung weiterreicht.

Die zweiten Aufgabe wird durch einen Stub bereitgestellt, die durch den aidl-Compiler erzeugt wird. Die Implementierung wird in das Gesamtsystem integriert, indem vom Stub abgeleitet wird und die Implementierung hinzugefügt wird. In der Beispielanwendung sieht das wie folgt aus:

public class TestImplementation extends Test.Stub { 
	public int increment(int i) throws RemoteException {
		return i+1;
	}
}

Die Implementierung ist offenbar denkbar simpel. Der Parameter wird entgegengenommen und das Inkrement wird zurückgegeben. Die Serialisierung und Deserialisierung bleibt den implementieren vollständig verborgen. Die einzige Ausnahme bildet die RemoteException, die innerhalb der Methode erzeugt und geworfen werden kann. Damit kann angezeigt werden, dass ein Fehler entstanden ist, der sich aus der Verteilung ergibt. Von einem Server wird erwartet, dass er auf Anfragen von Clients wartet und diese bearbeitet. Dazu ist ein Prozess nötig. In Android dient dazu die Klasse „Service“. Um einen eigenen Service zu definieren, wird von dieser Klasse abgeleitet. Beispiel:

public class SomeService extends Service { 
	private TestImplementation impl;
	public void onCreate() { 
		super.onCreate();
		impl = new TestImplementation(); 
	}	
	public IBinder onBind(Intent intent) { return impl; }
}

Die Implementierung stellt lediglich ein Beispiel dar, man kann den Service auch anders implementieren. Man muss allerdings auf zwei Ereignisse reagieren: Erzeugung und Binden.

Ein Service wird einmal erzeugt. Das erfolgt, wenn der erste Aufruf eines Clients erfolgt. Der Client versucht, sich in dem Moment an den Service zu binden. Binden bedeutet, dass eine Verbindung zwischen Client und Server aufgebaut wird. Nach erfolgreichem Binden können RPCs aufgerufen werden. Das Binden erfolgt immer, wenn ein Client eine neue Verbindung aufbauen will.

Diese Implementierung ist sehr einfach. Sobald ein Service erzeugt werden soll (onCreate()), wird ein Objekt der Implementierung erzeugt. Diese Serviceimplementierung merkt sich dieses eine Objekt. Bei jedem Bindeversuch wird eine Referenz auf dieses Objekt geliefert, siehe onBind(). Es ist zu erkennen, dass die Implementierung offenbar auch IBinder implementiert. Auch dieser Code ist generiert: Der Stub implementiert IBinder und damit erbt die Implementierung (TestImplementation) diese Fähigkeit.

Der Service muss nun noch Android bekannt gegeben werden. Wenig verwunderlich erfolgt das im Manifest der Anwendung:

<service android:name=".SomeService" />

Auf Clientseite sind folgende Aufgaben zu erfüllen:

  • Es ist eine Implementierung notwendig, die dem Client „vortäuscht“, dass er den Service direkt aufruft.
  • Vor dem Aufruf muss sich der Client an den Service binden können. Letzteres erfolgt über eine ServiceConnection. Eine Instanz einer ServiceConnection repräsentiert die Bindung an einen Service, der den entsprechenden Dienst erbringen kann. Eine beispielhafte Implementierung ist diese:
public class TestServiceConnection implements ServiceConnection { 
	
	private Test testService = null;
	
	public Test getTestService() {
		return this.testService;
	}
	
	public void onServiceConnected(ComponentName name, IBinder service) {
		Log.d(null, "Service Connected");
		this.testService = (Test) service;
	}

	public void onServiceDisconnected(ComponentName name) {}
}

Objekte konkreter ServiceConnections werden erzeugt und danach im Prozess des Bindens von einem Binder genutzt. Folgender Code initiiert beispielsweise das Binden:

mConnection = new TestServiceConnection();
this.bindService(new Intent(this, SomeService.class), mConnection, Context.BIND_AUTO_CREATE);

In diesem Fall wird exemplarisch eine Verbindung zu einem Service aufgebaut, der durch den Klassennamen bekannt ist. Informieren Sie sich über andere Möglichkeiten des Bindens im Handbuch! Das Prinzip ist in jedem Fall gleich. Es wird verlangt, dass eine Verbindung zu einem Service erzeugt werden soll (bindService()). Zur Beschreibung des Services dient das Intent. Das Connection-Objekt wird als Referenz übergeben, der letzte Parameter ermöglicht das Setzen von Optionen für den Verbindungsaufbau.

Dieser Aufruf erfolgt asynchron, das heißt im Hintergrund wird nun versucht, eine Verbindung zum Service aufzubauen. Konnte eine Verbindung zum Serviceprozess aufgebaut werden, so wird auf der ServiceConnection die Methode onServiceConnected() aufgerufen. Als Parameter wird eine Referenz auf einen IBinder übergeben. Tatsächlich zeigt diese Referenz auf den Clientstub der RPC-Implementierung. Er implementiert ebenfalls das Interface „Test“. In dieser Implementierung wird dieses Objekt gespeichert und bei Bedarf wird es mittels getTestService() herausgegeben. In der Beispielanwendung erfolgt das, wenn auf einen Knopf gedrückt wird. Der Clientcode sieht dort etwa so aus:

int value = 0;
Test testSrv = this.mConnection.getTestService();
value = testSrv.increment(value);

Die ServiceConnection (mConnection) wurde bereits zuvor erzeugt und als Parameter dem Binder übergeben, siehe oben. An dieser Stelle wird davon ausgegangen, dass das Binden erfolgreich war. Es wird eine Referenz auf den Service (konkret den Clientstub) verlangt (getTestService() – siehe Implementierung von TestServiceConnection oben). Der Service implementiert die Servicebenutzeroberfläche „Test“. Der Service kann nun aufgerufen werden. An dieser Stelle ist er von einem lokalen Prozeduraufruf nicht zu unterscheiden. In der vollständigen Implementierung ist aber zu erkennen, dass zusätzlich auf eine RemoteException reagiert werden muss. Die Verteilung bleibt also nicht völlig transparent.

Einzelnachweise

Bearbeiten
  1. T. Schwotzer, Entfernter Mailzugriff auf RPC-Basis, Diplomarbeit, TU Chemnitz-Zwickau, Juli 1994 http://www.sharksystem.net/paper/diplom_schwotzer.pdf
  2. http://developer.android.com/guide/developing/tools/aidl.html. Stand 12. November 2010.