Archives

February 2011 の記事

Android OS 2.3、API Level 9 から NativeActivity が利用出来るようになります。
画面の初期化やイベントの取得も Java コードを通らずに C/C++ だけで
記述することが可能です。

とは言え実際にアプリケーションを記述する手段はこれまでとほとんど変わりません。
jni フォルダに C/C++ コードを置いて ndk-build を使います。
生成されるバイナリも従来同様 Dynamic Link Library (dll) ~.so です。

・今までの NDK (jni)
  Java 上で dll をロードし、interface class を経由して呼び出し

・NativeActivity
  システムが直接 dll をロードし dll 内のエントリポイントを直接呼び出す


●エントリポイントの指定

最初に呼ばれる関数名を AndroidManifest.xml に記述します。

・Java コードを含まないことを宣言
  <application> のアトリビュートに android:hasCode="false"

・dll 名の指定
  <activity> 宣言内に <meta-data> で指定する。
  例えば libappmain.so の場合

<meta-data android:name="android.app.lib_name" android:value="appmain" />

・エントリポイント名の指定
  例えば関数 Main_onCreate() から開始する場合

<meta-data android:name="android.app.func_name" android:value="Main_onCreate" />

これで Activity の起動時に libappmain.so 内の Main_onCreate() が
直接呼び出されるようになります。


●イベント

Java と全く同様に NativeActivity も onStart, onResume, onPause, onStop,
onDestroy, onRestart 等のイベントが呼ばれます。
あらかじめイベントごとに Callback 関数を登録しておく必要があります。

onCreate のみ特別で、Callback ではなく Activity 起動時のエントリ関数として
AndroidManifest.xml に記述します。
AndroidManifest.xml に func_name の記述がない場合は関数名
ANativeActivity_onCreate() とみなします。
この関数の実体はアプリケーション側で記述しておく必要があります。

(1) onCreate で AndroidManifest.xml に記述した関数が呼ばれる
(2) onCreate 内部で他のイベントの Callback 関数を登録する
(3) 各イベントで Callback 関数が呼ばれる


●android_native_app_glue

ndk の sample/native-activity や NativeActivity の Reference に
掲載されてるコードは android_native_app_glue を使っています。
これはサンプルに含まれる Utility Library です。
内部では下記の動作を行っています。

1. 各イベントごとの Callback 関数を登録する
2. NativeActivity 用のスレッドを生成する
3. イベントを格納するキューを作る
4. ユーザー定義関数 android_main() をスレッドから呼び出す

つまり、これにより android_main をイベント待ちのループとして記述できる
ようになります。Win32 API とかでよくあるスタイルです。


●static 領域の初期化

NativeActivity でも従来の jni 経由の NDK (dll) 呼び出しと全く同じです。
Activity が onDestroy で破棄されても dll 自体はしばらくメモリ上に
常駐する可能性があります。
この場合再び onCreate しても static 変数の値が初期化されずに以前の値を
残しています。

onCreate 時に自分で初期化するか前回試したような dll の分離が必要となります。


参考ページ
NativeActivity

関連エントリ
Android NDK の初期化と dll の分離
Android アプリケーションとプロセス


NDK では C/C++ のコードをそのまま使うことが出来ます。
Windows 上で作っていた lib やアプリケーションを移植し Android 上で
走らせています。
実際に動作するのですが少々問題があって、正しく起動する場合もあれば
アイコンをタッチした瞬間に落ちる場合があります。

Activity が終了しても Process イメージは残り、再び onCreate した時に
常駐している Native コードが再利用される可能性があるからです。
bss/data セグメントが初期化されていないことが原因でしょう。
static 変数は初期値に戻らず global オブジェクトのコンストラクタも走りません。

NVIDIA の資料でアプリ本体を別の dll に分離する手法が紹介されていたので
試してみました。

GameSauce 2010: Fast and Pretty: Making Responsive, Quality 3D Content on Android

Java Application からロードされるダミー (Thunk) dll を作り、その中で
自分でアプリ本体の dll を load/unload します。
確実に Unload できる点がポイントです。

NDK で複数の Dynamic Link Library (~.so) を build するには下記のようにします。

# Andriod.mak

LOCAL_PATH:= $(call my-dir)

#--------------------------------------
include $(CLEAR_VARS)

