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_t
、std::thread::native_handle()
std::thread
で pthread_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
のすべてのサブクラスに適用されます。
jclass
、jstring
、jarray
。
(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
値をキーとして使用する。
プログラマーは、ローカル参照を「過度に割り当てない」ようにする必要があります。これは実質的には
多数のローカル参照を作成する場合、
必要に応じて手動で解放する必要があります。
DeleteLocalRef
を使用することをおすすめします。「
スロットを予約する必要があるのは、
16 個のローカル参照があるため、それ以上のローカル参照が必要な場合は、必要に応じて削除するか、
EnsureLocalCapacity
/PushLocalFrame
。さらに予約できます。
jfieldID
と jmethodID
は不透明です。
であり、オブジェクト参照ではなく、
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 関数を呼び出すことができません。
コードは、関数の戻り値、
ExceptionCheck
、ExceptionOccurred
など)を検索して、結果を返す場合、
例外をクリアして処理します
例外発生時に呼び出すことができる唯一の 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 呼び出しに対して正しくないモード(
0
、JNI_ABORT
、JNI_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)までは、弱いグローバル参照は
NewLocalRef
、NewGlobalRef
、DeleteWeakGlobalRef
。(この仕様では、 弱いグローバル変数へのハード参照を作成してから、 制限されることはありません)。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 の既知のバグが含まれています。これらの最適化を JNIRegisterNatives
への明示的な登録は、 Android 8 ~ 11 でクラッシュが発生します。FindClass
がClassNotFoundException
をスローする下位互換性を確保するため、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_jclass
(Java_Foo_myfunc
ではなく)で、シンボルタイプが 小文字の「t」「T」ではなく「T」を 宣言を調整します。 - 明示的な登録の場合、
メソッド シグネチャ。渡するものは、
登録呼び出しがログファイル内の署名と一致することを確認します。
「B」は
byte
かつ「Z」boolean
です。 シグネチャ内のクラス名コンポーネントは「L」で始まり、「;」で終わる 「/」を使用パッケージ名/クラス名を区切るには、「$」を使用します。分離する 内部クラス名(Ljava/util/Map$Entry;
など)。
- メソッド ルックアップの遅延で C++ 関数の宣言に失敗する
javah
を使用して JNI ヘッダーを自動生成すると、解決できる場合があります。
問題を回避できます
よくある質問: FindClass
でクラスを見つけられませんでした。なぜですか?
(このアドバイスのほとんどは、方法を見つける失敗にも同様に当てはまります。
GetMethodID
、GetStaticMethodID
、またはフィールドを含む
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[]
に格納します。これにより
アクセスを制御できます。しかしネイティブ側では
コピーせずにデータにアクセスできるとは限りません。イン
一部の実装、GetByteArrayElements
、
GetPrimitiveArrayCritical
は、サービス引数への実際のポインタを返します。
マネージド ヒープにバッファリング バッファが割り当てられますが、
データをコピーします
もう 1 つのアプローチでは、データを直接バイトバッファに格納します。これらの
java.nio.ByteBuffer.allocateDirect
を使用して作成できます。または、
JNI の NewDirectByteBuffer
関数。レギュラーと異なる
バイトバッファを使用する場合、ストレージはマネージド ヒープに割り当てられず、
常にネイティブ コードから直接アクセス(
(GetDirectBufferAddress
を含む)。どの程度直接的であるかによって
マネージド コードからデータにアクセスする
非常に遅くなる可能性があります。
どちらのアプローチを選択するのかは、以下の 2 つの要因によって決まります。
- データアクセスのほとんどは Java で記述されたコードから行われますか。 または C/C++ で実行できるでしょうか。
- データが最終的にシステム API に渡される場合、どのような形式になるか
含まれている必要があります。(たとえば、最終的にテーブルに
byte[] を受け取って直接処理を行う関数で、
ByteBuffer
は賢いとは限りません)。
どちらのアプローチが優れているか明確でない場合は、直接バイトバッファを使用してください。サポート は JNI に直接組み込まれているため、今後のリリースでパフォーマンスが向上する見込みです。