JNI に関するヒント

JNI(Java Native Interface)では、Android で(C / C++ で作成された)ネイティブ コードを操作するために(Java または Kotlin プログラミング言語で作成された)マネージコードからコンパイルするバイトコードのスタイルを定義します。JNI はベンダーに依存せず、動的な共有ライブラリからコードをロードすることができます。時として扱いにくいこともありますが、かなり効率的です。

注: Android は Kotlin を Java プログラミング言語と同様の方法で ART 対応のバイトコードにコンパイルします。そのため、このページで紹介する、JNI アーキテクチャとそれに関連するコストについてのガイダンスは、Kotlin と Java プログラミング言語の両方に適用できます。詳しくは、Kotlin と Android についてのページをご覧ください。

JNI についてよく知らない方は、Java Native Interface の仕様をお読みいただき、JNI の仕組みと使用できる機能をまず理解してください。JNI には一読しただけではわかりにくい要素もあるため、ここで紹介する内容が JNI の理解の一助になれば幸いです。

JNI グローバル参照を参照して、JNI グローバル参照が作成および削除される場所を確認するには、Android Studio 3.2 以降で Memory ProfilerJNI ヒープビューを使用します。

一般的なヒント

JNI レイヤのフットプリントは最小限に抑えるようにします。その際、いくつかの特徴を考慮する必要があります。JNI ソリューションでは、以下のガイドラインに従うようにしてください(以下は、重要度の高い順に記載されています)。

  • JNI レイヤ全体でのリソースのマーシャリングを最小限に抑える。 JNI レイヤ全体でのマーシャリングにかかるコストは少なくありません。インターフェースの設計では、マーシャリングする必要があるデータの量と、データをマーシャリングする頻度を最小限に抑えるようにしてください。
  • 可能な場合は、マネージ プログラミング言語で作成したコードと C++ で作成したコード間の非同期でのやり取りを行わないようにする。これにより、JNI インターフェースの管理が容易になります。一般に、非同期アップデートを UI と同じ言語に維持することで、UI の非同期アップデートをシンプルにすることができます。たとえば、JNI を介して Java コードの UI スレッドから C++ 関数を呼び出すのではなく、Java プログラミング言語の 2 つのスレッド間でコールバックを行い、そのうちの 1 つのスレッドで C++ の呼び出しをブロックして、呼び出しのブロックが完了したときに UI スレッドに通知するほうが効率的です。
  • JNI を操作する、または JNI から操作される必要があるスレッドの数を最小限に抑える。Java と C++ の両方の言語でスレッドプールを使用する必要がある場合、JNI とのやり取りはプールの所有者とのみ行うようにし、個々のワーカー スレッドとは行わないようにしてください。
  • インターフェースのコードを容易に特定可能な少数の C++ および Java ソースの場所に保持し、将来のリファクタリングを容易にする。 必要に応じて、JNI 自動生成ライブラリの使用を検討してください。

JavaVM と JNIEnv

JNI では 2 つの主要なデータ構造(「JavaVM」と「JNIEnv」)が定義されています。基本的に、これらはどちらも関数テーブルへのポインタのポインタです(C++ バージョンでは、関数テーブルへのポインタと、テーブルを介して間接的に呼び出される各 JNI 関数のメンバー関数を持つクラスです)。JavaVM の「呼び出しインターフェース」関数を使用すると、JavaVM の作成と破棄を行うことができます。理論上は、各プロセスで複数の JavaVM を設定できますが、Android で設定できるのは 1 つだけです。

JNIEnv はほとんどの JNI 関数を提供します。ネイティブ関数はすべて、最初の引数として JNIEnv を受け取ります。

JNIEnv はスレッド ローカルのストレージ用に使用されます。そのため、スレッド間で JNIEnv を共有することはできません。コードでその JNIEnv を取得する方法が他にない場合は、JavaVM を共有し、GetEnv を使用してスレッドの JNIEnv を見つける必要があります(JNIEnv が存在する場合。以下の AttachCurrentThread をご覧ください)。