LOCAL_SRC_FILES := android_main.cpp ~
LOCAL_MODULE    := libappmain
LOCAL_CFLAGS    :=
LOCAL_C_INCLUDES:=
LOCAL_LDLIBS    := -llog -lGLESv2

include $(BUILD_SHARED_LIBRARY)
#--------------------------------------

#--------------------------------------
include $(CLEAR_VARS)

LOCAL_SRC_FILES := jniproxy.cpp
LOCAL_MODULE    := libjniproxy
LOCAL_CFLAGS    :=
LOCAL_LDLIBS    := -llog

include $(BUILD_SHARED_LIBRARY)
#--------------------------------------

ndk-build で 2つの dll ファイル libappmain.so, libjniproxy.so が作られます。
include $(CLEAR_VARS) は LOCAL_~ の変数を再定義可能にします。

Java 側でロードする dll は libjniproxy.so だけです。

// appmain.java

package jp.flatlib.appmain;
public class appmain {
     static {
         System.loadLibrary( "jniproxy" );
     }
     public static native void init();
     public static native void quit();
     public static native int  render();
     ~
}

jniproxy.cpp は libappmain.so のロード管理や呼び出しを行います。

// jniproxy.cpp

#include    <jni.h>
#include    <android/log.h>
#include    <dlfcn.h>

template<typename T>
void auto_load( T& a, void* ptr )
{
    a= reinterpret_cast<T>( ptr );
}

#define get_proc_0( image, func_name )  auto_load( proc_##func_name,  dlsym( image, #func_name ) )
#define get_proc( func_name )   get_proc_0( LoadApplication, func_name )

//-----------------------------------------------------------------------------
static void JNICALL (*proc_app_init)( JNIEnv* env, jobject obj );
static void JNICALL (*proc_app_quit)( JNIEnv* env, jobject obj );
static jint JNICALL (*proc_app_render)( JNIEnv* env, jobject obj );
~
//-----------------------------------------------------------------------------

static void*   LoadApplication= NULL;

static void InitializeApplication()
{
    // lib 読み込み
    LoadApplication= dlopen( "/data/data/jp.flatlib.appmain/lib/libappmain.so", RTLD_LAZY );
    if( !LoadApplication ){
    	~ error
    }

    // API 取り出し
    get_proc( app_init );
    get_proc( app_quit );
    get_proc( app_render );
    ~
}


static void FinalizeApplication()
{
    if( LoadApplication ){
        dlclose( LoadApplication );
        LoadApplication= NULL;
    }
}


extern "C" {
//-----------------------------------------------------------------------------

JNIEXPORT void JNICALL Java_jp_flatlib_appmain_appmain_init( JNIEnv* env, jobject obj )
{
    if( !LoadApplication ){
        InitializeApplication();
        proc_app_init( env, obj );
    }
}

JNIEXPORT void JNICALL Java_jp_flatlib_appmain_appmain_quit( JNIEnv* env, jobject obj )
{
    if( LoadApplication ){
        proc_app_quit( env, obj );
        FinalizeApplication();
    }
}

JNIEXPORT jint JNICALL Java_jp_flatlib_appmain_appmain_render( JNIEnv* env, jobject obj )
{
    return  LoadApplication ? proc_app_render( env, obj ) : 0;
}

~
//-----------------------------------------------------------------------------
};

元の android_main.cpp では jni 名を app_~ に変更しておきます。

// android_main.cpp
~
extern "C" {

JNIEXPORT void JNICALL app_init( JNIEnv* env, jobject obj )
{
}

JNIEXPORT void JNICALL app_quit( JNIEnv* env, jobject obj )
{
}

JNIEXPORT jint JNICALL app_render( JNIEnv* env, jobject obj )
{
}

~
};

アプリケーション側で dll の読み込みと終了タイミングを指定します。

スレッドの違いに注意が必要です。
例えば GL API は異なるスレッドから呼び出すことが出来ませんが、
GLSurfaceView.Renderer は別スレッドで動作しています。
Activity から呼び出す場合は GLSurfaceView.queueEvent() を使います。

mView.queueEvent( new Runnable(){ public void run(){ appmain.quit(); }  });

現在 Activity の onPause() と GLSurfaceView.Renderer の
onSurfaceChanged() を使っています。

dll の分離はうまくいっており、比較的簡単な追加コードだけで確実に初期化が
行われるようになりました。libjniproxy.so は 4KB 程度。
安定して動いています。

