JNI に関するヒント

JNI(Java Native Interface)では、Android のコンパイル元となるバイトコードの方式を定義します。 ネイティブ コードとやり取りするためのマネージド コード(Java または Kotlin プログラミング言語で記述されたもの) (C/C++ で記述)JNI はベンダーに依存せず、動的共有からのコードの読み込みをサポートしている 煩雑になることもありますが、ある程度効率的です。

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

まだよくご存じない場合は、 Java Native Interface 仕様 JNI の仕組みと使用可能な機能について理解してください。一部 インターフェースの構成要素が 以降のセクションが役に立ちます。

グローバル JNI 参照を参照して、グローバル JNI 参照が作成、削除される場所を確認するには、次のコマンドを使用します。 Memory Profiler の [JNI heap] ビュー Android Studio 3.2 以降で利用できます。

一般的なヒント

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

  • JNI レイヤ全体でリソースのマーシャリングを最小限に抑える。マーシャリング JNI レイヤには重要なコストがかかりますインターフェースを設計する際は、マーシャリングの必要があるデータの量と、データをマーシャリングする頻度を最小限に抑えるようにしてください。
  • 可能な限り、マネージ プログラミング言語で記述したコードと C++ で記述したコードとの間で非同期通信を行わないようにする。これにより、JNI インターフェースの管理が容易になります。通常は、非同期のリクエストを単純化し、 非同期更新を UI と同じ言語で維持することで、UI を更新する。たとえば、 Java コードの UI スレッドから C++ 関数を JNI 経由で呼び出すには、 Java プログラミング言語の 2 つのスレッド間でコールバックを実行します。 ブロッキング C++ 呼び出しを行い、ブロッキング呼び出しが発生したときに UI スレッドに通知する できます。
  • JNI を操作する、または JNI から操作される必要があるスレッドの数を最小限に抑える。Java と C++ の両方の言語でスレッドプールを使用する必要がある場合、JNI とのやり取りはプールの所有者とのみ行うようにし、個々のワーカー スレッドとは行わないようにしてください。
  • インターフェースのコードを、見つけやすいように C++ と Java のソースコードのなるべく上の方に配置すると、将来のリファクタリングが容易になる。必要に応じて、JNI 自動生成ライブラリを使用することをおすすめします。

JavaVM と JNIEnv

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

JNIEnv はほとんどの JNI 関数を提供します。ネイティブ関数はすべて、JNIEnv を 最初の引数(@CriticalNative メソッドを除く) ネイティブ呼び出しの高速化をご覧ください。

JNIEnv は、スレッド ローカル ストレージ用です。このため、スレッド間で JNIEnv を共有することはできません。 コードが JNIEnv を取得する方法がほかになければ、JavaVM を共有し、GetEnv を使用してスレッドの JNIEnv を検出するようにしてください(JNIEnv を取得する方法がほかにある場合は、下記の AttachCurrentThread をご覧ください)。

JNIEnv と JavaVM の C 宣言は、 あります。"jni.h" インクルード ファイルには、さまざまな typedef があります。 これは C と C++ のどちらに含まれているかによって異なります。そのため、 両方の言語に含まれるヘッダー ファイルに JNIEnv 引数を含める。(言い換えると、 ヘッダー ファイルには #ifdef __cplusplus が必要です。 JNIEnv を参照しています)。

スレッド

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

通常は、Thread.start() を使用して、Java コードに呼び出す必要のあるスレッドを作成することをおすすめします。そうすることで、十分なスタック スペースを確保して、 正しい ThreadGroup にあり、同じ ClassLoader を使用していること 使用できます。また、デバッグしやすいようにスレッド名を設定する場合も、ネイティブ コードからより Java で設定する方が簡単です(pthread_t または thread_t を使用している場合は pthread_setname_np() をご覧ください。std::thread を使用していて、pthread_t を必要とする場合は std::thread::native_handle() をご覧ください)。

ネイティブに作成されたスレッドをアタッチすると、java.lang.Thread オブジェクトが作成されて「メイン」の ThreadGroup に追加され、デバッガから認識できるようになります。AttachCurrentThread() に発信中 タスクを実行することは何もしません。

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 は 内部ランタイム データ構造体へのポインタ。検索には複数の文字列が必要になる場合があります ただし、フィールドを取得したり、メソッドを呼び出したりするための実際の呼び出しは、 非常に高速です。

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

クラス参照、フィールド 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();
    }

