ART虛擬機 | JNI靜態註冊和動態註冊

本文分析基於Android 11(R)html

源碼路徑java

註冊的本質是創建(Java層)native方法和(Native/C++層)JNI函數之間的一對一關係。靜態註冊指的是映射規則預先設定,一個native方法名能夠轉換成一個惟一的JNI函數名。動態註冊的映射規則由程序員本身設定,經過結構體將native方法和JNI函數指針綁定起來。android

庫加載

若是須要在Java中使用so庫中的代碼,那麼首先要作的就是庫加載。庫加載通常放在static代碼塊中,保證類加載後第一時間執行。c++

package com.hangl.jni;
public class TestJNI {
    static {
        System.loadLibrary("native");
    }     
...
}
複製代碼

衆所周知,庫加載時會去調用庫中的JNI_Onload函數。在ART源碼中,System.loadLibrary最終會調用JavaVMExt::LoadNativeLibrary。首先去庫中尋找JNI_Onload符號,將其轉成函數指針進行調用。接着,JNI_Onload函數返回該庫所必需的JNI版本號,由虛擬機判斷是否支持。程序員

void* sym = library->FindSymbol("JNI_OnLoad", nullptr);

using JNI_OnLoadFn = int(*)(JavaVM*, void*);
JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
int version = (*jni_on_load)(this, nullptr);
...
if (version == JNI_ERR) {
  StringAppendF(error_msg, "JNI_ERR returned from JNI_OnLoad in \"%s\"", path.c_str());
} else if (JavaVMExt::IsBadJniVersion(version)) {
  StringAppendF(error_msg, "Bad JNI version returned from JNI_OnLoad in \"%s\": %d",
                path.c_str(), version);
...
} else {
  was_successful = true;
}
複製代碼

當前虛擬機只支持三個JNI版本,以下所示。通常庫返回較多的是JNI_VERSION_1_4。與1_4相比,1_6版本的JNI只多了一個GetObjectRefType函數[連接],若是咱們的庫用不到這個函數,只須要1_4便可。web

bool JavaVMExt::IsBadJniVersion(int version) {
  // We don't support JNI_VERSION_1_1. These are the only other valid versions.
  return version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 && version != JNI_VERSION_1_6;
}
複製代碼

JNI_Onload函數是咱們進行動態註冊的寶地,在其中調用RegisterNatives便可進行註冊。具體的方法和細節留到"動態註冊"章節闡述。安全

虛擬機中的native方法

一般而言,一個Java方法在虛擬機中能夠找到兩種執行方式,一種是解釋執行,另外一種是機器碼執行。解釋執行時,解釋器會去尋找字節碼的入口地址。而機器碼執行時,虛擬機會去尋找機器指令的入口地址。考慮到每一個Java方法在虛擬機中都由ArtMethod對象表示,字節碼的入口信息(間接)存在其data_字段中,而機器碼入口信息則存在entry_point_from_quick_compiled_code_字段中。以下所示。markdown

// Must be the last fields in the method.
struct PtrSizedFields {
  // Depending on the method type, the data is
  // - native method: pointer to the JNI function registered to this method
  // or a function to resolve the JNI function,
  // - resolution method: pointer to a function to resolve the method and
  // the JNI function for @CriticalNative.
  // - conflict method: ImtConflictTable,
  // - abstract/interface method: the single-implementation if any,
  // - proxy method: the original interface method or constructor,
  // - other methods: during AOT the code item offset, at runtime a pointer
  // to the code item.
  void* data_;

  // Method dispatch from quick compiled code invokes this pointer which may cause bridging into
  // the interpreter.
  void* entry_point_from_quick_compiled_code_;
} ptr_sized_fields_;
複製代碼

不過對於native方法而言,它在Java世界中只有定義沒有實現,所以不會有字節碼信息。雖然不須要存儲字節碼的入口地址,但native方法在調用過程當中卻會多出一步。 JNI跳板函數.pngoracle

  • 首先進入一個跳板函數,其中會處理Java參數到Native參數的轉換以及線程狀態切換等過程。
  • 在跳板函數內部調用Native世界中實現的JNI函數。

