連載
» 2010年03月17日 00時00分 UPDATE

Androidで動く携帯Javaアプリ作成入門(15):Android NDKでJNIを使用してアプリを高速化するには (1/3)

[緒方聡,株式会社イーフロー]

Android NDK最新版でOpenGL ES 2.0をサポート

 今回のテーマは「AndroidでJNIJava Native Interface)を使用したアプリの高速化」です。C/C++OpenGL ES)といったネイティブコードを使うツールなどのセットは、Android NDKNative Development Kit)として提供されていて、JNIを使用した高速化に欠かせません。

OpenGL ES(OpenGL for Embedded Systems)OpenGL ES(OpenGL for Embedded Systems)携帯情報端末や組み込み向けのOpenGLのサブセット。OpenGLは、一般的に普及しているグラフィックスプログラミング用のC/C++のAPIのこと(参考:はじめよう3D描画、BREWでOpenGL ESプログラミング

 JNIとNDKの詳細は、以下の記事をご確認ください。

 そんなAndroid NDKですが、先日、最新版のRevision 3がリリースされ、OpenGL ES 2.0がサポートされるようになり、さらにグラフィック描画機能が強化されました。

 ちなみに、r3(Revision 3)リリースに伴い、いままでのNDKのバージョン、Android 1.6 NDK Release 1(1.6r1)はAndroid NDK r2(Revision 2)に、1.5 NDK Release 1(1.5r1)は同じくr1(Revision 1)に呼び方が変わっているので、ご注意ください。

同じサンプルをJNIで高速化した結果を動画で確認

 今回の題材として、前回の「Android 2.1の新機能「Live Wallpaper」で作る、美しく燃える“待ち受け”」のサンプルを使用して、燃えるエフェクトを高速化します。下記の動画で高速化されていることが確認できますね。

最初は、JNI高速版、次にJava版に切り替えて、再度JNI高速版に戻した動作サンプル

 今回の高速化を施したサンプルアプリは、以下よりダウンロードしてください。

そもそも、JNIってAndroidに必要なの?

 Webアプリケーションを開発するJavaプログラマにとって、JNIはあまり使用しない機能ではないかと思います。実際Javaという言語および実行環境はとても優秀なので、Javaだけで事足りてしまうためです。

 今回は、目的があってJNIを使用するのですが、その前にAndroidでJNIを使用するメリットとデメリットを見てみましょう。

メリット デメリット
既存のライブラリの活用 プラットフォームに依存
パフォーマンス向上 メモリ管理などが面倒
表1 JNIのメリットとデメリット

 メリットはC/C++などで実装された既存のライブラリを利用できること、パフォーマンスを向上できることです。Android 2.1のDalvik VMには、JITは搭載されていないため、パフォーマンス向上は顕著です。デメリットはプラットフォーム(CPUのアーキテクチャ)に依存してしまうことが挙げられます。

 現時点でAndroidのプラットフォームのほとんどはARMアーキテクチャを採用していますが、x86をはじめ、さまざまなCPUのプラットフォームがサポートされ始めています。

 今回のサンプルは、ARM向けのネイティブライブラリがアプリに組み込まれた形になっています。このネイティブライブラリはARMアーキテクチャ以外で動作できないので、必要であれば「System.getProperty("os.arch")」で取得した値に「arm」または「ARM」が含まれているかどうかで判断します。含まれていない場合は、ネイティブライブラリをロードせず、代替手段を講じるのがいいでしょう。

コラム 「難しいJNIのメモリ管理に対する戦略」

JNIの実装はC/C++を使用することになるのですが、C/C++を使用するに当たり、問題なのがメモリ管理です。以下に、JNIにおけるメモリ管理の戦略を紹介します。

・JNI_OnLoad()、JNI_OnUnload()
JNI_OnLoad()は、ネイティブライブラリがロードされた際に呼び出される関数で、JNI_OnUnload()関数はネイティブライブラリを含むクラスローダがガベージコレクタで回収される際に呼び出されます。ネイティブライブラリで確保したメモリは、JNI_OnUnload()関数で解放する方法が考えられます。ネイティブ側の作業領域やシステムで共有可能なメモリなどの解放に向いています。

・Object#finalize()
ネイティブ側で確保したメモリは、そのアドレスをJava側のフィールドにintとして保持しておき、対象のインスタンスがガベージコレクタで回収される際に、finalize()メソッドで解放する方法が考えられます。ネイティブ側の処理がJavaのインスタンスに連動する場合に向いています。

どちらのケースにしても、メモリ以外にリソースの解放も必要です。リソースの解放とメモリの解放は、必要となるタイミングが異なる場合があるので、注意してください。


アプリのどこが重いのかを調べ、ネイティブ化すると…

 今回の目的は、JNIを使用した高速化です。ですから、まずどの部分の処理が重いのか調査します。

public void draw(Canvas canvas){
    synchronized (this) {
        Log.d("TIME", "1:" + System.currentTimeMillis());
        for (int i = 0; i < width * height; i++) {
            if (seedparam[i]!= 0 && seedparam[i] > (int)((fireLevel - boost) * Math.random())) {
                pallet[i] = 127;
            } else if (seedparam[i] != 0) {
                pallet[i] = 0;
            }
        }
        if (boost > 0) {
            boost--;
        }
 
        Log.d("TIME", "2:" + System.currentTimeMillis());
        for (int i = 1; i < height - 1; i++) {
            for (int j = 1; j < width - 1; j++) {
                pallet[(i - 1) * width + j] = (pallet[i * width + j] + 
                        pallet[i * width + j - 1] + 
                        pallet[i * width + j + 1] + 
                        pallet[(i - 1) * width + j] + 
                        pallet[(i + 1) * width + j]) / 5;
            }
        }
        Log.d("TIME", "3:" + System.currentTimeMillis());
 
        for (int i = 0; i < width * height; i++) {
            image[i] = color[pallet[i]];
        }
 
        Log.d("TIME", "4:" + System.currentTimeMillis());
        bitmap.setPixels(image, 0, width, 0, 0, width, height);
        Log.d("TIME", "5:" + System.currentTimeMillis());
        canvas.drawBitmap(bitmap, 0, 0, null);
        Log.d("TIME", "6:" + System.currentTimeMillis());
    }
}

 上記は前回のサンプルの状態のソースコードで、ところどころにログが埋め込んであります。筆者の環境でのエミュレータ上での所要時間は以下の通りです。

区分 処理概要 経過時間
1〜2 各ピクセルのパレットを初期化 331ms
2〜3 各ピクセルのパレットを再計算 1224ms
3〜4 各ピクセル色をパレットから選定 219ms
4〜5 ビットマップにピクセルデータを設定 40ms
5〜6 ビットマップを描画 6ms
表2 筆者の環境でのエミュレータ上での所要時間

 forループ文内でピクセル処理を行っている個所が圧倒的に遅いです。ここを切り出してネイティブ化すれば、高速化が見込めそうです。

public void draw(Canvas canvas) {
    synchronized (this) {
        effect(fireLevel, width, height, image, pallet, seedparam, color);
        bitmap.setPixels(image, 0, width, 0, 0, width, height);
        canvas.drawBitmap(bitmap, 0, 0, null);
    }
}
private native void effect(int fireLevel, int width, int height, int[] image, int[] pallet, int[] seedparam, int[] color);

 新しいソースコードは、上記のように、処理時間のかかる部分をすべてネイティブコードに置き換えています。Javaのみで平均1300msかかっていた描画処理が、JNIに置き換えることで平均70msになりました。

区分 処理概要 経過時間
Native ピクセル処理 30ms
Java ビットマップ処理 40ms
表3 所要時間の比較

 Javaの処理をネイティブに移植することで高速化できる、という大変良い例になったかと思います。

 それでは、次ページよりJNIへの置き換え方法を詳しく見ていきましょう。

       1|2|3 次のページへ

Copyright© 2017 ITmedia, Inc. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。