JNIEnv と JavaVM の C 言語での宣言は、C++ の宣言とは異なります。"jni.h" インクルード ファイルは、C と C++ のどちらにインクルードするかに応じて typedef が異なります。そのため、両方の言語でインクルードされるヘッダー ファイルに JNIEnv の引数を含めることはおすすめできません(つまり、ヘッダー ファイルで #ifdef __cplusplus が必要な場合、そのヘッダー内の要素で JNIEnv を参照するには、なんらかの追加の処理が必要になることがあります)。

スレッド

すべてのスレッドは、カーネルによってスケジュール設定される Linux スレッドです。スレッドは通常、(Thread.start を使用して)マネージコードから開始されますが、他の場所で作成して JavaVM にアタッチすることも可能です。たとえば、pthread_create で開始されたスレッドを、JNI の AttachCurrentThread 関数または AttachCurrentThreadAsDaemon 関数を使用してアタッチすることができます。スレッドがアタッチされるまでは JNIEnv がないため、JNI の呼び出しを行うことができません

ネイティブに作成されたスレッドをアタッチすると、java.lang.Thread オブジェクトが作成されて「メイン」の ThreadGroup に追加され、デバッガから参照できるようになります。すでにアタッチされているスレッドで AttachCurrentThread を呼び出しても何も行われません。

Android は、ネイティブ コードを実行しているスレッドを停止しません。ガベージ コレクションが実行中の場合、またはデバッガから停止リクエストが発行された場合、Android は JNI を次に呼び出したときにスレッドを一時停止します。

JNI を介してアタッチされたスレッドは、終了する前に DetachCurrentThread を呼び出す必要があります。これを直接コーディングすることが難しい場合、Android 2.0(Eclair)以降では、pthread_key_create を使用して、スレッドが終了する前に呼び出されるデストラクタ関数を定義し、そこから DetachCurrentThread を呼び出すことができます(対応するキーを pthread_setspecific で使用して、JNIEnv をスレッド ローカル ストレージに保存します。こうすることで、JNIEnv が引数としてデストラクタに渡されます)。

jclass、jmethodID、jfieldID

ネイティブ コードからオブジェクトのフィールドにアクセスする場合は、以下を行います。

  • FindClass でクラスのクラス オブジェクト参照を取得する
  • GetFieldID でフィールドのフィールド ID を取得する
  • 適切な方法(GetIntField など)でフィールドの内容を取得する

同様に、メソッドを呼び出すには、クラス オブジェクト参照、メソッド ID の順に取得します。ID は、内部のランタイム データ構造体への単なるポインタであることがほとんどです。ID を調べるために文字列の比較を数回行わなければならないこともありますが、いったん ID を取得すると、実際のフィールドの取得やメソッドの呼び出しを非常にすばやく行えるようになります。

パフォーマンスが重要な場合は、値を 1 回調べてその結果をネイティブ コード内にキャッシュすることをおすすめします。プロセスごとに 1 つの JavaVM という制限があるため、このデータは静的なローカル構造体に保存するのが妥当でしょう。

クラス参照、フィールド ID、メソッド ID は、クラスがアンロードされるまで有効であることが保証されます。クラスがアンロードされるのは、ClassLoader に関連付けられているすべてのクラスのガベージ コレクションが可能な場合だけです。この状態はまれにしか起こりませんが、Android ではあり得ないことではありません。ただし、jclass はクラス参照であるため、NewGlobalRef を呼び出して保護する必要があります(次のセクションを参照)。

クラスがロードされたときに ID をキャッシュして、クラスのアンロードと再ロードが行われたときに自動的に再キャッシュする場合、ID を初期化するには、以下のようなコードを適切なクラスに追加する必要があります。

Kotlin

    companion object {
        /*
         * We use a static class initializer to allow the native code to cache some
         * field offsets. This native function looks up and caches interesting
         * class/field/method IDs. Throws on failure.
         */
        private external fun nativeInit()

        init {
            nativeInit()
        }
    }
    

Java

        /*
         * We use a class initializer to allow the native code to cache some
         * field offsets. This native function looks up and caches interesting
         * class/field/method IDs. Throws on failure.
         */
        private static native void nativeInit();

        static {
            nativeInit();
        }
    

ID の検索を実行する C / C++ コード内に nativeClassInit メソッドを作成します。このコードは、クラスが初期化されたときに 1 回実行されます。クラスのアンロードと再ロードが行われると再び実行されます。

ローカル参照とグローバル参照

ネイティブ メソッドに渡されるすべての引数と、JNI 関数から返されるほぼすべてのオブジェクトは「ローカル参照」です。つまりこれらは、現在のスレッドで現在のネイティブ メソッドが存在する間、有効になります。 ネイティブ メソッドから戻った後もオブジェクト自体が存在し続ける場合でも、参照は無効になります。

これは、jobject のすべてのサブクラス(jclassjstringjarray など)に適用されます(拡張された JNI チェックが有効な場合、参照の誤用のほとんどがランタイムによって警告されます)。

非ローカル参照を取得する唯一の方法は、NewGlobalRef 関数と NewWeakGlobalRef 関数を使用する方法です。

参照期間を延ばしたい場合は、「グローバル」参照を使用する必要があります。NewGlobalRef 関数はローカル参照を引数として受け取り、グローバル参照を返します。グローバル参照は、DeleteGlobalRef を呼び出すまで有効であることが保証されます。

このパターンは、FindClass から返された jclass をキャッシュする場合によく使用されます。以下に例を示します。

jclass localClass = env->FindClass("MyClass");
    jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));

