本文分析基於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
便可進行註冊。具體的方法和細節留到"動態註冊"章節闡述。安全
一般而言,一個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方法在調用過程當中卻會多出一步。 oracle
這樣一來,不用存儲字節碼入口信息的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函數必須由JNIEXPORT
和JNICALL
進行修飾。
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_
的值有兩種可能:
quick_code
== nullptr。quick_code
== nullptr時,爲entry_point_from_quick_compiled_code_
設置的值是art_quick_generic_jni_trampoline
函數指針。它至關於一個通用的跳板函數,在執行過程當中動態進行參數轉換。data_
的值(不考慮CriticalNative)只有一種可能:
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函數註冊 | 快速 |
參考文章