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