Android main()

Mein erster Android App-Prototyp im GATE Framework ist fertig. Und nachdem Android NDK Apps ganz anders aufgebaut sind als übliche C int main() Programme, musste ich einige Umleitungen legen, damit das alles läuft.

Eine gute Gelegenheit also niederzuschreiben, wie Android NDK Apps hochfahren und wo man was tun muss.


In der Praxis generiert das Visual Studio 2017+ Android Projekt-Template eigentlich schon jede Menge “Glue-Code” für OpenGL-ES Projekte, wo man nur noch seinen Code in eine Render-Funktion verlegen muss.

Doch im GATE Projekt möchte ich nicht, dass jede App anders “angefangen” werden muss. Ich möchte den Glue-Code also in den GATE-Platform-Support-Layer schieben und den Applikationscode genau so aussehen lassen, als ob eine main() Routine (genau genommen eine gate_main() Funktion) ausgeführt wird.

Und dazu muss man etwas genauer wissen, was bei Android im Hintergrund passiert.

NDK Apps sind “shared libraries”

Eine NDK basierte Android App produziert ein shared object (*.so), das eine Funktion export, und zwar ANativeActivity_onCreate. Beim Start lädt die Laufzeitumgebung die SO Datei und ruft diese Funktion auf um ihr einen ANativeActivity Pointer zu übergeben.

Diese ANativeActivity versorgt uns mit weiteren Zeigern zu JNI und weiteren Ressourcen, aber vor allem können wir dort eigene Callback-Routinen zu diversen Android-Ereignissen registrieren, wie z.B.:

  • onStart
  • onResume
  • onSaveInstanceState
  • onPause
  • onStop
  • onDestroy
  • onWindowFocusChanged
  • onNativeWindowCreated
  • onNativeWindowResized
  • onNativeWindowRedrawNeeded
  • onNativeWindowDestroyed
  • onInputQueueCreated
  • onInputQueueDestroyed
  • onContentRectChanged
  • onConfigurationChanged
  • onLowMemory

Der Glue-Code hängt an die meisten Callback-Pointer eigene Funktionen und ruft dann die ebenfalls generierte Funktion android_app_create() auf. Diese Funktion erzeugt eine interne struct android_app in der weitere App-relevante Daten angefügt werden können und startet einen Thread per pthread_create() zur Funktion android_app_entry(). An dieser Stelle kehrt dann ANativeActivity_onCreate zurück und alles weitere läuft dann in unserem Thread quasi asynchron zur Android Runtime.

Innerhalb von android_app_entry() wird ein “Looper” erzeugt (ALooper_prepare), der als Nachrichtenschleife fungieren und Ereignisse wie Tasten oder Touch-Events empfangen soll. Der Looper nutzt Pipes um auf Events reagieren zu können.
Nachdem der Looper erzeugt wurde wird die Funktion android_main() aufgerufen, die so lange synchron ausgeführt werden soll, wie die App laufen soll. Kehrt sie zurück, leitet android_app_destroy() den Shutdown der App ein und beendet den Thread.

android_main() lässt nun die Schleife laufen, die vom Looper Events herauszieht, wenn welche da sind, und danach wird der OpenGL ES Code aufgerufen um die aktuelle Szene zu zeichnen.

Reagieren auf Ereignisse

Durch die zuvor gesetzten Callbacks kommen nach der Rückkehr von ANativeActivity_onCreate() immer wieder Callbacks von Android herein. Solche wie onNativeWindowCreated bringen eine Pointer mit, der eine Resource bereitstellt und dieser wird an unsere struct android_app Instanz dran geheftet. Danach wird per android_app_write_cmd ein Ereignis-Byte in die zuvor erzeugte Pipe geschrieben, die unseren Looper aufwachen und das Ereignis weiterverarbeiten lässt. Die unterstützten Bytewerte sind in einer Enumeration abgebildet und beschreiben eigentlich alle ursprünglichen Callbacks, nämlich
APP_CMD_INPUT_CHANGED, APP_CMD_INIT_WINDOW, APP_CMD_TERM_WINDOW, APP_CMD_WINDOW_RESIZED, APP_CMD_WINDOW_REDRAW_NEEDED, APP_CMD_CONTENT_RECT_CHANGED, APP_CMD_GAINED_FOCUS, APP_CMD_LOST_FOCUS, APP_CMD_CONFIG_CHANGED, APP_CMD_LOW_MEMORY, APP_CMD_START, APP_CMD_RESUME, APP_CMD_SAVE_STATE, APP_CMD_PAUSE, APP_CMD_STOP, APP_CMD_DESTROY

Ein geschriebenes Byte landet über den Looper in der Funktion process_cmd, die bei dessen Setup registriert wurde, und process_cmd spaltet dann den Aufruf in 3 Aufruf auf, und zwar in android_app_pre_exec_cmd, onAppCmd und android_app_post_exec_cmd.

  • android_app_pre_exec_cmd bereitet Ereignisse vor und hängt empfangene Ressourcen-Handles und -Pointer an interne Strukturen.
  • onAppCmd leitet typische Input-Events zu eigenen Behandlungsroutinen weiter. Hier wird also auf eine Tastenanschlag oder eine Berührung reagiert.
  • android_app_post_exec_cmd kann noch Cleanups durchführen, macht aber sonst nicht besonders viel.

Beendet wird eine App durch das Setzen eines destroyRequested Flags in unserer struct android_app. Und das geschieht im generierten Code, wenn das Event APP_CMD_DESTROY ankommt.
Dann wird die Nachrichten Schleife verlassen und alle Ressourcen werden freigegeben.

Fazit

Das war es auch schon wieder.
Es ist eigentlich mit jeder UI-Bibliothek das Gleiche.
Man muss wissen, wie man:

  • sie initialisiert.
  • Zeichenoberflächen (Fenster) erhält.
  • auf den Oberflächen zeichnet.
  • Callbacks für Tasten, Mouse und Toucheingaben registriert.
  • am Ende alles herunterfährt.

Hat man die nötigen Funktionen dafür in einem UI Framework gefunden, kann man dahinter “ganz normal” seine Logik weiterentwickeln.

In so fern ist Android da nichts Besonderes.
Lustig wird es erst, wenn man dann JNI für Aufgaben braucht, die das NDK nicht über C/C++ Schnittstellen bereitstellt.

Aber das ist eine andere Geschichte.