C / C++ コード内に ID のルックアップを実行する nativeClassInit メソッドを作成します。コード は、クラスの初期化時に 1 回実行されます。クラスがアンロードされ、 再読み込みすると再度実行されます。

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

すべての引数がネイティブ メソッドに渡され、ほぼすべてのオブジェクトが 「ローカル参照」です。つまり、この ID は 現在のスレッド内にある現在のネイティブ メソッドの継続時間。 たとえオブジェクト自体がネイティブ メソッドの 参照が無効となります。

これは、以下を含む jobject のすべてのサブクラスに適用されます。 jclassjstringjarray。 (JNI を拡張すると、参照の誤使用のほとんどについてランタイムが警告するようになります)。 チェックが有効になります)。

非ローカル参照を取得するには、NewGlobalRef 関数と NewWeakGlobalRef 関数を使用する必要があります。

参照を長期間保持したい場合は、 「グローバル」ご覧ください。NewGlobalRef 関数はローカル参照を引数として受け取り、グローバル参照を返します。グローバル参照は、 DeleteGlobalRef

このパターンは、返された jclass をキャッシュするときに FindClass から。例:

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

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

その結果 オブジェクト参照が定数または一意であると想定してはいけません 使用できます。オブジェクトを表す値は あるメソッドの呼び出しから次のメソッドの呼び出しまでです。また、2 つのケースが 連続した呼び出しで同じ値を持つオブジェクトが複数存在する場合があります。使用不可 jobject 値をキーとして使用します。

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

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

注意すべき特殊な状況について言及しておきます。ネイティブ コンテナを AttachCurrentThread で呼び出されると、実行しているコードは スレッドがデタッチするまでローカル参照を自動的に解放することはありません。すべてのローカル 作成した参照は手動で削除する必要があります。一般的にネイティブ ループ内でローカル参照を作成するコードの場合、通常は 削除されます。

グローバル参照は気を付けて使用してください。グローバル参照は避けられないが、難しい メモリの誤動作を引き起こす可能性があります。他の条件がすべて同じであれば、グローバル参照を少なくすることをおすすめします。

UTF-8 文字列と UTF-16 文字列

Java プログラミング言語は UTF-16 を使用します。利便性を考えて、JNI には 修正 UTF-8 も同様です。「 修正されたエンコーディングは、\u0000 を 0x00 ではなく 0xc0 0x80 としてエンコードするため、C コードに役立ちます。 メリットは、C 言語スタイルのゼロ終端文字列を使用できることで、標準の libc 文字列関数で使用するのに適しています。デメリットは、JNI に渡した UTF-8 データがすべて正しく動作すると期待できない点です。

String の UTF-16 表現を取得するには、GetStringChars を使用します。なお、UTF-16 の文字列はゼロで終端されません。\u0000 も使用できます。 そのため、文字列の長さと jchar ポインタを保持する必要があります。

Get で取得した文字列は、必ず Release で解放するようにしてください。「 文字列関数は jchar* または jbyte* を返します。 ローカル参照ではなく、プリミティブ データへの C スタイルのポインタです。Google Release が呼び出されるまで有効であることが保証されます。つまり、 解放されません。

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

Android 8 より前は、Android で GetStringChars のコピーが不要だったため、通常は UTF-16 文字列で処理する方が高速でした。一方、GetStringUTFChars では割り当てと UTF-8 への変換が必要でした。Android 8 では、String の表現を変更し、ASCII 文字列に文字あたり 8 ビットを使用するようにしました(メモリを節約するため)。また、移動ガベージ コレクタの使用を開始しました。これらの機能により、ART が コピーを作成しなくても、String データへのポインタを提供できます。 (GetStringCritical)ただし、コードで処理される文字列のほとんどが短い場合は、スタックに割り振られたバッファと GetStringRegion または GetStringUTFRegion を使用して、ほとんどの場合、割り当てと割り当て解除を回避できます。例:

    constexpr size_t kStackBufferSize = 64u;
    jchar stack_buffer[kStackBufferSize];
    std::unique_ptr<jchar[]> heap_buffer;
    jchar* buffer = stack_buffer;
    jsize length = env->GetStringLength(str);
    if (length > kStackBufferSize) {
      heap_buffer.reset(new jchar[length]);
      buffer = heap_buffer.get();
    }
    env->GetStringRegion(str, 0, length, buffer);
    process_data(buffer, length);

