android_native_app_glueについて

Android上でゲームの描画処理を実行する場合、onDrawなタイミングではなく、 独自に実装したループ中で描画を実行する場合が多い。SurfaceViewを用いた 場合は30FPS、GLSurfaceViewを用いた場合は60FPS程度が見込める。 さらにはJavaコード不要なNativeActivityという仕組みもあり、既存のC/C++ コードをJava不要で移植できる(JNI不要)、描画処理中のGC動作のチューニ ングが不要というメリットがある。ちなみにJavaでIteratorを使用しただけで ヒープを消費するのってどういうこと・・・。デメリットとしてはAndroid SDKで定義されているTextView等はいっさい使えないというところ。本書では 特にandroid_native_app_glueというラッパーを用いたnative-activityという サンプルコードについて記述する。

1. NDKのビルド

1.1. プロジェクトの作成

$ cp -a ~/bin/ndk/samples/native-activity .
$ cd native-activity
$ android update project -p . -t 10 # android list targetの値

1.2. custom_rules.xml

build.xml経由で呼ばれるcustom_rules.xmlを追加。ビルド時にndk-buildを実 行するようにする。

<?xml version="1.0" encoding="UTF-8"?>
<project>
  <target name="-pre-build">
    <exec executable="ndk-build" failonerror="true"/>
  </target>
</project>

1.3. デバッグモード

デバッグモードでビルドするには、ndk-build NDK_DEBUG=1を指定するか、 AndroidManifest.xmlにdebuggable="true"を追加する。

-    <application android:label="@string/app_name" android:hasCode="false">
+    <application android:label="@string/app_name"
+                 android:hasCode="false"
+                 android:debuggable="true">

1.4. APP_ABI := all

全アーキ向けにコンパイルする。

 APP_PLATFORM := android-10
+APP_ABI := all

1.5. ndk-gdb

デバッグモードでビルド、インストールした後、ndk-gdb --startで、gdbと gdbserverの設定を実行。プログラム実行の前の方でbreakしてくれる為、b android_mainでnot definedと言われるが、ライブラリがリンクされればbreak してくれる。

0xb803503d in __futex_syscall4 () from
<path-to>/native-activity/obj/local/x86/libc.so
(gdb) b android_main
Function "android_main" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (android_main) pending.
(gdb) c
Continuing.
[New Thread 1513]
[Switching to Thread 1513]
 
Breakpoint 1, android_main (state=0x8cb58f8) at jni/main.c:235
235         app_dummy();
(gdb) 

native-activityのandroid_mainはstruct android_appへの関数登録以降はイ ベント取得と描画処理のループで構成されている。ここでは筆者が分かりづら かったものをピックアップする。

2. struct android_app

android_native_app_glueとのインターフェース。

Name Abstract
userData ユーザ定義のデータへのポインタ。
onAppCmd ユーザ定義のonResume等の処理。
onInputEvent ユーザ定義のonTouch等の入力処理。
activity ANativeActivityへのポインタ。
config AConfigurationへのポインタ。
savedState onSaveInstanceStateで退避するmalloc領域。
savedStateSize onSaveInstanceStateで退避する領域のサイズ。
looper 各種イベントを提供する。
inputQueue 入力イベントのキュー。
window NULLでない場合は描画可能。
contentRect ウィンドウのRectanble等。
activityState APP_CMD_RESUME, APP_CMD_PAUSE等の状態を保存。
destroyRequested onDestroyで1にセットされるフラグ。

上記以外のフィールドはandroid_native_app_glue側が使用する。

3. ALooper_pollAllの戻り値

onResume等のアクティビティ遷移、入力デバイスからのイベント、ユーザ定義 のイベント(センサ監視等)を取得するインターフェース。native-activity はengine.animatingが0の場合は描画不要な為、第一引数に-1を設定してブロッ ク可能にしている。

ident = ALooper_pollAll(engine.animating ? 0 : -1, NULL, &events,
                        (void **) &source))

identの値は以下。

Name Value Abstract
LOOPER_ID_MAIN 1 onAppCmdが呼ばれる。
LOOPER_ID_INPUT 2 onInputEventが呼ばれる。
LOOPER_ID_USER 3 ユーザ定義の処理。

onAppCmd, onInputEventは下記で呼ばれる。

