JNI实现源码分析【三 间接引用表】
在JNI實現源碼分析【二 數據結構】的參數傳遞一節中,我們提到,JNI為了安全性的考慮使用了形如jobject的結構來傳遞參數。而jobject被表述為指針,但又不是直接指向Object的指針那么jobject是如何和真正的Object建立聯系呢?
在JNI的API中,有一組API Global and Local References,這里的References又是什么?為啥會有這一組API?
答案都和間接引用表(IndirectRefTable)有關
0x01: IndirectRefTable
源碼見IndirectRefTable.h
代碼很復雜,等效理解就可以了,其作用就是一張保存了間接引用的表。讓jobject和Object建立起聯系。
0x02: 作用域
在JNI中,有兩個不同的作用域:全局作用域(進程級別)和線程作用域(線程級別)。這兩個作用域分別有自己的間接引用表。
全局作用域的間接引用表保存在gDvm.jniGlobalRefTable中。gDvm是一個全局變量,在虛擬機啟動的時候就創建。
線程作用域的間接引用表保存在thread.jniLocalRefTable中。和線程綁定,線程創建時創建,線程銷毀時銷毀。
JNI API中的全局引用和局部引用,指的就是全局作用域的間接引用表和線程作用域的間接引用表。
于是:
jobject NewGlobalRef(JNIEnv *env, jobject obj); void DeleteGlobalRef(JNIEnv *env, jobject globalRef);我們就是操作了全局引用表
而:
jobject NewLocalRef(JNIEnv *env, jobject ref); void DeleteLocalRef(JNIEnv *env, jobject localRef);我們操作了線程引用表
讓我們再來看看兩個表的大小,在創建的時候,就指定了其大小:
#define kGlobalRefsTableInitialSize 512 #define kGlobalRefsTableMaxSize 51200 if (!gDvm.jniGlobalRefTable.init(kGlobalRefsTableInitialSize,kGlobalRefsTableMaxSize,kIndirectKindGlobal)) {return false;}可以看到,全局引用表的初始大小為512,最大為51200。
#define kJniLocalRefMin 64 #define kJniLocalRefMax 512 if (!thread->jniLocalRefTable.init(kJniLocalRefMin,kJniLocalRefMax, kIndirectKindLocal)) {return false;}而局部引用表的初始大小為64,最大為512。 這里順便提一下,當超過這個最大時,就會報local reference table overflow (max=512)的錯誤。
0x03: jobject到Object的映射
到現在,我們應該可以順理成章的理解到,jobject到Object的映射借用了間接引用表,沒錯!
我們來分析局部引用,全局引用是類似的。
非常明了的代碼,我們使用了pRefTable->add將實際對象添加到了間接引用表,從而獲取了jobject的間接引用。
我們看一下add做了啥:
IndirectRef IndirectRefTable::add(u4 cookie, Object* obj) {IRTSegmentState prevState;prevState.all = cookie;size_t topIndex = segmentState.parts.topIndex;assert(obj != NULL);assert(dvmIsHeapAddress(obj));assert(table_ != NULL);assert(alloc_entries_ <= max_entries_);assert(segmentState.parts.numHoles >= prevState.parts.numHoles);/** We know there's enough room in the table. Now we just need to find* the right spot. If there's a hole, find it and fill it; otherwise,* add to the end of the list.*/IndirectRef result;IndirectRefSlot* slot;int numHoles = segmentState.parts.numHoles - prevState.parts.numHoles;if (numHoles > 0) {assert(topIndex > 1);/* find the first hole; likely to be near the end of the list,* we know the item at the topIndex is not a hole */slot = &table_[topIndex - 1];assert(slot->obj != NULL);while ((--slot)->obj != NULL) {assert(slot >= table_ + prevState.parts.topIndex);}segmentState.parts.numHoles--;} else {/* add to the end, grow if needed */if (topIndex == alloc_entries_) {/* reached end of allocated space; did we hit buffer max? */if (topIndex == max_entries_) {ALOGE("JNI ERROR (app bug): %s reference table overflow (max=%d)",indirectRefKindToString(kind_), max_entries_);return NULL;}size_t newSize = alloc_entries_ * 2;if (newSize > max_entries_) {newSize = max_entries_;}assert(newSize > alloc_entries_);IndirectRefSlot* newTable =(IndirectRefSlot*) realloc(table_, newSize * sizeof(IndirectRefSlot));if (table_ == NULL) {ALOGE("JNI ERROR (app bug): unable to expand %s reference table ""(from %d to %d, max=%d)",indirectRefKindToString(kind_),alloc_entries_, newSize, max_entries_);return NULL;}memset(newTable + alloc_entries_, 0xd1,(newSize - alloc_entries_) * sizeof(IndirectRefSlot));alloc_entries_ = newSize;table_ = newTable;}slot = &table_[topIndex++];segmentState.parts.topIndex = topIndex;}slot->obj = obj;slot->serial = nextSerial(slot->serial);result = toIndirectRef(slot - table_, slot->serial, kind_);assert(result != NULL);return result; }我擦,真的是太復雜了,里面肯定包含了某個算法,反正就是通過參數cookie,通過slot等,在表的合適位置引用了真正的Object,然后返回了一個值(間接引用),后續通過這個值,能夠去表里面的這個位置找到Object。
所以之前說過,jobject并不是直接指向Object的指針。甚至它并不是真正的地址,它僅僅是表的間接引用。
讓我們繼續看看,如何通過這個間接引用找到真實的Object吧:
Object* dvmDecodeIndirectRef(Thread* self, jobject jobj) {if (jobj == NULL) {return NULL;}switch (indirectRefKind(jobj)) {case kIndirectKindLocal:{Object* result = self->jniLocalRefTable.get(jobj);if (UNLIKELY(result == NULL)) {ALOGE("JNI ERROR (app bug): use of deleted local reference (%p)", jobj);ReportJniError();}return result;}case kIndirectKindGlobal:{// TODO: find a way to avoid the mutex activity hereIndirectRefTable* pRefTable = &gDvm.jniGlobalRefTable;ScopedPthreadMutexLock lock(&gDvm.jniGlobalRefLock);Object* result = pRefTable->get(jobj);if (UNLIKELY(result == NULL)) {ALOGE("JNI ERROR (app bug): use of deleted global reference (%p)", jobj);ReportJniError();}return result;}case kIndirectKindWeakGlobal:{// TODO: find a way to avoid the mutex activity hereIndirectRefTable* pRefTable = &gDvm.jniWeakGlobalRefTable;ScopedPthreadMutexLock lock(&gDvm.jniWeakGlobalRefLock);Object* result = pRefTable->get(jobj);if (result == kClearedJniWeakGlobal) {result = NULL;} else if (UNLIKELY(result == NULL)) {ALOGE("JNI ERROR (app bug): use of deleted weak global reference (%p)", jobj);ReportJniError();}return result;}case kIndirectKindInvalid:default:if (UNLIKELY(gDvmJni.workAroundAppJniBugs)) {// Assume an invalid local reference is actually a direct pointer.return reinterpret_cast<Object*>(jobj);}ALOGW("Invalid indirect reference %p in decodeIndirectRef", jobj);ReportJniError();return kInvalidIndirectRefObject;} }可以看到,通過jobject可以計算出RefKind,即jobject還包含了類型信息。
真正找Object是在pRefTable->get:
和我們前面想的一樣,這里通過一堆的計算后,在Object* obj = table_[index].obj;處找到了真實的Object。
0x04: JNI在背后默默做的事
在JNI環境中,我們永遠接觸不了真實的Object對象,上面映射方法是虛擬機內部的,我們在JNI環境也是沒法調用的。所以,我們在JNI環境中,使用的都是間接引用,比如jobject,jmethodID等。確實,JNI的所有API都在使用這些間接引用。
那么,這里就有一個問題了,既然間接引用和間接引用表有關,那在使用JNI的API時,獲取到這些間接引用時,JNI將真實的對象保存在哪個表里面?
答案是線程引用表,幾乎每一個API都有JNIEnv,JNIEnv和線程綁定,可以很容易定位到線程引用表。放到線程應用表,隨著線程的銷毀,引用表也不會被銷毀,不會一直占用空間。
我當初在JNI中想要獲取Throwable.printStackTrace時,就因為調用相關的API,然后產生了很多的間接引用,將間接引用表撐爆,報了:local reference table overflow (max=512)。
除了JNI的默認行為,假如我們想要自己控制引用的生命周期,比如提前刪除,將引用放置到全局引用表等,我們可以使用Ref相關的API即可,記住,不用了一定要刪除,不要存在引用泄漏。
作者:difcareer
鏈接:http://www.jianshu.com/p/127adc130508
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。 與50位技術專家面對面20年技術見證,附贈技術全景圖
總結
以上是生活随笔為你收集整理的JNI实现源码分析【三 间接引用表】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JNI实现源码分析【二 数据结构】
- 下一篇: JNI实现源码分析【四 函数调用】