[譯]Android冰淇淋三明治ICS(4.0+)JNI局部引用的變化

原文地址:http://blog.k-res.net/archives/1525.html

譯序:

這篇文章的內容實際是在我發現一個項目bug後尋找解決方案時找到的,當時項目原有target爲8(ICS 4.0之前的2.X版本),在4.0+的S3上運行一切正常,而後target升級到14時再在S3上運行時就會出現類似如下的native crash:

05-13 14:07:13.139: E/dalvikvm(22265): JNI ERROR (app bug): attempt to use stale localreference 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


上面crash內容中比較關鍵的提示是attempt to use stale local reference和調用棧上的dvmDecodeIndirectRef,實際指的是JNI調用時對Java部分對象引用的錯誤,按照關鍵內容找到了貌似是android dalvik team開發人員寫的一篇相關的文章,按照解釋順利改正了不嚴謹的JNI使用代碼,問題解決!感覺有必要翻譯一下全文,加深一下理解(由於本人水平有限,翻譯不當之處歡迎指出!):

正文:

[本文作者是Elliott Hughes,Dalvik小組的軟件工程師。- Tim Bray]

如果你不寫用到JNI的原生代碼的話,那麼這篇文章對你沒什麼用。如果你寫的話,那麼你真應該好好讀讀本文。

什麼東西變了?爲嘛呢?

每個開發者都想要一個好用垃圾回收器(garbage collector,簡稱GC)。做的好的GC是會隨時移動對象(objects)的。這樣就能便於提供更高效的內存分配和批量內存回收,避免堆內存碎片,並可能提高定位性能(locality)。如果你把指向這些對象的指針遞交給原生代碼的話,隨時移動對象就是個問題了。JNI使用像jobject這樣的類型來解決這個問題:不是直接遞交指針,而是給你一個能夠在必要時兌換爲實際指針的透明句柄(opaque handle,概念上對開發人員透明)。通過使用句柄,當GC移動對象時,只需要更新句柄對應表使其指向對象的新位置就可以了。這就意味着原生代碼不用在每次GC運行時被留下一堆不可用的指針了。

在之前的Android版本中,我們並沒有使用間接句柄;我們用的是直接指針。由於我們並沒有實現會移動對象的GC所以這看起來沒嘛大問題,可是這卻會導致開發人員寫出看似工作正常而實際是有bug的代碼。在ICS中,即使我們依然沒有實現一個會移動對象的GC,可我們已經轉爲使用間接引用了所以你們也會開始檢查出你們原生代碼中的bug了。

ICS提供了一種JNI bug兼容模式:只要AndroidManifest.xml中的targetSdkVersion版本號是低於ICS的(14-),你的代碼就能得到“豁免”。可是一旦你更新了targetSdkVersion的話,你的代碼就必須是正確的!

CheckJNI已經被更新爲會檢測並報告這些錯誤,並且在ICS中,如果manifest中的debuggable=”true”的話CheckJNI默認就已經開啓了。

JNI引用的一些基礎知識

在JNI中,有一些不同的引用。其中最重要的兩種就是局部引用(local references)和全局引用(global references)。任意一個給定的jobject都可以是局部或是全局的。(另外還有弱全局weak globals,但這種有一個單獨的類型,jweak,在此並不涉及。)

全局/局部的區別同時影響生命週期和作用域。全局的可以在任意線程通過本線程的JNIEnv*使用,並且可以有效到明確調用DeleteGlobalRef()之時。局部的只能在其最初被遞交到的線程中使用,並且可以有效到明確調用DeleteLocalRef()之時,或者,更普遍的,到你從你的原生函數中返回爲止。當原生函數返回時,所有的局部引用都會被自動刪除掉。

在之前的系統中,局部引用是直接的指針,局部引用永遠不會真正變爲不可用的。那就意味着你可以無限使用一個局部引用,即使你已經明確對它調用過DeleteLocalRef()了,或者使用PopLocalFrame()明確刪除了它。

雖然任意JNIEnv*只能在一個線程中可用,但由於Android在JNIEnv*中從來沒有保存過每個線程的狀態,所以之前在錯誤的線程中使用JNIEnv*也不會出問題。現在每個線程都有一個局部引用表,在正確的線程中使用JNIEnv*就是至關重要的了。

以上講的就是ICS會進行檢測的bug。我會過一些常見的實例來具體說明這些問題,如果發現他們,並且如何進行修復。你確實需要修復這些問題,這是很重要的,因爲很有可能未來版本的Android就會加入能移動對象的回收器。我們也不可能一直提供bug兼容模式。

常見JNI引用bug

Bug:在原生代碼接口類中長期保存jobject時忘記調用NewGlobalRef()

如果你用了原生接口類(native peer)(一個長期存在的對應Java對象的原生對象,通常在Java對象創建時創建,在Java對象的finalizer運行時銷燬),一定不能在原生對象中長期保存jobject,因爲下次你再使用它的時候它就已經不再可用了。(JNIEnv*也有類似的情況。在同一線程內發生的原生調用時它可能還是可用的,否則就不可用了。)