すべての JNI メソッドは、ローカル参照とグローバル参照の両方を引数として使用できます。同じオブジェクトへの参照が異なる値を持つことも可能です。たとえば、同じオブジェクトで NewGlobalRef を連続して呼び出した場合、戻り値が異なることがあります。 2 つの参照が同じオブジェクトを参照しているかどうかを確認するには、IsSameObject 関数を使用する必要があります。 ネイティブ コードで == を使用して参照を比較しないでください。

重要なのは、ネイティブ コードではオブジェクト参照が不変または一意であると仮定してはならないということです。オブジェクトを表す 32 ビット値は、連続する 2 回の同一メソッドの呼び出しで異なる場合があります。また、連続する呼び出しで 2 つの異なるオブジェクトが同じ 32 ビット値を持つこともあります。jobject の値をキーとして使用しないでください。

プログラマーは、ローカル参照を「過度に割り当てない」ようにする必要があります。実際には、多数のローカル参照を作成する場合は、おそらくオブジェクトの配列を使用して実行するでしょうが、参照の解放は JNI に任せるのではなく、DeleteLocalRef を使用して手動で行う必要があります。実装で必要なのは、16 個のローカル参照用のスロットを予約することだけです。そのため、ローカル参照がさらに必要な場合は、ローカル参照を適宜削除するか、EnsureLocalCapacity または PushLocalFrame を使用してスロットを追加で予約する必要があります。

jfieldIDjmethodID はオブジェクト参照ではなく不透明型のため、NewGlobalRef に渡してはなりません。GetStringUTFCharsGetByteArrayElements などの関数から返される元データのポインタもオブジェクトではありません(これらのポインタはスレッド間で渡されることがあり、対応する解放の呼び出しが行われるまで有効なままになります)。

例外的な事例について 1 点言及しておきます。AttachCurrentThread を使用してネイティブ スレッドをアタッチすると、実行中のコードはスレッドがデタッチされるまでローカル参照を自動的に解放しなくなります。作成したローカル参照はすべて、手動で削除する必要があります。一般に、ローカル参照をループで作成するネイティブ コードでは、まず間違いなく手動による削除が必要になります。

グローバル参照は気を付けて使用してください。グローバル参照の使用を避けられないこともありますが、デバッグするのが難しく、メモリの(不正な)動作の診断が困難になることもあります。他の条件がすべて同じであれば、グローバル参照を少なくすることをおすすめします。

UTF-8 および UTF-16 文字列

Java プログラミング言語では UTF-16 を使用します。便宜上、JNI には Modified UTF-8 でも動作するメソッドが用意されています。修正版のエンコードは、\u0000 を 0x00 ではなく 0xc0 0x80 としてエンコードするため、C 言語のコードでは便利です。これの長所は、C 言語スタイルのゼロ終止の文字列を使用でき、標準の libc 文字列関数で使用するのに適している点です。短所は、任意の UTF-8 データを JNI に渡すことができず、JNI が正常に動作することが期待できない点です。

可能な場合、通常は UTF-16 文字列のほうが高速に処理できます。Android では現在、GetStringChars ではコピーは不要ですが、GetStringUTFChars では割り当てと UTF-8 への変換が必要です。UTF-16 文字列はゼロ終止ではないことに注意してください。\u0000 が許可されるため、文字列長と jchar ポインタを保持する必要があります。

Get で取得した文字列は必ず Release で解放してください。文字列関数は jchar* または jbyte* を返します。これらはローカル参照ではなく、プリミティブ データを指す C 言語スタイルのポインタであり、Release が呼び出されるまで有効であることが保証されます。つまり、ネイティブ メソッドから戻っても、これらは解放されません。

NewStringUTF に渡すデータは Modified UTF-8 形式である必要があります。よくある誤りとして、ファイルまたはネットワーク ストリームから文字データを読み取る、文字データをフィルタリングせずに NewStringUTF に渡す、といったことがあります。データが有効な MUTF-8(または、互換性のあるサブセットである 7 ビット ASCII)であることがわかっている場合を除き、無効な文字を削除するか、適切な Modified UTF-8 フォームに変換する必要があります。そうしないと、UTF-16 変換が予期しない結果になる可能性があります。CheckJNI(エミュレータではデフォルトで有効になっています)は文字列をスキャンし、無効な入力を受け取ると VM を中断します。

プリミティブ配列

JNI には、配列オブジェクトの内容にアクセスするための関数が用意されています。オブジェクト配列では一度に 1 つのエントリにアクセスする必要があるのに対し、プリミティブ配列では、C 言語で宣言されているかのように読み取りと書き込みを直接行うことができます。

