JNI に関するヒント

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

注: Android は Kotlin を ART 対応のバイトコードにコンパイルするため、 Java プログラミング言語と同様に利用できるため、このページのガイダンスは、 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 によるアクセスまたはアクセスを必要とするスレッドの数を最小限に抑える。 Java 言語と C++ 言語の両方でスレッドプールを使用する必要がある場合は、JNI を維持するようにしてください。 個々のワーカー スレッド間ではなく、プール オーナー間における通信を確立する必要があります。
  • インターフェースのコードを、簡単に特定できる少数の C++ および Java ソースに保存する 将来のリファクタリングを容易にするロケーションを確保できます。JNI 自動生成の使用を検討する ライブラリを使用します。

JavaVM と JNIEnv

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

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

JNIEnv は、スレッド ローカル ストレージ用です。このため、スレッド間で JNIEnv を共有することはできません。 コードで JNIEnv を取得する方法が他にない場合は、 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() を使用して、次のことを必要とするスレッドを作成することをおすすめします。 呼び出すことができます。そうすることで、十分なスタック スペースを確保して、 正しい ThreadGroup にあり、同じ ClassLoader を使用していること 使用できます。また、デバッグ用のスレッド名を Java で設定する方が、 ネイティブ コード(pthread_t またはpthread_setname_np() thread_tstd::thread::native_handle() std::threadpthread_t が必要な場合)。

ネイティブに作成されたスレッドをアタッチすると java.lang.Thread が発生する メイン サブセクションに追加することをThreadGroup, デバッガから参照できるようになります。AttachCurrentThread() に発信中 タスクを実行することは何もしません。

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

JNI を介してアタッチされたスレッドは、 終了前に DetachCurrentThread() を確認してください。 これを直接コーディングするのが面倒な場合は、Android 2.0(Eclair)以降では、 pthread_key_create() を使用してデストラクタを定義できます。 関数を呼び出す必要があります。また、 そこから DetachCurrentThread() を呼び出します。( pthread_setspecific() を含むキーで JNIEnv を格納します。 Thread-local-storageそうするとデストラクタに 使用します)。

jclass、jmethodID、jfieldID

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

  • FindClass で、クラスのクラス オブジェクト参照を取得します。
  • GetFieldID で、フィールドのフィールド ID を取得します。
  • 次のような適切な値を使用して、フィールドの内容を取得します。 GetIntField

同様に、メソッドを呼び出すには、クラス オブジェクト参照を取得して、メソッド ID を取得します。多くの場合、ID は 内部ランタイム データ構造体へのポインタ。検索には複数の文字列が必要になる場合があります ただし、フィールドを取得したり、メソッドを呼び出したりするための実際の呼び出しは、 非常に高速です。

パフォーマンスが重要な場合は、値を一度確認して結果をキャッシュに保存すると便利です。 ネイティブコードで変更できます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 を拡張すると、参照の誤使用のほとんどについてランタイムが警告するようになります)。 チェックが有効になります)。

非ローカル参照を取得する唯一の方法は、 NewGlobalRefNewWeakGlobalRef

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

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

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

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

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

プログラマーは、ローカル参照を「過度に割り当てない」ようにする必要があります。これは実質的には 多数のローカル参照を作成する場合、 必要に応じて手動で解放する必要があります。 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 文字列関数での使用に適しています。この方法の短所は、 任意の UTF-8 データを JNI に渡して、正しく動作することを期待します。

String の UTF-16 表現を取得するには、GetStringChars を使用します。 なお、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 を中止します。

Android 8 より前は、Android のように UTF-16 文字列を使用した操作が通常高速でした GetStringChars にはコピーが不要でしたが、 GetStringUTFChars には割り当てと UTF-8 への変換が必要でした。 Android 8 では 1 文字あたり 8 ビットを使用するように String 表現が変更されました メモリ節約のために ASCII 文字列を使用し、 動く garbage Collector を使用します。これらの機能により、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 を呼び出す必要があります。 配列に変更を加えた後、変更後にそのフィールドへの その配列の内容を使用するコードを実行したときに、 スキップできますフラグを確認するもう 1 つの理由として、 JNI_ABORT を効率的に処理。たとえば 配列を取得してその位置を変更し、他の関数に断片を渡して、 変更を破棄しますJNI が新しいコピーを作成していることがわかっている場合は、 編集可能なコピーします。JNI が コピーを作成する必要があります

次の場合に Release 呼び出しをスキップできると想定するのはよくある誤りです(サンプルコードでもよくあります)。 *isCopy は false です。これは誤りです。コピーバッファが 1 つも 元のメモリは固定する必要があり、メモリの移動はできない ガベージ コレクタに対して行われます。

また、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 または GetStringUTFRegion でスペースから String

例外

例外の保留中は、ほとんどの JNI 関数を呼び出すことができません。 コードは、関数の戻り値、 ExceptionCheckExceptionOccurred など)を検索して、結果を返す場合、 例外をクリアして処理します

例外発生時に呼び出すことができる唯一の JNI 関数 :

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

多くの JNI 呼び出しで例外をスローできますが、多くの場合、簡単な方法を提供しています。 チェックの必要がなくなりますたとえば、NewString が 例外を確認する必要はありません。ただし、 メソッドを呼び出し(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 の仕様 をご覧ください)。つまり、メソッドのシグネチャが間違っている場合、 メソッドが初めて呼び出されたときです

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

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

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

@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 では、これらのアノテーションは無視されます(ABI の不一致)。 @CriticalNative の引数が誤った引数のマーシャリングにつながり、クラッシュする可能性があります。

パフォーマンスが重視されるメソッドでこのようなアノテーションが必要になる場合は、 メソッドを依存する代わりに、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 までは、これらの最適化アノテーションは無視されます。ABI @CriticalNative が一致しないと誤った引数が使用される クラッシュする可能性が高まります

    @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 出力で、 ライブラリの読み込みに関するメッセージ。
  • 名前またはシグネチャの不一致により、メソッドが見つからない。この 一般的に、次の原因が考えられます。 <ph type="x-smartling-placeholder">
      </ph>
    • メソッド ルックアップの遅延で 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 を使用すると、 クラスの内部名を指定します。

コードの圧縮を有効にする場合は 保持するコードの構成を行います。構成 適切な保存ルールを使用することが重要です。コード圧縮ツールを使用しない場合、クラス、メソッド、コードが削除される可能性があるためです。 JNI からのみ使用されるフィールド

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

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

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

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

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

  • 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 に直接組み込まれているため、今後のリリースでパフォーマンスが向上する見込みです。