プリミティブ配列

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

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

isCopy 引数に非 null ポインタを渡すことにより、データがコピーされたかどうかを判断できます。これはめったに 便利です。

Release 呼び出しは mode 引数を取り、 3 つの値のいずれかになりますランタイムが実行するアクションは、 返されたものが実際のデータへのポインタなのか、データのコピーが返された場合であってもです。

  • 0
    • 実際: 配列オブジェクトの固定が解除されています。
    • コピー: データがコピーバックされます。コピーを格納したバッファが解放されます。
  • JNI_COMMIT
    • 実際: 何もしません。
    • コピー: データがコピーバックされます。コピーを含むバッファ 解放されません
  • JNI_ABORT
    • ポインタ: 配列オブジェクトの固定が解除されます。それ以前 中止されません
    • コピー: コピーを格納したバッファが解放されます。変更内容はすべて失われます

isCopy フラグをチェックする理由の 1 つは、配列に変更を加えた後に JNI_COMMIT を指定して Release を呼び出す必要があるかどうかを確認することです。配列の変更と、配列のコンテンツを使用するコードの実行を交互に行う場合、no-op(何もしない)コミットをスキップできる場合があります。フラグを確認するもう 1 つの理由として、 JNI_ABORT を効率的に処理。たとえば 配列を取得してその位置を変更し、他の関数に断片を渡して、 変更を破棄しますJNI が新しいコピーを作成していることがわかっている場合は、 編集可能なコピーします。JNI が コピーを作成する必要があります

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

また、JNI_COMMIT フラグを指定しても配列は解放されません。 別のフラグを指定して Release を再度呼び出す必要があります。 あります。

領域呼び出し

Get<Type>ArrayElements のような呼び出しに代わる方法 と GetStringChars は、必要なすべての処理を行う場合に データのコピーです以下の点を考慮してください。

    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 または GetStringUTFRegionString

例外

例外の保留中は、ほとんどの 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++ 例外は、C++ コードからマネージド コードへの JNI 遷移境界を越えてスローしてはなりません)。JNI の Throw 命令と ThrowNew 命令は、現在のスレッド内で例外ポインタを設定するだけです。ネイティブ コードからマネージコードに戻り次第、例外が認識され適切に処理されます。

ネイティブコードはExceptionCheck を呼び出すか、例外を ExceptionOccurred です。これを ExceptionClear。従来どおり、例外を処理せずに破棄すると、問題が発生することがあります。

Throwable オブジェクトを操作する組み込み関数がない そのため、たとえば例外文字列を取得したい場合は、 Throwable クラスを見つけて、そのメソッド ID を getMessage "()Ljava/lang/String;" を呼び出してこれを呼び出し、結果が 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 を使用している。
  • Release のモード: Release 呼び出しに対して正しくないモード(0JNI_ABORTJNI_COMMIT 以外のモード)を渡している。
  • 型安全性: ネイティブ メソッドから互換性のない型を返している(たとえば、String を返すように宣言されているメソッドから StringBuilder を返している)。
  • UTF-8: Modified UTF-8 の無効なバイト シーケンスを JNI 呼び出しに渡している。

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

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

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

ユーザーに root 権限のあるデバイスでは、以下の一連のコマンドを使用することにより、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

また、アプリのマニフェストで android:debuggable 属性を次のように設定することもできます。 アプリに対してのみ CheckJNI をオンにします。ビルドツールは Android ビルドツールによって自動的に ビルドタイプによって異なります。

ネイティブ ライブラリ

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

実際には、古いバージョンの Android には PackageManager にバグがあり、インストールと 更新されていたためです。この問題や他のネイティブ ライブラリのロードに関する問題については、ReLinker プロジェクトから対応策が提供されています。

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

ネイティブ メソッドを持つクラスが 1 つだけの場合は、 System.loadLibrary をそのクラスの静的イニシャライザに指定します。そうでない場合は、Application から呼び出しを行うことをおすすめします。そうすれば、ライブラリは必ず、しかも常に早い段階でロードされます。