VM の実装に制約を加えることなく、インターフェースの効率性を可能な限り高めるために、Get<PrimitiveType>ArrayElements ファミリーの呼び出しでは、ランタイムが実際の要素へのポインタを返すか、メモリを割り当ててコピーを作成するかのいずれかを行います。どちらの方法でも、返された生のポインタは、対応する Release が呼び出されるまで有効であることが保証されます(つまり、データがコピーされなかった場合、配列オブジェクトが固定され、ヒープのコンパクト化の一環として再配置することができなくなります)。 Get で取得した配列はすべて、Release で解放する必要があります。 また、Get の呼び出しが失敗した場合でも、コードが後で Release によって NULL ポインタを解放しないようにする必要があります。

isCopy 引数に NULL でないポインタを渡すことで、データがコピーされたかどうかを確認できます。ただし、これが役に立つことはほとんどありません。

Release の呼び出しでは、以下の 3 つの値のいずれかを指定可能な mode 引数を受け取ります。ランタイムで実行されるアクションは、実際のデータへのポインタとデータのコピーのどちらが返されたかによって異なります。

  • 0
    • ポインタ: 配列オブジェクトの固定が解除されます。
    • コピー: データが再コピーされます。コピーが格納されたバッファが解放されます。
  • JNI_COMMIT
    • ポインタ: 何も行われません。
    • コピー: データが再コピーされます。コピーが格納されたバッファは解放されません
  • JNI_ABORT
    • ポインタ: 配列オブジェクトの固定が解除されます。それまでの書き込みは中止されません
    • コピー: コピーが格納されたバッファが解放されます。コピーに対する変更はすべて失われます。

isCopy フラグをチェックする理由の 1 つは、配列を変更した後に JNI_COMMIT を指定して Release を呼び出す必要があるかどうかを確認するためです。配列の変更と、配列の内容を使用するコードの実行を繰り返し行う場合には、「何もしない」コミットをスキップできる場合があります。このフラグをチェックする理由としてもう 1 つ考えられることは、JNI_ABORT の処理を効率的に行えるようにするためです。たとえば、配列を取得して変更を行い、その断片を他の関数に渡してから変更を破棄したい場合があります。JNI が新しいコピーを作成することがわかっていれば、「編集可能」なコピーをもう 1 つ作成する必要はありません。JNI からオリジナルが渡される場合は、独自のコピーを作成する必要があります。

*isCopy が false の場合には Release の呼び出しをスキップできると考えることはよくある誤りです(サンプルコードでたびたび見られます)。これは誤りです。コピーバッファが割り当てられなかった場合、元のメモリは固定され、ガベージ コレクタで移動することはできません。

また、JNI_COMMIT フラグを指定すると配列が解放されないため、最終的には別のフラグを指定して Release を再度呼び出す必要があります。

領域呼び出し

データをコピーするだけの場合は、Get<Type>ArrayElementsGetStringChars などに代わる非常に便利な方法があります。次のコードについて考えてみましょう。

    jbyte* data = env->GetByteArrayElements(array, NULL);
        if (data != NULL) {
            memcpy(buffer, data, len);
            env->ReleaseByteArrayElements(array, data, JNI_ABORT);
        }

このコードは、配列を取得し、その配列から最初の len バイトの要素をコピーして、配列を解放します。Get の呼び出しは実装に応じて、配列の内容を固定またはコピーします。このコードは、データのコピー(おそらく 2 回目)を行ってから Release を呼び出しています。ここでは、JNI_ABORT を指定することで、3 回目のコピーが行われないようにしています。

これと同じことをもっと簡単に実現することができます。

    env->GetByteArrayRegion(array, 0, len, buffer);

この方法には次のようなメリットがあります。

  • JNI の呼び出しが 2 回ではなく 1 回で済むため、オーバーヘッドを削減できます。
  • 固定や追加のデータコピーが必要ありません。
  • プログラマーのミスによるリスクを軽減できます。エラーの後に Release を呼び出すのを忘れるリスクがありません。

同様に、Set<Type>ArrayRegion を使用すると、データを配列にコピーすることができ、GetStringRegion または GetStringUTFRegion を使用すると、String から文字をコピーすることができます。

例外

例外の保留中は、ほとんどの JNI 関数を呼び出すことができません。コードでは(関数の戻り値、ExceptionCheckExceptionOccurred のいずれかで)例外を確認して戻るか、例外をクリアして処理することが期待されます。

例外の保留中に呼び出すことができる JNI 関数は以下だけです。

  • DeleteGlobalRef
  • DeleteLocalRef
  • DeleteWeakGlobalRef
  • ExceptionCheck
  • ExceptionClear
  • ExceptionDescribe
  • ExceptionOccurred
  • MonitorExit
  • PopLocalFrame
  • PushLocalFrame
  • Release<PrimitiveType>ArrayElements
  • ReleasePrimitiveArrayCritical
  • ReleaseStringChars
  • ReleaseStringCritical
  • ReleaseStringUTFChars

