月別アーカイブ: 2011年2月

Android NDK の初期化と dll の分離

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    
#include    
#include    

template
void auto_load( T& a, void* ptr )
{
    a= reinterpret_cast( 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 のアプリケーションは明確な終了をあまり意識させない作りになっています。
それでも使っているうちにだんだん 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