ランタイムがネイティブ メソッドを見つける方法には、明示的に行うか、 RegisterNatives で登録するか、ランタイムに動的にルックアップさせることもできます。 dlsym に置き換えます。RegisterNatives の利点は、事前に シンボルが存在するかどうかをチェックします。また、共有ライブラリを小さくして高速に JNI_OnLoad 以外をエクスポートします。ランタイムにコードを 記述するコードが少なくて済みます

RegisterNatives を使用するには:

  • JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) 関数を指定します。
  • JNI_OnLoad で、RegisterNatives を使用してすべてのネイティブ メソッドを登録します。
  • バージョン スクリプトを使用してビルドするか、-fvisibility=hidden を使用して、ライブラリから JNI_OnLoad のみがエクスポートされるようにします。これによりコードの処理が速くなり、コードのサイズが小さくなります。また、 アプリに読み込まれた他のライブラリとの競合(ただし、スタック トレースの有用性が低くなる) 。

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

Kotlin

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

Java

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

次の場合、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 JNI_ERR;
    }

    // Find your class. JNI_OnLoad is called from the correct class loader context for this to work.
    jclass c = env->FindClass("com/example/app/package/MyClass");
    if (c == nullptr) return JNI_ERR;

    // Register your class' native methods.
    static const JNINativeMethod methods[] = {
        {"nativeFoo", "()V", reinterpret_cast<void*>(nativeFoo)},
        {"nativeBar", "(Ljava/lang/String;I)Z", reinterpret_cast<void*>(nativeBar)},
    };
    int rc = env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod));
    if (rc != JNI_OK) return rc;

    return JNI_VERSION_1_6;
}

代わりに「Discovery」を使うためネイティブ メソッドを使う場合は、特定の方法で名前を付ける必要があります( JNI の仕様 をご覧ください)。つまり、メソッドのシグネチャが間違っている場合、 メソッドが初めて呼び出されたときです

FindClass 呼び出しを JNI_OnLoad から行うと、共有ライブラリのロードに使用されたクラスローダーのコンテキスト内でクラスが解決されます。他のプロバイダから呼び出された場合 コンテキストの中で、FindClass はメソッドの先頭にあるメソッドに関連付けられたクラスローダーを使用します。 Java スタック、または存在しない場合(アタッチされたばかりのネイティブ スレッドからの呼び出しであるため) 「system」を使用してクラスローダーを使用します。システム クラスローダーは、アプリケーションの クラスを検索することはできません。FindClass を使用して自分のクラスを 説明します。クラスのルックアップとキャッシュを行う場所として JNI_OnLoad が都合がいいのはそのためです。有効な jclass グローバル参照があれば、アタッチされているどのスレッドからでも使用できるからです。

@FastNative@CriticalNative によるネイティブ呼び出しの高速化

ネイティブ メソッドに @FastNative または @CriticalNative のアノテーションを付けると(両方ではなく)、マネージド コードとネイティブ コード間の遷移を高速化できます。ただし、これらのアノテーションは 動作に特定の変化が伴うため、使用前に慎重に検討する必要があります。Google では、 以下に変更点について簡単に説明します。詳細についてはドキュメントをご覧ください。

@CriticalNative アノテーションは、マネージド オブジェクト(パラメータまたは戻り値、または暗黙的な this として)を使用しないネイティブ メソッドにのみ適用できます。このアノテーションは JNI 遷移 ABI を変更します。ネイティブ実装では 関数のシグネチャからの JNIEnv パラメータと jclass パラメータ。

@FastNative メソッドまたは @CriticalNative メソッドの実行中に、 重要な処理のためにスレッドを一時停止することはできず、ブロックされる可能性があります。これらは使用しないでください 長時間実行メソッド用の アノテーション(通常は高速だが、一般的に無制限のメソッドを含む) 特に、コードで重要な I/O オペレーションを実行したり、コードによって変更されるネイティブ ロックを取得したりしないようにする必要があります。 保持できます。

これらのアノテーションは、 Android 8 CTS テスト済みの一般公開となりました。 Android 14 の API です。これらの最適化は、Android 8 ~ 13 のデバイスでも機能する可能性が高い(ただし、 強力な CTS 保証がない)ですが、ネイティブ メソッドの動的ルックアップは Android 12 以降では、JNI RegisterNatives による明示的な登録が厳密に必須です Android 8 ~ 11 で動作するアプリです。これらのアノテーションは Android 7 以降では無視されます。@CriticalNative の ABI が一致しないと、引数のマーシャリングが正しく行われず、クラッシュする可能性があります。