JNI の呼び出しの多くは例外をスローできますが、多くの場合、より簡単にエラーを確認する方法が用意されています。たとえば、NewString から NULL 以外の値が返された場合、例外を確認する必要はありません。ただし、(CallObjectMethod などの関数を使用して)メソッドを呼び出した場合、例外がスローされると戻り値が有効にならないため、例外を必ず確認する必要があります。

解釈されたコードからスローされた例外はネイティブ スタック フレームを巻き戻しません。また、Android は C++ の例外をまだサポートしていません。JNI の Throw 命令と ThrowNew 命令は、現在のスレッドで例外ポインタを設定するだけです。ネイティブ コードからマネージコードに戻り次第、例外が示され、その処理が適切に行われます。

ネイティブ コードは、ExceptionCheck または ExceptionOccurred を呼び出すことによって例外を「キャッチ」し、ExceptionClear を使用して例外をクリアすることができます。従来どおり、例外を処理せずに破棄すると、問題が発生することがあります。

Throwable オブジェクト自体を操作するための組み込み関数はないため、(たとえば)Throwable クラスを検索するのに必要な例外文字列を取得する場合は、getMessage "()Ljava/lang/String;" のメソッド ID を調べて呼び出します。この結果が NULL 以外の場合は、GetStringUTFChars を使用して、printf(3) または同等のメソッドに渡すことができる文字列を取得します。

拡張されたチェック機能

JNI ではエラーのチェックをほとんど行いません。エラーはたいてい、クラッシュを引き起こします。Android には CheckJNI と呼ばれるモードも用意されています。このモードでは、標準の実装を呼び出す前に、JavaVM および JNIEnv 関数テーブル ポインタが、一連の拡張されたチェックを実行する関数テーブルに切り替えられます。

追加されたチェックには以下のものがあります。

  • 配列: 負のサイズの配列を割り当てようとしている。
  • 不正なポインタ: 不正な jarray、jclass、jobject、jstring を JNI 呼び出しに渡している、または、NULL 非許容の引数で NULL ポインタを JNI 呼び出しに渡している。
  • クラス名: 「java/lang/String」スタイルのクラス名以外を JNI 呼び出しに渡している。
  • クリティカルな呼び出し: 「クリティカル」な get とそれに対応する release 間で JNI 呼び出しを行っている。
  • 直接バイトバッファ: 不正な引数を NewDirectByteBuffer に渡している。
  • 例外: 保留中の例外があるときに JNI 呼び出しを行っている。
  • JNIEnv*: 不適切なスレッドから JNIEnv* を使用している。
  • jfieldID: NULL の jfieldID を使用している、jfieldID を使用してフィールドを不適切な型の値に設定している(たとえば、StringBuilder を文字列フィールドに割り当てようとしている)、静的フィールドの jfieldID を使用してインスタンス フィールドを設定している(またはその逆)、あるクラスの jfieldID を別のクラスのインスタンスで使用している。
  • jmethodID: Call*Method JNI 呼び出しの際に不適切なタイプの jmethodID を使用している(不正な戻り値の型、静的 / 非静的の不一致、「this」に対する不適切な型(非静的な呼び出しの場合)または不適切なクラス(静的な呼び出しの場合)など)。
  • 参照: 不適切なタイプの参照で DeleteGlobalRef または DeleteLocalRef を使用している。
  • 解放モード: 解放の呼び出しに対して不正な解放モード(0JNI_ABORTJNI_COMMIT 以外のモード)を渡している。
  • 型の安全性: ネイティブ メソッドから互換性のない型を返している(たとえば、文字列を返すように宣言されているメソッドから StringBuilder を返している)。
  • UTF-8: Modified UTF-8 の無効なバイト シーケンスを JNI 呼び出しに渡している。

(メソッドおよびフィールドへのアクセスが可能かどうかは、今でもチェックされていません。アクセスの制限はネイティブ コードには適用されません。)

CheckJNI を有効にする方法はいくつかあります。

エミュレータを使用する場合、CheckJNI はデフォルトで有効になっています。

ユーザーにルート権限のあるデバイスでは、以下の一連のコマンドを使用することにより、CheckJNI を有効にしてランタイムを再起動できます。

adb shell stop
    adb shell setprop dalvik.vm.checkjni true
    adb shell start

いずれの場合でも、ランタイムの起動時に、logcat に次のようなメッセージが出力されます。

D AndroidRuntime: CheckJNI is ON

通常のデバイスでは、次のコマンドを使用できます。

adb shell setprop debug.checkjni 1

このコマンドを実行しても、すでに実行中のアプリでは CheckJNI は有効になりませんが、その後に起動したアプリでは CheckJNI が有効になります(プロパティを他の値に変更するか、再起動すると、CheckJNI が再び無効になります)。この場合、アプリを次に起動したときに、logcat に次のようなメッセージが出力されます。

