Android main()
« | 03 Jul 2021 | »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.:
onStartonResumeonSaveInstanceStateonPauseonStoponDestroyonWindowFocusChangedonNativeWindowCreatedonNativeWindowResizedonNativeWindowRedrawNeededonNativeWindowDestroyedonInputQueueCreatedonInputQueueDestroyedonContentRectChangedonConfigurationChangedonLowMemory
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_cmdbereitet Ereignisse vor und hängt empfangene Ressourcen-Handles und -Pointer an interne Strukturen.onAppCmdleitet typische Input-Events zu eigenen Behandlungsroutinen weiter. Hier wird also auf eine Tastenanschlag oder eine Berührung reagiert.android_app_post_exec_cmdkann 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.