パフォーマンスが重視されるメソッドでこのようなアノテーションが必要になる場合は、 メソッドを依存する代わりに、JNI RegisterNatives を使用してメソッドを明示的に登録します。 名前ベースの「検出」いくつかあります。アプリの起動のパフォーマンスを最適化するには、 @FastNative メソッドまたは @CriticalNative メソッドの呼び出し元を ベースライン プロファイル。Android 12 以降 コンパイル済みのマネージド メソッドから @CriticalNative ネイティブ メソッドを呼び出すと、 すべての引数がレジスタに収まる限り(たとえば arm64 では 8 つの整数引数と最大 8 つの浮動小数点引数)。

場合によっては、ネイティブ メソッドを 2 つに分割した方がよいこともあります。これは、 もう 1 つは低速のケースに対応するものです例:

Kotlin

fun writeInt(nativeHandle: Long, value: Int) {
    // A fast buffered write with a `@CriticalNative` method should succeed most of the time.
    if (!nativeTryBufferedWriteInt(nativeHandle, value)) {
        // If the buffered write failed, we need to use the slow path that can perform
        // significant I/O and can even throw an `IOException`.
        nativeWriteInt(nativeHandle, value)
    }
}

@CriticalNative
external fun nativeTryBufferedWriteInt(nativeHandle: Long, value: Int): Boolean

external fun nativeWriteInt(nativeHandle: Long, value: Int)

Java

void writeInt(long nativeHandle, int value) {
    // A fast buffered write with a `@CriticalNative` method should succeed most of the time.
    if (!nativeTryBufferedWriteInt(nativeHandle, value)) {
        // If the buffered write failed, we need to use the slow path that can perform
        // significant I/O and can even throw an `IOException`.
        nativeWriteInt(nativeHandle, value);
    }
}

@CriticalNative
static native boolean nativeTryBufferedWriteInt(long nativeHandle, int value);

static native void nativeWriteInt(long nativeHandle, int value);

64 ビットに関する注意事項

64 ビットポインタを使用するアーキテクチャをサポートするには、long int(Java フィールドにネイティブ構造体へのポインタを格納する場合)。

サポートされていない機能と後方互換性

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

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

以前の Android リリースとの下位互換性のために、次のことが必要になる場合があります。 次の点に留意してください。

  • ネイティブ関数の動的ルックアップ

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

  • スレッドのデタッチ

    Android 2.0(Eclair)までは、pthread_key_create を使用できませんでした デストラクタ関数を使用して、このデストラクタ関数を使用して 終了」確認します。(ランタイムは pthread キーデストラクタ関数も使用し、 どちらが最初に呼び出されるかで競い合うことになります)。

  • 弱いグローバル参照

    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 のバグは、古いリリースでは検出できません。詳しくは、 <ph type="x-smartling-placeholder"></ph> ICS での JNI ローカル参照の変更をご覧ください。

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

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

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

  • @FastNative@CriticalNative

    Android 7 までは、これらの最適化アノテーションは無視されます。@CriticalNative の ABI の不一致により、引数のマーシャリングが誤りになり、クラッシュする可能性があります。

    @FastNative@CriticalNative メソッドは Android 8 ~ 10 では実装されておらず、 Android 11 の既知のバグが含まれています。これらの最適化を JNI RegisterNatives への明示的な登録は、 Android 8 ~ 11 でクラッシュが発生します。

  • FindClassClassNotFoundException をスローする

    下位互換性を確保するため、Android は ClassNotFoundException をスローします。 NoClassDefFoundError の代わりに FindClass。この動作は Java リフレクション API と一致しています。 Class.forName(name)

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

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

java.lang.UnsatisfiedLinkError: Library foo not found

このエラー メッセージは、ライブラリが見つからなかった場合や、イン ライブラリは存在するが dlopen(3) が開けなかったケース 失敗の詳細は例外の詳細メッセージで確認できます。

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

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