1  class MyPeer {
2  public:
3    MyPeer(jstring s) {
4      str_ = s; // 錯誤: 沒有確定是全局就長期保存引用
5    }
6    jstring str_;
7  };
8  
9  static jlong MyClass_newPeer(JNIEnv* env, jclass) {
10    jstring local_ref = env-&gt;NewStringUTF("hello, world!");
11    MyPeer* peer = new MyPeer(local_ref);
12    return static_cast&lt;jlong&gt;(reinterpret_cast&lt;uintptr_t&gt;(peer));
13    // 錯誤: local_ref 在我們返回時將變得不再可用, 但我們已經將其保存在'peer'中了.
14  }
15  
16  static void MyClass_printString(JNIEnv* env, jclass, jlong peerAddress) {
17    MyPeer* peer = reinterpret_cast&lt;MyPeer*&gt;(static_cast&lt;uintptr_t&gt;(peerAddress));
18    // 錯誤: peer-&gt;str_ is 不可用!
19    ScopedUtfChars s(env, peer-&gt;str_);
20    std::cout &lt;&lt; s.c_str() &lt;&lt; std::endl;
21  }

這個問題的解決方法是隻保存JNI全局引用。由於JNI全局引用永遠不會被自動釋放,所以很重要的一點就是你得自己自己釋放他們。這個問題會由於你的析構函數裏沒有JNIEnv*而變得稍微有點囧。最簡單的解決方法通常就是在你的原生接口類中加入一個明確的銷燬函數,並在Java接口類的析構(finalizer)中調用。

1  class MyPeer {
2  public:
3    MyPeer(JNIEnv* env, jstring s) {
4      this-&gt;s = env-&gt;NewGlobalRef(s);
5    }
6    ~MyPeer() {
7      assert(s == NULL);
8    }
9    void destroy(JNIEnv* env) {
10      env-&gt;DeleteGlobalRef(s);
11      s = NULL;
12    }
13    jstring s;
14  };

你應該總是保持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)

Bug:錯誤的認爲FindClass()返回全局引用

FindClass()返回的是局部引用。許多人認爲是全局的。在像Android這樣不具備類卸載(class unloading)的系統中,你可以把jfieldID和jmethodID當作全局處理。(他們實際上不是引用,但在支持類卸載的系統中也存在類似的生存週期問題。)但是jclass是引用,而且FindClass()返回的是局部引用。一種常見的錯誤就是“靜態jclass”。除非你手動將局部引用轉換爲全局引用,否則你的代碼就會有問題。下面是正確代碼的寫法:

1  static jclass gMyClass;
2  static jclass gSomeClass;
3  
4  static void MyClass_nativeInit(JNIEnv* env, jclass myClass) {
5    // ‘myClass’ (和其他非主要參數) 僅僅是局部引用.
6    gMyClass = env-&gt;NewGlobalRef(myClass);
7  
8    // FindClass僅返回局部引用.
9    jclass someClass = env-&gt;FindClass("SomeClass");
10    if (someClass == NULL) {
11      return// FindClass 已經拋出了 NoClassDefFoundError 的異常.
12    }
13    gSomeClass = env-&gt;NewGlobalRef(someClass);
14  }

如果你的代碼確實有這類錯誤的話,會收到類似這樣的崩潰信息:

    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)

Bug:調用DeleteLocalRef()後繼續使用已被刪除的引用

我想這事不用說也應該知道,調用DeleteLocalRef()刪除引用後再使用會出現非法訪問,但是因爲這以前是可以正常工作的,所以你也許已經犯了這個錯誤但還沒意識到。常見的模式像是這樣:原生代碼部分有一個長期運行的循環,開發人員爲了要避免達到局部引用上限而嘗試清理每一個局部引用,但可能會意外地將想要作爲返回值的引用也給刪除掉!

解決方法很簡單:別對你還要用到的(包括作爲返回值的)引用調用DeleteLocalRef()。

Bug:調用PopLocalFrame()後繼續使用已被彈出的引用

這其實是上面那個bug的微妙變種。PushLocalFrame()和PopLocalFrame()調用能批量刪除局部引用。當調用PopLocalFrame()時,你將frame上的一個想要保留的引用傳入爲參數(通常是要用作返回值),或者NULL。過去,你會發現像這樣的錯誤代碼不會有任何問題:

1  static jobjectArray MyClass_returnArray(JNIEnv* env, jclass) {
2    env-&gt;PushLocalFrame(256);
3    jobjectArray array = env-&gt;NewObjectArray(128, gMyClass, NULL);
4    for (int i = 0; i &lt; 128; ++i) {
5        env-&gt;SetObjectArrayElement(array, i, newMyClass(i));
6    }
7    env-&gt;PopLocalFrame(NULL); // 錯誤: 應當傳遞 'array'.
8    return array; // 錯誤: 數組已經不可用.
9  }

解決方法通常是將引用傳遞給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編碼時要更注意一些細節,這是額外的工作。但是我們認爲隨着我們做出更好更佳的內存管理代碼你們也能走在更前面。

原文(有牆!):

JNI Local Reference Changes in ICS -http://android-developers.blogspot.com/2011/11/jni-local-reference-changes-in-ics.html


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章