void android_main(struct android_app* state) {
<snip>
            // Process this event.
            if (source != NULL) {
                source->process(state, source);
            }

4. onAppCmd

onAppCmdのsource->processは以下。ユーザ定義のonAppCmdだけでなく、 android_app_pre_exec_cmdとandroid_app_post_exec_cmdも呼び出される。

static void process_cmd(struct android_app* app, struct android_poll_source* source) {
    int8_t cmd = android_app_read_cmd(app);
    android_app_pre_exec_cmd(app, cmd);
    if (app->onAppCmd != NULL) app->onAppCmd(app, cmd);
    android_app_post_exec_cmd(app, cmd);
}

cmdの値は以下。

Name Value Abstract
APP_CMD_INPUT_CHANGED   AInputQueue変更。
APP_CMD_INIT_WINDOW 1 ANativeWindow準備。
APP_CMD_TERM_WINDOW 2 ANativeWindow破棄。
APP_CMD_WINDOW_RESIZED 3 ANativeWindowリサイズ。
APP_CMD_WINDOW_REDRAW_NEEDED 4 ANativeWindow再描画要求。
APP_CMD_CONTENT_RECT_CHANGED 5 Rect変更。
APP_CMD_GAINED_FOCUS 6 フォーカス取得。
APP_CMD_LOST_FOCUS 7 フォーカス消失。
APP_CMD_CONFIG_CHANGED 8 AConfiguration変更。
APP_CMD_LOW_MEMORY 9 システムメモリ減少。
APP_CMD_START 10 onStart。
APP_CMD_RESUME 11 onResume。
APP_CMD_SAVE_STATE 12 onSaveInstanceState。
APP_CMD_PAUSE 13 onPause。
APP_CMD_STOP 14 onStop。
APP_CMD_DESTROY 15 onDestroy。

以降では各cmdの値の遷移を記述する。(pre)がandroid_app_pre_exec_cmd、 (user)がonAppCmd、(post)がandroid_app_post_exec_cmdでの処理を表す。

4.1. アプリ起動時のcmd

* APP_CMD_START
  (pre)  android_app->activityStateにAPP_CMD_STARTをセット。
* APP_CMD_RESUME
  (pre)  android_app->activityStateにAPP_CMD_RESUMEをセット。
  (post) free_saved_stateの呼び出し。
* APP_CMD_INPUT_CHANGED
  (pre)  android_app->inputQueueをlooperからデタッチする。
         android_app->pendingInputQueueをandroid_app->inputQueueへセット。
         android_app->inputQueueをlooperにアタッチする。
* APP_CMD_INIT_WINDOW
  (pre)  android_app->pendingWindowをandroid_app->windowへセット。
  (user) engine_init_displayの呼び出し。
         engine_draw_frameの呼び出し。
* APP_CMD_GAINED_FOCUS
  (user) ASensorEventQueue_enableSensorの呼び出し。
         ASensorEventQueue_setEventRateの呼び出し。

4.2. バックグラウンド移行時のcmd

* APP_CMD_SAVE_STATE
  (user) android_app->savedStateをmallocし、engine->stateをコピー。
  (post) android_app->stateSavedに1をセット。
  (pre)  android_app->activityStateにAPP_CMD_PAUSEをセット。
* APP_CMD_LOST_FOCUS
  (user) ASensorEventQueue_disableSensorの呼び出し。
         engine->animatingに0をセット。
         engine_draw_frameの呼び出し。
* APP_CMD_TERM_WINDOW
  (pre)  pthread_cond_broadcastの呼び出し。
  (user) engine_term_displayの呼び出し。
  (post) android_app->windowにNULLをセット。
* APP_CMD_STOP
  (pre)  android_app->activityStateにAPP_CMD_STOPをセット。

4.3. フォアグラウンド移行時のcmd

onCreateとほぼ同じ(APP_CMD_INPUT_CHANGEDは呼ばれない)。

4.4. onSaveInstanceStateの処理

onPauseしたアプリは場合によって、システムにメモリを回収されてしまう為、 onResumeしたつもりがonCreateになってしまう場合がある。onPause時に回収 されないメモリ領域を指定することで、onCreateでonResume相当の処理を継続 できる。下記でmallocした領域はonSaveInstanceStateスレッドで回収される。

static void engine_handle_cmd(struct android_app* app, int32_t cmd) {
    struct engine* engine = (struct engine*)app->userData;
    switch (cmd) {
<snip>
        case APP_CMD_SAVE_STATE:
            // The system has asked us to save our current state.  Do so.
            engine->app->savedState = malloc(sizeof(struct saved_state));
            *((struct saved_state*)engine->app->savedState) = engine->state;
            engine->app->savedStateSize = sizeof(struct saved_state);
            break;
<snip>
    }
}

onAppCmdの後に呼ばれるpost処理。

void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd) {
    switch (cmd) {
<snip>
        case APP_CMD_SAVE_STATE:
            LOGV("APP_CMD_SAVE_STATE\n");
            pthread_mutex_lock(&android_app->mutex);
            android_app->stateSaved = 1;
            pthread_cond_broadcast(&android_app->cond);
            pthread_mutex_unlock(&android_app->mutex);
            break;
<snip>
    }
}