別のクラスの UnsatisfiedLinkError エラーとして、以下のようなメッセージが表示されることがあります。

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 の出力でライブラリのロードに関するメッセージを確認します。
  • 名前またはシグネチャの不一致により、メソッドが見つからない。この場合の主な原因は以下のとおりです。
    • メソッド ルックアップの遅延で C++ 関数の宣言に失敗する extern "C" と適切 公開設定(JNIEXPORT)。Ice Cream 導入前は Sandwich の JNIEXPORT マクロは正しくなかったため、新しい GCC を 古い jni.h は機能しません。 arm-eabi-nm を使用できます。 ライブラリに表示されるシンボルを確認します。目で見ると 破損(例: _Z15Java_Foo_myfuncP7_JNIEnvP7_jclassJava_Foo_myfunc ではなく)のいずれか)を指定します。また、シンボルタイプが 小文字の「t」「T」ではなく「T」を 宣言を調整します。
    • 明示的な登録の場合、 メソッド シグネチャ。渡するものは、 登録呼び出しがログファイル内の署名と一致することを確認します。 「B」はbyte かつ「Z」boolean です。 シグネチャ内のクラス名コンポーネントは、「L」で始まり「;」で終わります。パッケージ名とクラス名の区切りには「/」を使用し、内部クラス名の区切りには「$」を使用します(例: Ljava/util/Map$Entry;)。

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

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

(このアドバイスのほとんどは、方法を見つける失敗にも同様に当てはまります。 GetMethodIDGetStaticMethodID、またはフィールドを含む GetFieldID または GetStaticFieldID に置き換えます)。

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

コードの圧縮を有効にする場合は 保持するコードの構成を行います。構成 適切な keep ルールが重要です。そうしないと、コード圧縮ツールによってクラス、メソッド、 JNI からのみ使用されるフィールド

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

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

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

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

この問題に対しては、次のような対応策があります。

  • FindClass ルックアップを 1 回実行します。 JNI_OnLoadし、後で使用するためにクラス参照をキャッシュに保存する あります。実行の一環として行われた FindClass 呼び出し JNI_OnLoad は、サービスに関連付けられたクラスローダーを使用します。 System.loadLibrary を呼び出した関数(これは 特別なルールがあります)。 アプリコードがライブラリを読み込む場合は、FindClass 正しいクラスローダーを使用します
  • クラスのインスタンスを必要な関数に渡して、 クラス引数を受け取るネイティブ メソッドを宣言し、 次に Foo.class を渡します。
  • ClassLoader オブジェクトへの参照をどこかにキャッシュに保存する loadClass の呼び出しを直接発行できます。これには、 多少の手間がかかります。

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

大規模なアプリケーションにアクセスする必要がある状況に マネージド コードとネイティブ コードの両方の元データのバッファビットマップや音のサンプルの操作などではよくあることです。このような状況に対する基本的なアプローチは 2 つあります。

1 つ目のアプローチとしては、データを byte[] に格納します。これにより、マネージコードから高速にアクセスできるようになります。しかし、ネイティブ コードからは、データをコピーせずにアクセスできるという保証はありません。イン 一部の実装、GetByteArrayElementsGetPrimitiveArrayCritical は、サービス引数への実際のポインタを返します。 マネージド ヒープ内にバッファが割り当てられるのに、 データをコピーします

もう 1 つのアプローチでは、データを直接バイトバッファに格納します。これらの java.nio.ByteBuffer.allocateDirect を使用して作成できます。または、 JNI の NewDirectByteBuffer 関数。レギュラーと異なる バイトバッファを使用する場合、ストレージはマネージド ヒープに割り当てられず、 常にネイティブ コードから直接アクセス( (GetDirectBufferAddress を含む)。どの程度直接的であるかによって マネージド コードからデータにアクセスする 非常に遅くなる可能性があります。

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

  1. データアクセスが多いのは、Java で記述されたコードなのか、C / C++ で記述されたコードなのか。
  2. データが最終的にシステム API に渡される場合、どのような形式になるか 含まれている必要があります。(たとえば、最終的にテーブルに byte[] を受け取って直接処理を行う関数で、 ByteBuffer は賢いとは限りません)。

どちらのアプローチが優れているか明確でない場合は、直接バイトバッファを使用してください。サポート は JNI に直接組み込まれているため、今後のリリースでパフォーマンスが向上する見込みです。