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.:
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.