D Late-enabling CheckJNI

アプリのマニフェストで CheckJNI を有効にするように android:debuggable 属性を設定することも可能です。Android ビルドツールは、特定のビルドタイプに対してこの処理を自動的に行います。

ネイティブ ライブラリ

標準の System.loadLibrary を使用して、共有ライブラリからネイティブ コードをロードすることができます。

実際のところ、古いバージョンの Android には PackageManager にバグがあり、それが原因でネイティブ ライブラリのインストールとアップデートの信頼性が低下していました。この問題やその他のネイティブ ライブラリのロードに関する問題の回避策が ReLinker プロジェクトから提供されています。

System.loadLibrary(または ReLinker.loadLibrary)は、静的クラス イニシャライザから呼び出します。引数は「装飾されていない」ライブラリ名であるため、libfubar.so をロードするには "fubar" を渡します。

ランタイムでネイティブ メソッドを見つける方法には、RegisterNatives でネイティブ メソッドを明示的に登録する方法と、dlsym を使用してランタイムで動的に検索する方法の 2 つがあります。RegisterNatives のメリットは、シンボルが存在することを事前に確認できることと、JNI_OnLoad 以外をエクスポートしないようにすることで共有ライブラリのコンパクト化と高速化が可能であることです。ランタイムで関数を見つけられるようにするメリットは、作成するコードをいくらか少なくできる点にあります。

RegisterNatives を使用するには以下を行います。

  • JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) 関数を提供します。
  • JNI_OnLoadRegisterNatives を使用して、ネイティブ メソッドをすべて登録します。
  • ライブラリから JNI_OnLoad のみがエクスポートされるように、-fvisibility=hidden を指定してビルドします。これにより、コードが高速化およびコンパクト化され、アプリにロードされた他のライブラリとの競合の可能性を回避できます(ただし、ネイティブ コード内でアプリがクラッシュすると、作成される有用なスタック トレースが少なくなります)。

静的イニシャライザは次のようになります。

Kotlin

    companion object {
        init {
            System.loadLibrary("fubar")
        }
    }
    

Java

    static {
        System.loadLibrary("fubar");
    }
    

C++ で作成した場合、JNI_OnLoad 関数は次のようになります。

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
        JNIEnv* env;
        if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
            return -1;
        }

        // Get jclass with env->FindClass.
        // Register methods with env->RegisterNatives.

        return JNI_VERSION_1_6;
    }

代わりにネイティブ メソッドの「検出」を使用する場合は、特定の方法でネイティブ メソッドに名前を付ける必要があります(詳しくは、JNI の仕様をご覧ください)。つまり、メソッドのシグネチャが誤っている場合、メソッドが実際に初めて呼び出されるまで、そのことがわからないということになります。

ネイティブ メソッドを持つクラスが 1 つだけの場合、System.loadLibrary の呼び出しをそのクラス内で行うようにすることは理にかなっています。そうでない場合はおそらく、Application から呼び出しを行う必要があります。これにより、ライブラリが必ずロードされ、しかも必ず早い段階でロードされるようになります。

FindClass の呼び出しを JNI_OnLoad から行うと、共有ライブラリのロードに使用されたクラスローダーのコンテキスト内でクラスが解決されます。FindClass は通常、Java スタックの最上部でメソッドに関連付けられているローダーを使用します。または、(スレッドがちょうどアタッチされていたことが原因で)そのようなローダーがない場合は、「システム」クラスローダーを使用します。これにより、JNI_OnLoad から簡単にクラス オブジェクト参照の検索とキャッシュを行えるようになります。

64 ビットに関する考慮事項

64 ビットポインタを使用するアーキテクチャをサポートするには、ポインタを Java フィールドのネイティブ構造体に格納するときに、int フィールドではなく long フィールドを使用します。

サポートされていない機能と下位互換性

JNI 1.6 の機能がすべてサポートされています。ただし、以下の例外があります。

  • DefineClass は実装されていません。Android は Java バイトコードまたはクラスファイルを使用しないため、バイナリクラス データを渡すことができません。

