NDKを使ってみた

もちろん、NDKを使ってライフゲームをもっと早くするというお話です。

まず、最初にやったのはNDKのセットアップ、
「Android ndk」とかで検索すれば、その手順が出てきます。
自分の場合はホームディレクトリに”Develop”フォルダを作って、
そこにまとめてインストールしています。

~/Develop
    ├── android-ndk-r6b
    ├── android-sdk-linux_x86
    ├── eclipse
    └── workspace

この”android-ndk-r6b”が、現在使用してるNDKです。
なので、”.bashrc”には、以下の記述を追加しています。
export PATH=${PATH}:~/Develop/android-sdk-linux_x86/tools
export PATH=${PATH}:~/Develop/android-ndk-r6b

あとは、
$ ndk-build -v
とかでパスが通ったことを確認できれば準備完了です。

次に、EclipseにCDTをインストール(*1)するということ。
これは、「Eclipse CDT」で検索してください。
これによって、EclipseでCソースやヘッダーファイルの編集が可能になります。
あと、”Android.mk”(NDKにおけるMakefile)も色分けされます。

1. Cで実装する処理の選定
まず、どこを高速化するかですよね。
ライフゲームの場合は選択肢がないので、
誕生と消失を行う関数をCソースに移植します。

2. ByteBufferによる書き直し
さっそくCで書き直す前に、ByteBufferで実装し直します。
というのも、Javaの配列をそのまま渡してしまうと、
引数として受け取ったあとにCで使える配列に変換する必要出てしまいます。
このオーバーヘッド(*2)をなくすためにByteBufferに置き換えます。

3. Cで書き直す
Cで書き直すにもファイルをどこに置くかですよね。
“AndroidManifest.xml”がある階層に、”jni”フォルダを用意します。
その中に、Cソースとヘッダーファイル、そして”Android.mk”を置きます。

jni
 ├── Android.mk
 ├── lifegamelib.c
 └── lifegamelib.h

ちなみに、ByteBufferを引数で渡すと、こんな感じで先頭アドレスを確保出来ます。

void Java_your_domain_LifeGameWallpaper_LifeGame_ndkExecLifeGame(
	JNIEnv* env, jobject thiz, jobject buf, jint w, jint h)
{
	// ByteBufferを配列の先頭アドレスとして確保
	unsigned char *p = (*env)->GetDirectBufferAddress( env, buf );

	// 処理を書く
}

それと、自分が作成した”Android.mk”はこんな感じです。

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_SRC_FILES := lifegamelib.c
LOCAL_MODULE    := lifegamelib

include $(BUILD_SHARED_LIBRARY)

4. ビルドする
端末(コンソール?)を使って、”AndroidManifest.xml”がある階層で、
$ ndk-build
これだけデス。(*3)

これで、”libs”フォルダが作られて、その中にさらにフォルダが作られて、
上記の例だと、”liblifegamelib.so”が生成されます。
こんな感じで、先頭に”lib”が付いて、拡張子に”.so”がくっ付きます。

5. ライブラリを使用する
このライブラリを使うクラスでは、以下のように宣言します。

package your.domain.LifeGameWallpaper;

import java.nio.ByteBuffer;

public class LifeGame {
	private final byte
		STATE_DEAD = 0,
		STATE_STAY = 1,
		STATE_BORN = 2;

	private int _width;
	private int _height;
	private byte[] _buf;
	private ByteBuffer _directBuffer;

	// ライブラリを読み込むための記述
	static {
		System.loadLibrary( "lifegamelib" );
	}

	// ライブラリ内の関数を参照するための記述
	private native void ndkExecLifeGame(ByteBuffer buf, int w, int h);

	// 既存のメソッド定義等

}

あとは、ndkExecLifeGameを適所で呼び出すだけです。

まとめ
まずは、自分がハマったポイントから。

  • 生成されるライブラリは”liblifegamelib.so”だけど、
    loadLibraryの引数は”lifegamelib”と書く。
  • Cソースにおける関数名は、
    “Java_package_class_method”のように定義しないとJava側で呼び出せない。

という訳で、まずloadLibraryでハマって、関数名の記述ミスでハマりました。
あと、どの程度早くなったのかというと、
ライフゲームの処理が10msくらい掛かっていたのが、3msくらいになりました。
Javaのコードをコピペして、コンパイルが通る程度に手を入れた訳ですが、
十分に効果がありました。
ただ、まだまだ無駄なメモリコピーがあって、
あと、入力と出力の計2つByteBufferを渡さなきゃいけないところを、
1つのByteBufferの前半と後半を使って処理してたりと、
人様に見せられない恥ずかしい実装なので、コードはちょっと晒せないデス。
それと、NDKの導入は考えてなかったので、
“liblifegamelib.so”という恥ずかしいファイル名になってしまって、
これも本来なら晒したくなかったデス。

あと、メモとして、
Android側のコードに変更がないと、ライブラリの変更が反映されないらしいです。

今後の予定は以下の通り。

  • ライブ壁紙の設定を実装する
  • 描画周りが5msくらい掛かっているので最適化する
  • 計測方法を検証する(*4)

(*1) CDTのインストールをしなくてもNDKを使った開発はできます。
(*2) どの程度オーバーヘッドが発生するのかは未検証デス、ゴメンなさい。
(*3) ビルドオプションは、自分で検索してください。
(*4) デバッグのためにケーブルを接続した状態だと遅いらしい(?)

おしまい。

Leave a Comment