這樣一來,不用存儲字節碼入口信息的data_字段就能夠用來存儲JNI函數的入口地址了。而entry_point_from_quick_compiled_code_中存儲的就是跳板函數的入口地址。具體可參考ART視角 | 爲何調用Native方法能夠進入C++世界ide

靜態註冊

當咱們不在JNI_Onload中調用RegisterNatives,或者壓根不在so庫中編寫JNI_Onload函數時,native方法的映射只能由靜態註冊完成。

雖然這個過程的名字叫"靜態註冊",但實際註冊是在運行時按需動態完成的,只不過因爲映射關係是事先肯定的,因此才被叫作"靜態"。

那麼具體的映射規則是什麼呢?

JNI實現了兩套映射規則,一套是簡化版,一套是爲了解決方法重載的複雜版。最終轉換出來的函數名按照以下規則順次拼接。

  • 前綴Java_
  • 類名,將/轉成_
  • 下劃線鏈接符_
  • 方法名
  • 若是須要區分兩個重載的方法,則用雙下劃線__鏈接參數簽名。若是該方法沒有被重載,則省略這一步。

爲了區分重載方法,字符串的末尾須要拼接參數簽名,而簽名中是有可能有[;_字符的。爲了避免在函數名中出現這些特殊字符(或者爲了避免和以前的鏈接符_混淆),轉換時對這些字符作了特殊處理。

  • _轉換爲_1

  • ;轉換爲_2

  • [轉換爲_3

因爲Java類名和方法名中都不可能以數字開頭,因此這樣的轉換不會跟Java類名或方法名衝突。具體規則參考JNI文檔。如下是一個轉換示例。

package pkg;  

class Cls { 

     native double f(int i, String s); 

     ... 

} 
複製代碼

轉換爲:

JNIEXPORT jdouble JNICALL Java_pkg_Cls_f__ILjava_lang_String_2 ( JNIEnv *env, /* interface pointer */ jobject obj, /* "this" pointer */ jint i, /* argument #1 */ jstring s) /* argument #2 */ { 

     /* Obtain a C-copy of the Java string */ 

     const char *str = (*env)->GetStringUTFChars(env, s, 0); 

     /* process the string */ 

     ... 

     /* Now we are done with str */ 

     (*env)->ReleaseStringUTFChars(env, s, str); 

     return ... 

} 
複製代碼

不過要注意一點,靜態註冊的JNI函數必須由JNIEXPORTJNICALL進行修飾。

JNIEXPORT代表將函數名輸出到動態符號表中,這樣後續註冊時調用dlsym才能找的到。默認狀況下,全部的函數名都會輸出到動態符號表中。但爲了安全性,咱們能夠在編譯時傳入-fvisibility=hidden來關閉這種輸出(JNIEXPORT修飾的依然會輸出),防止別人知道so中定義了哪些函數。這一點對於商業軟件尤其重要。

JNICALL主要用於消除不一樣硬件平臺調用規則的差別,對於AArch64而言,JNICALL不執行任何動做。

規則介紹完畢,接下來就要深刻註冊的具體過程。

上文中提到,ArtMethod對象的data_字段存儲JNI函數的入口地址,而entry_point_from_quick_compiled_code_存儲跳板函數的入口地址。但是對靜態註冊而言,直到第一次方法調用時映射關係才創建。

一個方法被調用以前,首先要加載它所屬的類。那麼在類加載到方法第一次調用的這段時間裏,data_entry_point_from_quick_compiled_code_等於什麼呢?

類加載時會調用LinkCode函數,爲ArtMethod對象設置entry_point_from_quick_compiled_code_data_字段。

static void LinkCode(ClassLinker* class_linker, ArtMethod* method, const OatFile::OatClass* oat_class, uint32_t class_def_method_index) REQUIRES_SHARED(Locks::mutator_lock_) {
  ...
  const void* quick_code = nullptr;
  if (oat_class != nullptr) {
    // Every kind of method should at least get an invoke stub from the oat_method.
    // non-abstract methods also get their code pointers.
    const OatFile::OatMethod oat_method = oat_class->GetOatMethod(class_def_method_index);
    quick_code = oat_method.GetQuickCode();
  }
  ...
  if (quick_code == nullptr) {
    method->SetEntryPointFromQuickCompiledCode(  //set entry_point_from_quick_compiled_code_ 字段
        method->IsNative() ? GetQuickGenericJniStub() : GetQuickToInterpreterBridge());  
  }
  ...
  if (method->IsNative()) {
    // Set up the dlsym lookup stub. Do not go through `UnregisterNative()`
    // as the extra processing for @CriticalNative is not needed yet.
    method->SetEntryPointFromJni(  // set data_ 字段
        method->IsCriticalNative() ? GetJniDlsymLookupCriticalStub() : GetJniDlsymLookupStub());
  ...
  }
}
複製代碼

entry_point_from_quick_compiled_code_的值有兩種可能:

  1. 因爲跳板函數主要負責參數轉換,所以對於不一樣的native方法,只要它們的參數個數和類型一致,就可使用同一個跳板函數。這些跳板函數只在AOT編譯條件下才會生成,所以純解釋執行時quick_code == nullptr。
  2. quick_code == nullptr時,爲entry_point_from_quick_compiled_code_設置的值是art_quick_generic_jni_trampoline函數指針。它至關於一個通用的跳板函數,在執行過程當中動態進行參數轉換。

data_的值(不考慮CriticalNative)只有一種可能:

  1. 設置爲art_jni_dlsym_lookup_stub函數指針。該函數在執行時根據靜態轉換規則找到JNI函數,接着跳轉過去。所以真正的註冊發生在它裏面。

接下來看看JNI函數的尋找過程。art_jni_dlsym_lookup_stub是彙編代碼,其內部會調用artFindNaitveMethod找到JNI函數的指針,而後經過br x17指令跳轉到該JNI函數中。

ENTRY art_jni_dlsym_lookup_stub
    ...
    // Call artFindNativeMethod() for normal native and artFindNativeMethodRunnable()
    // for @FastNative or @CriticalNative.
    ...
    b.ne  .Llookup_stub_fast_or_critical_native
    bl    artFindNativeMethod
    b     .Llookup_stub_continue
    .Llookup_stub_fast_or_critical_native:
    bl    artFindNativeMethodRunnable
.Llookup_stub_continue:
    mov   x17, x0    // store result in scratch reg.
    ...
    cbz   x17, 1f   // is method code null ?
    br    x17       // if non-null, tail call to method's code.
1:
    ret             // restore regs and return to caller to handle exception.
END art_jni_dlsym_lookup_stub
複製代碼
extern "C" const void* artFindNativeMethodRunnable(Thread* self) REQUIRES_SHARED(Locks::mutator_lock_) {
   ...
   const void* native_code = class_linker->GetRegisteredNative(self, method);
   if (native_code != nullptr) {
     return native_code;
   }
   ...
   JavaVMExt* vm = down_cast<JNIEnvExt*>(self->GetJniEnv())->GetVm();
   native_code = vm->FindCodeForNativeMethod(method);
   ...
   return class_linker->RegisterNative(self, method, native_code);
 }
複製代碼

artFindNativeMethod內部調用的是artFindNativeMethodRunnable,它首先判斷ArtMethod的data_字段是否是已經註冊過了,若是是則直接返回data_存儲的函數指針。不然調用FindCodeForNativeMethod去尋找。最後將找到的函數指針寫入data_字段中。

// See section 11.3 "Linking Native Methods" of the JNI spec.
void* FindNativeMethod(Thread* self, ArtMethod* m, std::string& detail) REQUIRES(!Locks::jni_libraries_lock_) REQUIRES_SHARED(Locks::mutator_lock_) {
  std::string jni_short_name(m->JniShortName());
  std::string jni_long_name(m->JniLongName());
  ...
  {
    // Go to suspended since dlsym may block for a long time if other threads are using dlopen.
    ScopedThreadSuspension sts(self, kNative);
    void* native_code = FindNativeMethodInternal(self,
                                                 declaring_class_loader_allocator,
                                                 shorty,
                                                 jni_short_name,
                                                 jni_long_name);
    if (native_code != nullptr) {
      return native_code;
    }
  }
  ...
}
複製代碼

FindCodeForNativeMethod內部調用FindNativeMethod,建立兩個字符串,一個是jni_short_name,另外一個是jni_long_name。其實兩者反映的就是以前所說的兩種映射規則。

std::string GetJniShortName(const std::string& class_descriptor, const std::string& method) {
  // Remove the leading 'L' and trailing ';'...
  std::string class_name(class_descriptor);
  CHECK_EQ(class_name[0], 'L') << class_name;
  CHECK_EQ(class_name[class_name.size() - 1], ';') << class_name;
  class_name.erase(0, 1);
  class_name.erase(class_name.size() - 1, 1);

  std::string short_name;
  short_name += "Java_";
  short_name += MangleForJni(class_name);
  short_name += "_";
  short_name += MangleForJni(method);
  return short_name;
}
複製代碼
std::string ArtMethod::JniLongName() {
  std::string long_name;
  long_name += JniShortName();
  long_name += "__";

  std::string signature(GetSignature().ToString());
  signature.erase(0, 1);
  signature.erase(signature.begin() + signature.find(')'), signature.end());

  long_name += MangleForJni(signature);

  return long_name;
}
複製代碼

Short name是不考慮重載的映射規則,long name則增長了參數信息用於區分不一樣方法。尋找符號時,先找short name,再找long name。

動態註冊

動態註冊須要在JNI_Onload中主動調用RegisterNatives函數,並傳入class和JNINativeMethod結構體兩個參數。如下是一個實際的例子。

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
  JNIEnv* env = NULL;
  jint ret = vm->AttachCurrentThread(&env, NULL);
  LOG_ALWAYS_FATAL_IF(ret != JNI_OK, "AttachCurrentThread failed");
  android::RegisterDrawFunctor(env);
  android::RegisterDrawGLFunctor(env);
  android::RegisterGraphicsUtils(env);

  return JNI_VERSION_1_4;
}
複製代碼
const char kClassName[] = "com/android/webview/chromium/GraphicsUtils";
const JNINativeMethod kJniMethods[] = {
    { "nativeGetDrawSWFunctionTable", "()J",
        reinterpret_cast<void*>(GetDrawSWFunctionTable) },
    { "nativeGetDrawGLFunctionTable", "()J",
        reinterpret_cast<void*>(GetDrawGLFunctionTable) },
};

void RegisterGraphicsUtils(JNIEnv* env) {
  jclass clazz = env->FindClass(kClassName);
  LOG_ALWAYS_FATAL_IF(!clazz, "Unable to find class '%s'", kClassName);

  int res = env->RegisterNatives(clazz, kJniMethods, NELEM(kJniMethods));
  LOG_ALWAYS_FATAL_IF(res < 0, "register native methods failed: res=%d", res);
}
複製代碼

在調用RegisterNatives傳入clazz時,該類已經在FindClass時被加載。接着看看JNINativeMethod結構體。其內部存儲了三個字段,一個是方法名,一個是方法簽名,還有一個是JNI函數指針。經過clazz,方法名和方法簽名能夠惟一肯定Java世界中的一個方法。將其和JNI函數指針對應,便肯定了Java世界到Native世界一對一的映射規則。

RegisterNatives中的註冊過程也很簡單,經過方法名和方法簽名找到對應的ArtMethod對象,而後將JNI函數指針寫入其data_字段。所以它的註冊速度要優於靜態註冊。

註冊種類 註冊時機 註冊速度
靜態註冊 方法第一次被調用時經過art_jni_dlsym_lookup_stub函數註冊 普通,須要經過dlsym在庫中查找符號
動態註冊 庫加載時經過RegisterNatives函數註冊 快速

參考文章

  1. JNI調用和動態註冊探索
  2. JNI規則
  3. ART視角 | 爲何調用Native方法能夠進入C++世界
相關文章
相關標籤/搜索