stateSaved=1にセットされた後、pthread_mutex_unlockで下記が動き出す。 malloc領域を退避する。

static void* onSaveInstanceState(ANativeActivity* activity, size_t* outLen) {
    struct android_app* android_app = (struct android_app*)activity->instance;
    void* savedState = NULL;
 
    LOGV("SaveInstanceState: %p\n", activity);
    pthread_mutex_lock(&android_app->mutex);
    android_app->stateSaved = 0;
    android_app_write_cmd(android_app, APP_CMD_SAVE_STATE);
    /** ここでstateSaved=1になるまで待つ */
    while (!android_app->stateSaved) {
        pthread_cond_wait(&android_app->cond, &android_app->mutex);
    }
 
    /** stateSaved=1のセット後のpthread_mutex_unlockでここに来る */
    if (android_app->savedState != NULL) {
        /** mallocされた領域を回収 */
        savedState = android_app->savedState;
        *outLen = android_app->savedStateSize;
        android_app->savedState = NULL;
        android_app->savedStateSize = 0;
    }
 
    pthread_mutex_unlock(&android_app->mutex);
 
    return savedState;
}

退避されたmalloc領域からデータを復元。

void android_main(struct android_app* state) {
    struct engine engine;
<snip>
    if (state->savedState != NULL) {
        // We are starting with a previous saved state; restore from it.
        engine.state = *(struct saved_state*)state->savedState;
    }

malloc領域をfreeする。

void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd) {
    switch (cmd) {
<snip>
        case APP_CMD_RESUME:
            free_saved_state(android_app);
            break;
    }
}

free_saved_state内部でfreeを呼ぶので、ユーザ側でfreeを呼ぶ必要はない。

static void free_saved_state(struct android_app* android_app) {
    pthread_mutex_lock(&android_app->mutex);
    if (android_app->savedState != NULL) {
        free(android_app->savedState);
        android_app->savedState = NULL;
        android_app->savedStateSize = 0;
    }
    pthread_mutex_unlock(&android_app->mutex);
}

5. onInputEvent

onInputEventのsource->processは以下。

static void process_input(struct android_app* app, struct android_poll_source* source) {
    AInputEvent* event = NULL;
    while (AInputQueue_getEvent(app->inputQueue, &event) >= 0) {
        LOGV("New input event: type=%d\n", AInputEvent_getType(event));
        if (AInputQueue_preDispatchEvent(app->inputQueue, event)) {
            continue;
        }
        int32_t handled = 0;
        if (app->onInputEvent != NULL) handled = app->onInputEvent(app, event);
        AInputQueue_finishEvent(app->inputQueue, event, handled);
    }
}

native-activityではモーションイベントの座標を利用。

static int32_t engine_handle_input(struct android_app* app,
AInputEvent* event) {
    struct engine* engine = (struct engine*)app->userData;
    if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) {
        engine->animating = 1;
        engine->state.x = AMotionEvent_getX(event, 0);
        engine->state.y = AMotionEvent_getY(event, 0);
        return 1;
    }
    return 0;
}

イベントの種類はキーとモーションの2つに大別される。

Name Value Abstract
AINPUT_EVENT_TYPE_KEY 1 キー用。
AINPUT_EVENT_TYPE_MOTION 2 モーション用。