Android」カテゴリーアーカイブ

Android

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

Android SDK r9/NDK r5b

更新されていたので install 手順のファイル名を修正。

Android SDK/NDK install 手順

cygwin を使ってる場合以前の NDK r5 には問題がありました。
具体的には下記の通り。

・コンパイルエラーが発生したときに DOS path のままの依存ファイル(*.d)が残ってしまう
・依存ファイルの cygdrive パス置換 awk script が、1つの行に複数ファイル名が書かれているケースに対応していない

ちょうど ndk の build script を書き換えて対応していたところでしたが
r5b で修正されたようです。

その代わり r5b でそのまま通らなくなったプロジェクトがありました。
cygwin の symbolic link を使っていたためで、高速化のためにパスの扱いが
変更になったようです。

export NDK_USE_CYGPATH=1

を設定しておくことで従来と同じ動作となりコンパイルが通ります。

Android SDK/NDK install 手順からサンプル実行まで

Android SDK は install した Version によってフォルダ名などが若干異なって
いるようです。
SDK install 手順から端末の接続、NDK によるサンプル実行まで一番新しいもので
まとめてみました。(たまに更新します)

Android SDK/NDK install

結構手順が多く iOS のように Xcode 入れて終わりではありません。
その代わり Developer や Device 登録等なしに実機で動作テストできます。

Mac OS X にも Android 開発環境を入れてみましたが、adb ドライバ不要で
あっさりと端末を認識したのでむしろ簡単でした。
iOS と共通の開発環境として良いかもしれません。

NDK も入れているのは OS 2.1 で OpenGL ES 2.0 を使うため。
もうひとつはプラットフォーム間で共通のエンジンやライブラリを構築するためです。

Android 端末のベンチマークテスト (3) IDEOS

IDEOS U8150-B も追加しました。
S21HT と違い IDEOS は ARMv6 世代で最初から Android です。

           Desire   ZenTouch2  ZiiO7    LuvPad   ODROID-S     S21HT     IDEOS
-----------------------------------------------------------------------------
Android      2.2       2.1       2.1       2.2       2.2        2.2       2.2
Processor QSD8250    i.MX51    ZMS-08   Tegra250  S5PC110  MSM7201A   MSM7225
CPU clock    1GHz    800MHz      1GHz   1GHz x2      1GHz    528MHz    600MHz
CPU Arch   ARMv7A    ARMv7A    ARMv7A    ARMv7A    ARMv7A   ARMv6TEJ  ARMv6TEJ
CPU       Scorpion  CortexA8  CortexA8  CortexA9  CortexA8 ARM1136EJ ARM1136EJ
FPU      VFP3,NEON VFP3,NEON VFP3,NEON     VFP3  VFP3,NEON      ---       ---
GPU      Adreno200  AMD Z430   ZMS-08   Tegra250 PVRSGX540 Adreno130      ---
GL ES         2.0       2.0       2.0       2.0       2.0       1.1       1.1
RAM         576MB     256MB?    512MB     512MB     512MB     192MB     256MB
Display   800x480   480x320   800x480  1024x600   480x320   640x480   320x240
-----------------------------------------------------------------------------
(1)Graph    28.16    225.21    395.79    293.48    517.90      9.38     21.34
(1)Float  2049.57    432.77    581.37   2816.16   1675.83    128.41    290.60
(1)Memory  339.03    183.67    721.17    516.18    680.16    116.95    153.40
(2)CPU      759ms    1207ms    1038ms     436ms     719ms    4698ms    2971ms
(3)GPU Abs  14633     22071       ---       ---       ---       ---       ---
(3)GPU Rel  11587     26300       ---       ---       ---       ---       ---
(4)Quadrant  1259       979      1995      1827      1040       434       ---
(5)Linpack  32.82      5.66      5.97     36.71     14.03      1.98      3.78
-----------------------------------------------------------------------------
* 値が大きいほうが速い, (2) のみ値が小さいほうが速い

MSM7225 の CPU core は MSM7201A とほぼ同等だと思っていましたが予想よりも
高い数値が出ています。動作周波数も 528MHz ではなく 600MHz でした。
それでも ARMv7 世代とは比べ物にならず、VFP がないこともあって数倍から
一桁程度の差が付いています。

ただ処理速度と体感的な速度は全くの別物で、パネルの反応や追従性の方が
大きく影響します。
プロセッサは最速だけどタッチに反応しないことがある LuvPad よりも、
遅いはずなのにきちんと操作に反応する IDEOS の方がずっと好印象なのは
面白いところです。

MSM7225 は GPU として 3D アクセラレータが搭載されておらず、
OpenGL ES の API は CPU によるエミュレーションが行われているようです。

ラスタライズはパースペクティブ補正がかからずテクスチャマップが歪みフィルタもなし。
Depth は 16bit のみで Stencil 無しです。
GPU 系のテストは動作しないものが多くなっています。
アプリによってはそこそこ 3D が動くのは解像度が低いせいかもしれません。

比べると S21HT (TouchDiamond) の MSM7201A は解像度が高く非常に遅いものの
GL の描画は正しく行われていました。3D GPU が機能していたことがわかります。

関連エントリ
各種 Android 端末のベンチマークテスト (2)
各種 Android 端末のベンチマークテスト