古い Android リリースとの下位互換性を維持するには、以下に注意する必要があります。

  • ネイティブ関数の動的検索

    Android 2.0(Eclair)までは、メソッド名の検索時に文字「$」が「_00024」に正しく変換されませんでした。この問題を回避するには、明示的な登録を使用するか、内部クラスからネイティブ メソッドを移動する必要があります。

  • スレッドのデタッチ

    Android 2.0(Eclair)までは、pthread_key_create のデストラクタ関数を使用して、「スレッドが終了する前のデタッチ」のチェックを回避することができませんでした(ランタイムでも pthread key のデストラクタ関数を使用するため、どちらが先に呼び出されるかは不定です)。

  • 弱いグローバル参照

    Android 2.2(Froyo)までは、弱いグローバル参照が実装されていませんでした。古いバージョンでは、弱いグローバル参照を使用しようとすると強く拒否されます。Android プラットフォームのバージョン定数を使用して、弱いグローバル参照がサポートされているかどうかをテストできます。

    Android 4.0(Ice Cream Sandwich)までは、弱いグローバル参照を渡すことができるのは、NewLocalRefNewGlobalRefDeleteWeakGlobalRef のみでした(この仕様により、プログラマーには、弱いグローバル参照を使用してなんらかの処理を行う前に弱いグローバル参照に対するハード参照を作成して、その処理が制限をまったく受けないようにすることが強く推奨されています)。

    Android 4.0(Ice Cream Sandwich)以降では、弱いグローバル参照を他の JNI 参照と同じように使用できます。

  • ローカル参照

    Android 4.0(Ice Cream Sandwich)までは、ローカル参照は実際のところ、直接ポインタでした。Ice Cream Sandwich において、より効率的なガベージ コレクタをサポートするために必要な間接参照が追加されましたが、これは、以前のリリースでは JNI の多くのバグを検出できないことを意味しています。詳しくは、ICS での JNI ローカル参照の変更点をご覧ください。

    Android 8.0 より前のバージョンの Android では、ローカル参照の数にバージョン固有の上限があります。Android 8.0 以降では、Android はローカル参照を無制限にサポートしています。

  • GetObjectRefType による参照タイプの特定

    Android 4.0(Ice Cream Sandwich)までは直接ポインタが使用されていたため(上記を参照)、GetObjectRefType を正しく実装することができませんでした。その代わり、弱いグローバル参照テーブル、引数、ローカル参照テーブル、グローバル参照テーブルの順に調べるヒューリスティックが使用されていました。そのため、直接ポインタを初めて検出したときにたまたま調べていたタイプが参照のタイプとして報告されます。これはたとえば、GetObjectRefType の呼び出し元のグローバルな jclass が、暗黙的な引数として静的なネイティブ メソッドに渡された jclass とたまたま同じだった場合、JNIGlobalRefType ではなく JNILocalRefType を取得することを意味します。

よくある質問: UnsatisfiedLinkError エラーが発生します。なぜですか?

ネイティブ コードの処理中に、次のようなエラー メッセージが表示されることがよくあります。

java.lang.UnsatisfiedLinkError: Library foo not found

このエラー メッセージは、ライブラリが見つからなかった、または、ライブラリは存在するが dlopen(3) で開くことができなかったことを示します。エラーの詳細は例外の詳細メッセージで確認できます。

「ライブラリが見つからない」例外が発生する一般的な理由には以下のようなものがあります。

  • ライブラリが存在しないか、アプリからアクセスできない。ライブラリが存在し、アクセスできることを確認するには、adb shell ls -l <path> を使用します。
  • ライブラリの構築に NDK が使用されていない。その結果、デバイス上に存在しない関数やライブラリに依存することになる可能性があります。

UnsatisfiedLinkError エラーのもう 1 つのクラスを以下に示します。

java.lang.UnsatisfiedLinkError: myfunc
            at Foo.myfunc(Native Method)
            at Foo.main(Foo.java:10)

logcat には次のようなメッセージが出力されます。

W/dalvikvm(  880): No implementation found for native LFoo;.myfunc ()V

このメッセージは、ランタイムが一致するメソッドを見つけようとしたが見つからなかったことを示しています。このエラーが発生する一般的な理由には以下のようなものがあります。

  • ライブラリがロードされていない。logcat の出力でライブラリのロードに関するメッセージを確認します。
  • 名前またはシグネチャの不一致により、メソッドが見つからない。この一般的な原因は以下のとおりです。
    • メソッドの検索が不十分な場合、extern "C" と適切な可視性(JNIEXPORT)を指定して C++ 関数を宣言することができない。Ice Cream Sandwich より前のバージョンでは JNIEXPORT マクロに誤りがあったため、新しい GCC と古い jni.h を併用することができませんでした。arm-eabi-nm を使用すると、ライブラリ内と同じようにシンボルを参照できます。シンボルが壊れているように見える(たとえば、Java_Foo_myfunc ではなく _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass のようになっている)場合や、シンボルのタイプが大文字「T」ではなく小文字「t」になっている場合は、宣言を調整する必要があります。
    • 明示的な登録で、メソッド シグネチャの入力時に軽微なエラーが発生する。登録の呼び出しに渡す内容がログファイル内のシグネチャと一致していることを確認します。「B」は byte、「Z」は boolean を表します。シグネチャ内のクラス名のコンポーネントは、「L」で始まり「;」で終わります。パッケージ名とクラス名を分離するには「/」を使用し、内部クラスの名前を分離するには「$」を使用します(例: Ljava/util/Map$Entry;)。

