https://blog.k-res.net/archives/1525.html
翻訳順序:
この記事の内容は、プロジェクトのバグを見つけて解決策を探していたときに実際に見つかりました。当時、プロジェクトの当初のターゲットは8(ICS 4.0より前の2.Xバージョン)で、すべてが正常に実行されていました。 S3 4.0以降で、ターゲットを14にアップグレードしてからS3で実行すると、次のようなネイティブクラッシュが表示されます。
05-13 14:07:13.139: E/dalvikvm(22265): JNI ERROR (app bug): attempt to use stale local reference 0x20d00001
05-13 14:07:13.139: E/dalvikvm(22265): VM aborting
05-13 14:07:13.139: A/libc(22265): Fatal signal 11 (SIGSEGV) at 0xdeadd00d (code=1), thread 22457 (Thread-1276)
05-13 14:07:13.239: I/DEBUG(1894): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
05-13 14:07:13.249: I/DEBUG(1894): Build fingerprint: ‘samsung/m0zc/m0chn:4.1.2/JZO54K/I9300ZCEMB1:user/release-keys’
05-13 14:07:13.249: I/DEBUG(1894): pid: 22265, tid: 22457, name: Thread-1276 >>> cn.android.app <<<
05-13 14:07:13.249: I/DEBUG(1894): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr deadd00d
05-13 14:07:13.489: I/DEBUG(1894): r0 00000000 r1 00000000 r2 deadd00d r3 00000000
05-13 14:07:13.489: I/DEBUG(1894): r4 408cb1a8 r5 0000020c r6 20d00001 r7 fffff86c
05-13 14:07:13.489: I/DEBUG(1894): r8 5ee308dc r9 00004e58 sl fffff870 fp 5ee307b8
05-13 14:07:13.489: I/DEBUG(1894): ip 00004000 sp 5ee30540 lr 400f7c95 pc 40866e50 cpsr 60000030
05-13 14:07:13.489: I/DEBUG(1894): d0 3ff000003f800000 d1 0000000000000000
05-13 14:07:13.489: I/DEBUG(1894): d2 0000000000000000 d3 0000000000000000
05-13 14:07:13.489: I/DEBUG(1894): d4 0000000000000000 d5 0000000000000000
05-13 14:07:13.489: I/DEBUG(1894): d6 0000000000000000 d7 0000000000000000
05-13 14:07:13.489: I/DEBUG(1894): d8 0000000000000000 d9 0000000000000000
05-13 14:07:13.489: I/DEBUG(1894): d10 0000000000000000 d11 0000000000000000
05-13 14:07:13.489: I/DEBUG(1894): d12 0000000000000000 d13 0000000000000000
05-13 14:07:13.489: I/DEBUG(1894): d14 0000000000000000 d15 0000000000000000
05-13 14:07:13.489: I/DEBUG(1894): d16 0000000000000000 d17 0000000000000000
05-13 14:07:13.489: I/DEBUG(1894): d18 0000000000000000 d19 0000000000000000
05-13 14:07:13.489: I/DEBUG(1894): d20 0000000000000000 d21 0000000000000000
05-13 14:07:13.489: I/DEBUG(1894): d22 0000000000000000 d23 0000000000000000
05-13 14:07:13.489: I/DEBUG(1894): d24 0000000000000000 d25 0000000000000000
05-13 14:07:13.489: I/DEBUG(1894): d26 0000000000000000 d27 0000000000000000
05-13 14:07:13.489: I/DEBUG(1894): d28 0000000000000000 d29 0000000000000000
05-13 14:07:13.489: I/DEBUG(1894): d30 0000000000000000 d31 0000000000000000
05-13 14:07:13.489: I/DEBUG(1894): scr 60000010
05-13 14:07:13.499: I/DEBUG(1894): backtrace:
05-13 14:07:13.499: I/DEBUG(1894): #00 pc 00045e50 /system/lib/libdvm.so (dvmAbort+75)
05-13 14:07:13.499: I/DEBUG(1894): #01 pc 00028c3c /system/lib/libdvm.so (IndirectRefTable::get(void*) const+336)
05-13 14:07:13.499: I/DEBUG(1894): #02 pc 00049eeb /system/lib/libdvm.so (dvmDecodeIndirectRef(Thread*, _jobject*)+30)
05-13 14:07:13.499: I/DEBUG(1894): #03 pc 0004ca77 /system/lib/libdvm.so
05-13 14:07:13.499: I/DEBUG(1894): #04 pc 00653480 /data/data/cn.android.app/lib/libgameapp.so (CKSoundManager::LoadBGM(char const*)+56)
…
05-13 14:07:13.509: I/DEBUG(1894): memory map around fault addr deadd00d:
05-13 14:07:13.509: I/DEBUG(1894): be9ae000-be9cf000 [stack]
05-13 14:07:13.509: I/DEBUG(1894): (no map for address)
05-13 14:07:13.509: I/DEBUG(1894): ffff0000-ffff1000 [vectors]
05-13 14:07:13.674: I/DEBUG(1894): !@dumpstate -k -t -z -d -o /data/log/dumpstate_app_native -m 22265
上記のクラッシュコンテンツのより重要なヒントは、コールスタックで古いローカル参照とdvmDecodeIndirectRefを使用しようとすることです。これは、実際には、JNIが呼び出されたときのオブジェクト参照のJava部分のエラーを参照します。重要なコンテンツによると、 android dalvikチームの開発者によって書かれたようです。関連記事によると、説明に従って非厳密なJNIコードがスムーズに修正され、問題が解決されました。理解を深めるために全文を翻訳する必要があると感じています(レベルが限られているため、不適切な翻訳を指摘してください!):
テキスト:
[この記事の著者は、DalvikチームのソフトウェアエンジニアであるElliottHughesです。-ティムブレイ]
JNIを使用するネイティブコードを記述しない場合、この記事はほとんど役に立ちません。あなたが書くなら、あなたは本当にこの記事を読むべきです。
変化したこと?どうして?
すべての開発者は、優れたガベージコレクター(GCと呼ばれるガベージコレクター)を望んでいます。優れたGCは、いつでもオブジェクトを移動します。これにより、より効率的なメモリ割り当てとバルクメモリの再利用の提供が容易になり、ヒープメモリの断片化が回避され、場合によっては局所性が向上します。これらのオブジェクトへのポインタをネイティブコードに渡すと、いつでもオブジェクトを移動することが問題になります。JNIは、jobjectのような型を使用してこの問題を解決します。ポインターを直接送信する代わりに、必要に応じて実際のポインターに変換できる透過ハンドル(開発者に対して概念的に透過的な不透明ハンドル)を提供します。ハンドルを使用することにより、GCがオブジェクトを移動するときに、オブジェクトの新しい位置を指すようにハンドル対応テーブルを更新するだけで済みます。これは、GCを実行するたびに、ネイティブコードに使用できないポインタをたくさん残す必要がないことを意味します。
以前のバージョンのAndroidでは、間接ハンドルを使用していませんでした。直接ポインターを使用していました。オブジェクトを移動するGCを実装しなかったため、これは大きな問題ではないようですが、開発者は正常に機能しているように見えても実際にはバグのあるコードを記述します。ICSでは、オブジェクトを移動するGCをまだ実装していない場合でも、間接参照に切り替えたため、ネイティブコードのバグのチェックを開始します。
ICSはJNIバグ互換モードを提供します。AndroidManifest.xmlのtargetSdkVersionバージョン番号がICS(14-)よりも小さい限り、コードを「免除」できます。ただし、targetSdkVersionを更新したら、コードは正しくなければなりません。
CheckJNIは、これらのエラーを検出して報告するように更新されました。ICSでは、マニフェストでdebug =” true”の場合、CheckJNIはデフォルトですでに有効になっています。
JNIが引用したいくつかの基本的な知識
JNIには、いくつかの異なる参照があります。最も重要な2つは、ローカル参照とグローバル参照です。特定のジョブジェクトは、ローカルまたはグローバルにすることができます。(弱いグローバル弱いグローバルもありますが、これには別のタイプ、jweakがあり、ここでは関係しません。)
グローバル/ローカルの区別は、ライフサイクルとスコープの両方に影響します。グローバルなものは、このスレッドのJNIEnv *を介して任意のスレッドで使用でき、DeleteGlobalRef()が明示的に呼び出されるまで有効です。ローカルは、最初に送信されたスレッドでのみ使用でき、DeleteLocalRef()が明示的に呼び出されるまで、またはより一般的には、ネイティブ関数から戻るまで有効になります。ネイティブ関数が戻ると、すべてのローカル参照が自動的に削除されます。
以前のシステムでは、ローカル参照は直接ポインターであり、ローカル参照が実際に使用できなくなることはありませんでした。つまり、ローカル参照に対してDeleteLocalRef()を明示的に呼び出したり、PopLocalFrame()を使用して明示的に削除したりした場合でも、ローカル参照を無期限に使用できます。
JNIEnvは1つのスレッドでのみ使用できますが、Androidは各スレッドの状態をJNIEnvに保存したことがないため、以前に間違ったスレッドでJNIEnvを使用しても問題はありません。各スレッドにローカル参照テーブルがあるので、正しいスレッドでJNIEnvを使用することが重要です。
上記はICSが検出するバグです。これらの問題が見つかった場合、およびそれらを修正する方法を説明するために、いくつかの一般的な例を見ていきます。これらの問題を修正する必要があります。これは非常に重要です。Androidの将来のバージョンでは、オブジェクトを移動できるリサイクル業者が追加される可能性が非常に高いためです。バグ互換モードを常に提供できるとは限りません。
一般的なJNIリファレンスのバグ
バグ:ネイティブコードインターフェイスクラスでjobjectを長期間保存するときに、NewGlobalRef()を呼び出すのを忘れる
ネイティブピア(ネイティブピア)(Javaオブジェクトに対応する長寿命のネイティブオブジェクト。通常、Javaオブジェクトの作成時に作成され、Javaオブジェクトのファイナライザーの実行時に破棄されます)を使用する場合は、ジョブジェクトをに格納しないでください。ネイティブオブジェクトは長期間使用できます。次回使用するときに使用できなくなるためです。(JNIEnv *にも同様の状況があります。同じスレッドでネイティブ呼び出しが発生した場合でも、引き続き使用できる場合があります。そうでない場合は使用できません。)
class MyPeer {
public:
MyPeer(jstring s) {
str_ = s; // 错误: 没有确定是全局就长期保存引用
}
jstring str_;
};
static jlong MyClass_newPeer(JNIEnv* env, jclass) {
jstring local_ref = env->NewStringUTF("hello, world!");
MyPeer* peer = new MyPeer(local_ref);
return static_cast<jlong>(reinterpret_cast<uintptr_t>(peer));
// 错误: local_ref 在我们返回时将变得不再可用, 但我们已经将其保存在'peer'中了.
}
static void MyClass_printString(JNIEnv* env, jclass, jlong peerAddress) {
MyPeer* peer = reinterpret_cast<MyPeer*>(static_cast<uintptr_t>(peerAddress));
// 错误: peer->str_ 不可用!
ScopedUtfChars s(env, peer->str_);
std::cout << s.c_str() << std::endl;
}
この問題の解決策は、JNIグローバル参照のみを保存することです。JNIグローバル参照が自動的にリリースされることはないため、自分でリリースする必要があることが非常に重要です。デストラクタにJNIEnv *がないため、この問題は少し恥ずかしいものになります。最も簡単な解決策は、通常、ネイティブインターフェイスクラスに明確な破棄関数を追加し、Javaインターフェイスクラスのファイナライザで呼び出すことです。
class MyPeer {
public:
MyPeer(JNIEnv* env, jstring s) {
this->s = env->NewGlobalRef(s);
}
~MyPeer() {
assert(s == NULL);
}
void destroy(JNIEnv* env) {
env->DeleteGlobalRef(s);
s = NULL;
}
jstring s;
};
NewGlobalRef()/ DeleteGlobalRef()のペアの呼び出しを常に保持する必要があります。CheckJNIはグローバル参照のリークをキャッチしますが、上限は非常に高い(デフォルトは2000)ので注意してください。
コードにこのようなエラーがある場合は、次のようなクラッシュメッセージが表示されます。
JNI ERROR (app bug): accessed stale local reference 0x5900021 (index 8 in a table of size 8)
JNI WARNING: jstring is an invalid local reference (0x5900021)
in LMyClass;.printString:(J)V (GetStringUTFChars)
"main" prio=5 tid=1 RUNNABLE
| group="main" sCount=0 dsCount=0 obj=0xf5e96410 self=0x8215888
| sysTid=11044 nice=0 sched=0/0 cgrp=[n/a] handle=-152574256
| schedstat=( 156038824 600810 47 ) utm=14 stm=2 core=0
at MyClass.printString(Native Method)
at MyClass.main(MyClass.java:13)
別のスレッドからJNIEnv *を使用すると、次のようなクラッシュメッセージが表示されます。
JNI WARNING: threadid=8 using env from threadid=1
in LMyClass;.printString:(J)V (GetStringUTFChars)
"Thread-10" prio=5 tid=8 NATIVE
| group="main" sCount=0 dsCount=0 obj=0xf5f77d60 self=0x9f8f248
| sysTid=22299 nice=0 sched=0/0 cgrp=[n/a] handle=-256476304
| schedstat=( 153358572 709218 48 ) utm=12 stm=4 core=8
at MyClass.printString(Native Method)
at MyClass$1.run(MyClass.java:15)
バグ:FindClass()がグローバル参照を返すという誤った考え
FindClass()はローカル参照を返します。多くの人がそれは全体像だと思っています。クラスのアンロードがないAndroidのようなシステムでは、jfieldIDとjmethodIDをグローバル処理として扱うことができます。(これらは実際には参照ではありませんが、クラスのアンロードをサポートするシステムにも同様のライフサイクルの問題が存在します。)ただし、jclassは参照であり、FindClass()はローカル参照を返します。よくある間違いは「静的jclass」です。ローカル参照をグローバル参照に手動で変換しない限り、コードに問題が発生します。正しいコードを書く方法は次のとおりです。
static jclass gMyClass;
static jclass gSomeClass;
static void MyClass_nativeInit(JNIEnv* env, jclass myClass) {
// ‘myClass’ (和其他非主要参数) 仅仅是局部引用.
gMyClass = env->NewGlobalRef(myClass);
// FindClass仅返回局部引用.
jclass someClass = env->FindClass("SomeClass");
if (someClass == NULL) {
return; // FindClass 已经抛出了 NoClassDefFoundError 的异常.
}
gSomeClass = env->NewGlobalRef(someClass);
}
コードにこのようなエラーがある場合は、次のようなクラッシュメッセージが表示されます。
JNI ERROR (app bug): attempt to use stale local reference 0x4200001d (should be 0x4210001d)
JNI WARNING: 0x4200001d is not a valid JNI reference
in LMyClass;.useStashedClass:()V (IsSameObject)
バグ:DeleteLocalRef()を呼び出した後、削除された参照を引き続き使用します
言うまでもなく、DeleteLocalRef()を呼び出して参照を削除して使用すると、不正アクセスが発生することも知っておく必要がありますが、これは以前は正常に機能していたため、この間違いを犯した可能性がありますが、まだ気づいていません。一般的なパターンは次のとおりです。ネイティブコード部分には長時間実行されるループがあります。開発者は、ローカル参照の上限に達しないようにすべてのローカル参照をクリーンアップしようとしますが、必要な参照を誤って削除してしまう可能性があります。戻り値。ドロップ!
解決策は簡単です。まだ必要な参照(戻り値を含む)でDeleteLocalRef()を呼び出さないでください。
バグ:PopLocalFrame()を呼び出した後も、ポップされた参照を引き続き使用します
これは実際には上記のバグの微妙な変形です。PushLocalFrame()およびPopLocalFrame()呼び出しは、ローカル参照をバッチで削除できます。PopLocalFrame()を呼び出すときは、パラメーター(通常は戻り値として使用される)またはNULLとして保持するフレームへの参照を渡します。過去には、次のようなエラーコードに問題はないことがわかります。
static jobjectArray MyClass_returnArray(JNIEnv* env, jclass) {
env->PushLocalFrame(256);
jobjectArray array = env->NewObjectArray(128, gMyClass, NULL);
for (int i = 0; i < 128; ++i) {
env->SetObjectArrayElement(array, i, newMyClass(i));
}
env->PopLocalFrame(NULL); // 错误: 应当传递 'array'.
return array; // 错误: 数组已经不可用.
}
解決策は通常、PopLocalFrame()への参照を渡すことです。上記の例では、個々の配列要素への参照を保存する必要はありません。GCが配列自体を認識している限り、要素(およびそれらが指すオブジェクト)自体を処理します。
コードにこのようなエラーがある場合は、次のようなクラッシュメッセージが表示されます。
JNI ERROR (app bug): accessed stale local reference 0x2d00025 (index 9 in a table of size 8)
JNI WARNING: invalid reference returned from native code
in LMyClass;.returnArray:()[Ljava/lang/Object;
総括する
はい、JNIでコーディングする際には、細部に注意を払うようお願いします。これは余分な作業です。しかし、私たちがより良いメモリ管理コードを作成するにつれて、あなたも先に進むことができると私たちは考えています。
オリジナル(壁があります!):
ICSでのJNIローカルリファレンスの変更
http://android-developers.blogspot.com/2011/11/jni-local-reference-changes-in-ics.html