その代わり onPause()/onResume() で unload/load となるので、
端末を Sleep させたり別のアプリに切り替えただけでアプリケーションの
ndk 部は再起動とほぼ同様の状態となります。


関連エントリ
Android アプリケーションとプロセス


Android のアプリケーションは明確な終了をあまり意識させない作りになっています。
それでも使っているうちにだんだん Android の挙動が分かってきます。

・Home ボタンでホームメニューに戻っただけでは終了しない
・Back ボタンで戻る場合はアプリが終了していることが多い

よってアプリケーションの切り替え方法は 2種類あります。

(1) 現在の状態を保ったままバックグラウンドに切り替える方法
(2) 現在の状態を捨てて前の画面(アプリ)に戻る方法

さらにあまり意識していないだけでもう1つあることがわかりました。

(3) 現在の状態を保ったまま別のアプリを呼び出す。

例えばアプリ内から URL をタッチしてブラウザが呼ばれた場合。
また Home ボタン長押しでタスクマネージャーを呼び出し、そこからアプリを
呼び出した場合も同様です。
どちらも Back ボタンを押すと前のアプリの画面に戻ることが出来ます。

(2) はスタックから pop する動作で、ブラウザに例えると [←] ボタン。
(3) はスタックに push する動作で、ブラウザだとページ内のリンクのクリック。
(1) はスタックを切り替えます。ブラウザの新しい Tab を開くようなものです。

厳密にはアプリケーションは Activity という単位に分かれており、
実行したり外部から呼び出されるのはこの Activity です。

アプリケーションがどこかのサイトを示すなら Activity はページ相当でしょうか。
Android は複数のサイト(アプリ)間にまたがって各ページにリンクを貼ることが
可能で、その履歴が保存されます。

この Activity の呼び出しヒストリが保存されている一連のスタックを "Task" と
呼んでいます。やはりブラウザの Tab のように、操作によっては複数の履歴
スタック ("Task") が作られます。

これらのアプリが動作している VM は Linux の Process の上で動いています。


Android はマルチタスクの OS なので複数のアプリケーションが起動します。
動作中のアプリがバックグラウンドに残ってシステムが重くならないよう、
普段から意識して Back ボタンを多用していました。
ブラウザのように明確な Tab 切り替えができて、かつ Tab 単位で Close
ボタンがあればもっと分かりやすくなっていたのではないかと思います。


●複数の終了・中断状態

画面から見えなくなったりバックグラウンドに回るとアプリケーションは停止し
待機状態になります。Activity には下記のイベントが発生します。

・onPause
・onStop
・onDestory

Home ボタン等でバックグラウンドに切り替わった場合は下記の通り。

・onPause → onStop

アプリが切り替わっても部分的に画面が見ている場合は onPause だけです。
Back ボタンで前のアプリに戻る場合はさらに onDestory も呼ばれて終了します。

・onPause → onStop → onDestory

最初ここでわからなかったのが Linux process との関係です。
onDestory で Activity のインスタンスが削除されても Linux から見える
Process は残っています。

RAM 容量に十分な空きがある場合、Process 空間にロードされたプログラム
コードをすぐ削除せずに、キャッシュとして再利用することが目的のようです。

この状態ではメモリは占有していますが停止しているため CPU は消費しません。
メモリが足りなくなると即座に回収されます。

リソース\状態         (A)onPause/onStop   (B)onDestory   (C)未実行
-------------------------------------------------------------------
Activity インスタンス  存在                無し           無し
メモリ空間             専有                専有           無し
CPU リソース           アプリ依存で消費    無し           無し

(B) は Activity が終了しても Process が残っている状態を意味しています。
メモリが足りなくなった場合システムは (B)→(A) の順で回収します。

裏でアプリが動いて重くなるのは (A) 状態のまま多数存在していることが原因と
考えられます。ただしその挙動やリソース消費は厳密にはアプリ依存です。
(B) は CPU を消費しないキャッシュなので、一見 RAM を消費し Process が
残っているように見えますが気にする必要はなさそうです。

アプリを作る上で注意しなければならない点は下記の通り。

・onPause や onStop 状態からも強制終了させられる可能性がある
・onDestory でもプロセス空間は残っていることがある

特に NDK を使っていると 2つ目の点で困ったことが起こります。
続く


参考ページ
Application Fundamentals