blog

上級Android開発:Android NDK入門

ガイド\n\n\nAndroid NDKとは何ですか?\nAndroid NDKは、C/C++のような言語でアプリケーションの一部を実装できるツールのコレクションです。\nNDKはいつ使うのですか?\...

Feb 3, 2015 · 12 min. read
シェア

道しるべ

この記事は、Android NDKの経験が不足しているが、この分野での知識を広げたい人を支援することを目的としています。この記事は2つのパートに分かれており、最初のパートでは、インターフェイスのJNIから始めます。

Android NDKとは何ですか?

Android NDKは、C/C++のような言語でアプリケーションの一部を実装できるツールの集まりです。

NDKを使うタイミングは?

Googleは、NDKの使用を以下のような稀なケースに限って推奨しています:

  • パフォーマンスを改善する必要があります。
  • サードパーティライブラリの使用。例として、多くのサードパーティライブラリはC/C++で書かれており、AndroidアプリケーションはFfmpeg、OpenCVなどの既存のサードパーティライブラリを使用する必要があります。
  • 基礎となるプログラミング

JNIとは何ですか?

JNIは、Java仮想マシンの制御下でコードを実行するための標準メカニズムです。コードはアセンブラまたはC/C++プログラムとして記述され、動的ライブラリとしてアセンブルされます。また、非静的バインディングの使用も可能です。これは、Javaプラットフォーム上でC/C++を呼び出したり、逆にJavaプラットフォーム上でC/C++を呼び出したりする方法を提供します。

JNIの利点

他の同様のインターフェースに対するJNIの主な競争上の優位性は、バイナリの互換性、JNIで記述されたアプリケーションの互換性、特定のプラットフォーム上でのJava仮想マシンの互換性を保証するために一から設計されていることです。C/C++でコンパイルされたコードがどのプラットフォームでも実行できるのはこのためです。ただし、以前のバージョンの中には、バイナリ互換性をサポートしていないものもありました。

バイナリの互換性とは、プログラムの互換性の一種で、実行ファイルを変更することなく、異なるコンパイル環境でもプログラムが動作するようにするものです。

JNIの組織構成

このJNI関数テーブルは、C++仮想関数テーブルのように構成されます。仮想マシンは複数の関数テーブルを実行できます。例えば、1つはデバッグ関数用、もう1つは呼び出し関数用です。JNIインターフェース・ポインターは現在のスレッドでのみ動作します。つまり、ポインタはあるスレッドから別のスレッドに移動することはできません。しかし、異なるスレッドでローカル・メソッドを呼び出すことは可能です。

サンプルコード

jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (JNIEnv *env, jobject obj, jint i, jstring s) 
{ 
     const char *str = (*env)->GetStringUTFChars(env, s, 0); 
     (*env)->ReleaseStringUTFChars(env, s, str); 
     return 10; 
} 
  • *env - インタフェースへのポインタ.
  • obj - ローカル・メソッドで宣言されたオブジェクト参照。
  • i と s - 渡されるパラメータ。

プリミティブ型はVMとネイティブ・コードでコピーされ、オブジェクトは参照を使用して互いに渡されます。VMはネイティブ・コードに渡されたすべてのオブジェクト参照を追跡する必要があります。同時に、ネイティブ・コードは不要なオブジェクト参照をVMに通知する必要があります。

ローカル・リファレンスとグローバル・リファレンス

JNIは、ローカル参照、グローバル参照、グローバル弱参照の3種類を定義しています。ローカル参照はメソッドの完了時に有効です。JNI関数が返すJavaオブジェクトはすべてローカル参照です。プログラマーは、VMがすべてのローカル参照をクリアすることを期待していますが、ローカル参照は、それが作成されたスレッドでのみ使用可能です。必要であれば、インターフェイスの DeleteLocalRef JNI メソッドを使用して、ローカル参照を直ちに解放できます:

jclass clazz; 
clazz = (*env)->FindClass(env, "java/lang/String"); 
... 
(*env)->DeleteLocalRef(env, clazz) 

グローバル参照は、完全に解放されるまで有効です。グローバル参照を作成するには、NewGlobalRef メソッドを呼び出します。グローバル参照が不要な場合は、DeleteGlobalRef メソッドで削除できます:

jclass localClazz; 
jclass globalClazz; 
... 
localClazz = (*env)->FindClass(env, "java/lang/String"); 
globalClazz = (*env)->NewGlobalRef(env, localClazz); 
... 
(*env)->DeleteLocalRef(env, localClazz); 

不正解

  • パフォーマンスの低下
  • C言語のライブラリ関数の大部分では、エラーを避けることは困難です。

JNIは、ユーザーがJavaの例外処理を使用できるようにします。ほとんどのJNIメソッドはエラー・コードを返しますが、それ自身は例外を報告しません。したがって、コード自体で例外を処理し、それをJavaに投げる必要があります。JNI内部では、呼び出し関数によって返されたエラー・コードが最初に検査され、ExpectOccurred()が呼び出されてエラー・オブジェクトが返されます。

jthrowable ExceptionOccurred(JNIEnv *env); 

JNIプリミティブ型

JNIは独自の生のデータ型とデータ参照型を持っています。

Java

ローカル型(JNI

説明

boolean jboolean 符号なし8ビット
byte(バイト型) jbyte 符号付き8ビット
char(文字型) jchar 符号なし16ビット
short(短い整数) jshort 符号付き16ビット
int( jint 符号付き32ビット
long(長い整数) jlong 符号付き8ビット
char(文字型) jfloat 符号付き32ビット
倍精度浮動小数点 jdouble 64ビット
無効 jfloat 32

JNI参照型

UTF-8エンコーディングの改善

JNIは、改良されたUTF-8文字列を使用して、さまざまな文字種を表現します。JavaはUTF-16エンコーディングを使用します。UTF-8エンコーディングは、主にCで使用されます。これは、通常の0×00の代わりに0xc0としてエンコードされるためです。

#p#

JNI関数:

JNIインターフェースは独自のデータセットだけでなく、独自の関数も持っています。これらのデータセットや関数をレビューするには多くの時間がかかります。より詳しい情報は公式ドキュメントにあります:

//////.ml

JNI関数の使用例

以下では、簡単な例を挙げながら、この情報が何を意味しているのかを正しく理解できるようにします:

#include <jni.h> 
    ... 
JavaVM *jvm; 
JNIEnv *env; 
JavaVMInitArgs vm_args; 
JavaVMOption* options = new JavaVMOption[1]; 
options[0].optionString = "-Djava.class.path=/usr/lib/java"; 
vm_args.version = JNI_VERSION_1_6; 
vm_args.nOptions = 1; 
vm_args.options = options; 
vm_args.ignoreUnrecognized = false; 
JNI_CreateJavaVM(&jvm, &env, &vm_args); 
delete options; 
jclass cls = env->FindClass("Main"); 
jmethodID mid = env->GetStaticMethodID(cls, "test", "(I)V"); 
env->CallStaticVoidMethod(cls, mid, 100); 
jvm->DestroyJavaVM(); 

弦をひとつずつ分析してみましょう:

  • JavaVM - Java仮想マシンを作成、削除する関数を呼び出すインターフェースを提供します。
  • JNIEnv - JNI関数のほとんどを保証します。
  • JavaVMlnitArgs —  Java虚拟机参数。
  • JavaVMOption - Java仮想マシンのオプション。

JNIの_CreateJavaVM()メソッドは、Java仮想マシンを初期化し、JNIインターフェースへのポインターを返します。

JNI_DestroyJavaVM()メソッドは、作成されたJava仮想マシンをロードします。

スレッドは、AttachCurrentThread関数とAttachCurrentThreadAsDaemon関数を通じてJava仮想マシンにアタッチされます。スレッドが正常に追加されない場合、そのスレッドはJNIEnvにアクセスできません。Androidは、メモリを解放するためにGCが実行されている場合でも、JNIで作成されたスレッドを停止することはできません。DetachCurrentThreadメソッドが呼び出されるまで、スレッドはJava VMから切り離されません。

***ステップ

プロジェクトの構成は図3のようにします:

図3では、ローカルコードはすべてjniフォルダに格納されています。新しいプロジェクトが作成されると、Libsフォルダーは4つのサブフォルダーに分割されます。つまり、1つのサブディレクトリが1つのプロセッサ・アーキテクチャに対応し、ライブラリの数はプロセッサ・アーキテクチャの数に依存します。

ローカルプロジェクトとAndroidプロジェクトを作成するには、以下の手順を参照してください:

  • jni フォルダーを作成します - ネイティブ・コードを含むプロジェクト・ソース・コードのルート・ディレクトリーです。
  • プロジェクトをビルドするためのAndroid.mkファイルを作成します。
  • コンパイル・パラメータを保存するために Application.mk ファイルを作成します。この設定は必須ではありませんが、推奨されます。こうすることで、コンパイルの設定がより柔軟になります。
  • ビルド・プロセスを表示するためにndk-buildファイルを作成します。

アンドロイド

前述のように、Android.mkはローカルプロジェクトをコンパイルするmakefileです。android.mkはコードをモジュールごとに分割し、静的ライブラリをプロジェクトのlibsフォルダにコピーし、共有ライブラリとスタンドアロン実行ファイルを生成します。

最も合理的な構成例:

LOCAL_PATH := $(call my-dir) 
include $(CLEAR_VARS) 
LOCAL_MODULE    := NDKBegining 
LOCAL_SRC_FILES := ndkBegining.c 
include $(BUILD_SHARED_LIBRARY) 

詳しく見てみましょう:

  • LOCAL_PATH:-$ - 現在のファイルへのパスを返す関数マクロ my-dir を呼び出します。
  • include $ - LOCAL_PATH以外のすべての変数をクリアします。すべてのコンパイル制御ファイルが同じ GNU MAKE 実行環境にあり、すべての変数がグローバルであることを考慮すると、これは必要なステップです。
  • LOCAL_MODULE - 出力モジュールの名前。上記の例では、出力モジュールはNDKBeginingという名前ですが、生成後、libフォルダにlibNDKbeginingライブラリが作成されます。また、Androidは接頭辞libを付けます。例えば、"foo "という名前の共有ライブラリモジュールは、"libfoo.so "というファイルを生成します。 例えば、"foo "という名前の共有ライブラリモジュールは、"libfoo.so "というファイルを生成します。 しかし、Javaコードでライブラリを使用する場合、接頭辞は無視されるべきです。
  • LOCAL_SRC_FILE - コンパイルに必要なソース・ファイルのリスト。
  • include $ - 出力モジュールのタイプ。

Android.mkファイルでカスタム変数を設定できますが、LOCAL_、PRIVATE_、NDK_、APP_、my-dirという構文の命名規則に従う必要があります。 Googleは、たとえばカスタム例のプレフィックスにMY_を使うことを提案しています:

MY_SOURCE := NDKBegining.c

これは変数$を呼び出します。変数も同様に連結することができます:

LOCAL_SRC_FILES += $(MY_SOURCE)

アプリケーション.mk

このmakefileでは、コンパイルをより柔軟にするためにいくつかの変数が定義されています:

  • APP_OPTM - この変数はオプションで、アプリケーションを "リリース "にするか "デバッグ "にするかを指定するために使用します。この変数は、アプリケーションモジュールをビルドする際に、ビルドプロセスを最適化するために使用されます。デバッグのために "release "を指定することもできますが、"debug "はより多くの設定オプションをサポートします。
  • APP_BUILD_SCRI は Android.mk の別のパスを定義します。
  • APP_ABI - 最も重要な変数の1つです。モジュールのコンパイル時に使用するターゲット・プロセッサ・アーキテクチャを指定します。デフォルトでは、APP_ABI は ARMv5TE アーキテクチャに対応する "armeabi" に設定されています。例えば、ARMv7 をサポートするには、"armeabi-v7a" に設定します。IA-32-x86 や MIPS-mips のような複数のアーキテクチャをサポートするシステムでは、APP_ABI を "armeabi armeabi-v7a x86 mips" に設定する必要があります。NDK リビジョン 7 以降では、APP_ABI := "all rather enumerating all architectures" と設定するだけです。
  • APP_PLATFORM - ターゲット・プラットフォームの名前です;
  • APP_STL - Androidは最も無駄のないlibstdc c++ランタイム・ライブラリの1つを提供しているため、開発者が利用できるc++機能は非常に限られています。しかし、APP_STL変数を使用すると、これらのライブラリを拡張機能に対応させることができます。
  • NDK_TOOLCHAIN_VERSION-GCC - 選択されている GCC コンパイラのバージョン。

NDK-BUILDS(NDKビルド

NDK-buildはGNU Makeのラッパーコンテナです。NDK 4以降、NDK-buildは以下のパラメータをサポートしています:

  • clean - 生成されたすべてのバイナリをクリアします。
  • NDK_DEBUG=1 - 調整可能なコードを生成します。
  • NDK_LOG=1 - ログメッセージを表示します。
  • NDK_HOST_32BIT=1 - Androidが64ビット版をサポートするようにします。

NDK_DEBUGが "1 "に設定されると、デバッグ可能なバージョンが生成されます。NDK_DEBUGが設定されていない場合、ndk-buildはデフォルトでAndroidMainfest.xmlファイルにandroid:debuggable="true "属性が設定されていることを確認します。NDK v8以降を使用している場合、GoogleはAndoirdMainfest.xmlファイルでandroid:debuggableプロパティを使用することを推奨していません。

デフォルトでは、64ビット版がサポートされるように設定されています。NDK_HOST_32BIT=1 "を設定し、32ビットツールチェーンを強制的に使用することで、32ビットアプリケーションを使用することもできます。ただし、Googleは大規模なアプリケーションのパフォーマンスを向上させるため、64ビットアプリケーションの使用を推奨しています。

プロジェクトの設定方法は?

これは頭の痛いステップです。CDTプラグインをインストールし、cygwinまたはmingwコンパイラとAndroid NDKをダウンロードし、Eclipseの設定でこれらすべてを設定しなければなりません。Android NDKを使い始めたときは、これらの設定に3日かかりました。***問題はCygwinコンパイラにあることがわかりました:プロジェクト・フォルダのすべてのパーミッションは、読み取り、書き込み、実行可能に設定する必要があります。

もっと簡単です!このリンクからURLsdk/.html ADTパッケージをダウンロードするだけで、コンパイル・セッションを開始するのに必要なものがすべて揃っています。

Javaコードからのネイティブ・メソッドの呼び出し

Javaからネイティブ・コードを呼び出すには、まずJavaクラスにネイティブ・メソッドを定義する必要があります。例えば

native String nativeGetStringFromFile(String path) throws IOException; 
native void nativeWriteByteArrayToFile(String path, byte[] b) throws IOException 

メソッドの前に "native "キーワードを使う必要があります。Googleは、コンパイラがこのメソッドがJNIのエントリーポイントであることを認識できるように、メソッドに "native+x "のような名前をつけることを提案しています。これらのメソッドはC/C++ファイルに実装されますが、Googleは "native+x "のように命名することを提案しています。また、これらのメソッドを実装する前に、手動でヘッダーファイルを生成する必要があります。これは手動で行うこともできますし、JDKのjavahツールを使ってヘッダーファイルを生成することもできます。次に、コンソールを使わず、標準のEclipse開発環境を直接使う方法をさらに調べてみましょう:

  • NDK_APPLICATION_MK=<file> - アプリケーションの指定.mk
  • Eclipseを開き、Run -を選択します > External-tool-External -> External tools configurations。
  • jdk 内の javah.exe の絶対パスを指定します。
  • 作業ディレクトリの bin/class ディレクトリへのパスを指定します。
  • 次のパラメータを入力してください: "-jni ${java_type_name}"。

これで実行できます。ヘッダーファイルはbin/classesディレクトリにあるはずです。次に、これらのファイルをローカルプロジェクトのjniディレクトリにコピーします。プロジェクトの設定メニューを開き、Andorid Tools - Add Local Libraryを選択してください。これにより、jni.hヘッダファイルに含まれる関数を使用できるようになります。その後、.cppファイルを作成し、ヘッダファイルに定義されているメソッドを実装してください。

記事の長さと読みやすさを考慮して、簡単なコード例を載せませんでしたので、ここにはありません。必要であれば、このリンク/ngをご覧ください。

Read next

多視点からAndroidのエコシステムの開発動向を読む

MultiView技術の副社長、胡Xiaodongは、 "現時点では、Androidプラットフォーム上のMultiView読書のユーザー売上高は、iOSプラットフォーム上の売上高の6倍であり、Androidのエコシステムの成熟度の高まりと不可分であり、Android市場での有料読書の急速な成長について非常に楽観的であることを明らかにした" "。

Feb 2, 2015 · 3 min read