javah を使用して JNI ヘッダーを自動生成すると、一部の問題の発生を回避できる場合があります。

よくある質問: FindClass でクラスを見つけられませんでした。なぜですか?

(以下のアドバイスのほとんどは、GetMethodID または GetStaticMethodID でのメソッドの検出エラーと、GetFieldID または GetStaticFieldID でのフィールドの検出エラーに等しく適用できます。)

クラス名の文字列の形式が正しいことを確認します。JNI クラスの名前はパッケージ名で始まり、スラッシュで区切られます(例: java/lang/String)。配列クラスの検索では、最初に適切な数の角かっこを指定し、「L」と「;」でクラスをラップする必要があります。たとえば、1 次元配列の String の場合、[Ljava/lang/String; のように指定します。内部クラスの検索では、「.」ではなく「$」を使用します。一般に、クラスの内部名を確認する場合は、.class ファイルで javap を使用することをおすすめします。

ProGuard を使用する場合は、ProGuard によってクラスが削除されていないことを確認します。これは、クラス、メソッド、フィールドが JNI でのみ使用されている場合に起こり得ます。

クラス名が正しいように見える場合は、クラスローダーに問題がある可能性があります。FindClass は、コードに関連付けられているクラスローダー内でクラスの検索を開始して、コールスタックを調べます。これは次のようになります。

    Foo.myfunc(Native Method)
        Foo.main(Foo.java:10)

最初のメソッドは Foo.myfunc です。FindClass は、Foo クラスに関連付けられている ClassLoader オブジェクトを見つけて使用します。

通常はこれで間に合いますが、スレッドを自分で作成した場合は(pthread_create の呼び出し後、AttachCurrentThread でスレッドをアタッチするなど)、問題が生じることがあります。この時点では、アプリからのスタック フレームはありません。このスレッドから FindClass を呼び出すと、アプリに関連付けられているクラスローダーではなく「システム」クラスローダーで JavaVM が起動されるため、アプリに固有のクラスを見つけようとしても失敗します。

これを回避する方法はいくつかあります。

  • FindClass の検索を JNI_OnLoad で 1 回行って、後で使用するクラス参照をキャッシュします。JNI_OnLoad の実行の一環として行われる FindClass の呼び出しでは、System.loadLibrary を呼び出した関数に関連付けられているクラスローダーが使用されます(これは、ライブラリの初期化をより簡便にするために規定された特別なルールです)。アプリのコードでライブラリをロードしている場合、FindClass は適切なクラスローダーを使用します。
  • Class 型の引数を受け取るネイティブ メソッドを宣言し、Foo.class を渡すことにより、クラスのインスタンスを必要とされる関数に渡します。
  • ClassLoader オブジェクトへの参照をどこか便利な場所にキャッシュして、loadClass を直接呼び出します。これにはいくらか手間がかかります。

よくある質問: 元データをネイティブ コードと共有するにはどうすればよいですか?

気が付くと、マネージコードとネイティブ コードの両方から元データの大規模なバッファにアクセスする必要がある状況になっていることがあります。一般的な例として、ビットマップや音のサンプルの操作などがあります。こうした状況に対する基本的なアプローチには 2 種類あります。

データは byte[] に格納できます。これにより、マネージコードから非常に高速にアクセスできます。ただしネイティブ コードでは、データをコピーせずにデータにアクセスできるとは限りません。ある実装では、GetByteArrayElementsGetPrimitiveArrayCritical がマネージヒープ内の元データへの実際のポインタを返しますが、別の実装では、ネイティブ ヒープ上にバッファを割り当ててデータをコピーします。

もう 1 つのアプローチでは、データを直接バイトバッファに格納します。直接バイトバッファは、java.nio.ByteBuffer.allocateDirect または JNI の NewDirectByteBuffer 関数を使用して作成できます。通常のバイトバッファとは異なり、ストレージがマネージヒープ上に割り当てられないため、常にネイティブ コードから直接アクセスできます(アドレスは GetDirectBufferAddress で取得します)。直接バイトバッファへのアクセスの実装方法によっては、マネージコードからデータへのアクセスが非常に低速になることがあります。

どちらのアプローチを使用するかは、以下の 2 つの要因によって決まります。

  1. データへのアクセスが多いのは、Java と C / C++ のどちらで作成されたコードか。
  2. データが最終的にシステム API に渡される場合、どのフォームにする必要があるか(たとえば、データが最終的に、byte[] を受け取る関数に渡される場合、直接 ByteBuffer で処理を行うことは賢明でないかもしれません)。

どちらのアプローチが優れているかが明確でない場合は、直接バイトバッファを使用します。これらのアプローチに対するサポートを JNI に直接組み込むことで、将来のリリースでパフォーマンスが向上するはずです。