今回のテーマは「AndroidでJNI(Java Native Interface)を使用したアプリの高速化」です。C/C++やOpenGL ES(※)といったネイティブコードを使うツールなどのセットは、Android NDK(Native Development Kit)として提供されていて、JNIを使用した高速化に欠かせません。
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)に呼び方が変わっているので、ご注意ください。
今回の題材として、前回の「Android 2.1の新機能「Live Wallpaper」で作る、美しく燃える“待ち受け”」のサンプルを使用して、燃えるエフェクトを高速化します。下記の動画で高速化されていることが確認できますね。
最初は、JNI高速版、次にJava版に切り替えて、再度JNI高速版に戻した動作サンプル
今回の高速化を施したサンプルアプリは、以下よりダウンロードしてください。
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の実装は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への置き換え方法を詳しく見ていきましょう。
Copyright © ITmedia, Inc. All Rights Reserved.