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 | モーション用。 |