序言
因為在接下來的原始碼分析中將涉及大量的Java和Native的互相呼叫。當然對於我們的程式碼分析沒有什麼影響,但是,這樣一個黑盒子擺在面前,對於其實現原理還是充滿了好奇心。本篇將從JNI最基本的概念到簡單的程式碼例項和其實現原理逐步展開。
JNI
JNI(Java Native Interface,Java本地介面)是一種程式設計框架使得Java虛擬機器中的Java程式可以呼叫本地應用/或庫,也可以被其他程式呼叫。 本地程式一般是用其它語言C,C++或組合語言編寫的, 並且被編譯為基於本機硬體和作業系統的程式。在Android平臺,為了更方便開發者的使用和增強其功能性,Android提供了NDK來更方便開發者的開發。
為什麼要有JNI?
JNI允許程式設計師用其他程式語言來解決用純粹的Java程式碼不好處理的情況, 例如, Java標準庫不支援的平臺相關功能或者程式庫。也用於改造已存在的用其它語言寫的程式, 供Java程式呼叫。許多基於JNI的標準庫提供了很多功能給程式設計師使用, 例如檔案I/O、音訊相關的功能。當然,也有各種高效能的程式,以及平臺相關的API實現, 允許所有Java應用程式安全並且平臺獨立地使用這些功能。Java層可以用來負責UI功能實現,而C++負責進行計算操作。
JNI框架允許Native方法呼叫Java物件,就像Java程式訪問Native物件一樣方便。Native方法可以建立Java物件,讀取這些物件, 並呼叫Java物件執行某些方法。當然Native方法也可以讀取由Java程式自身建立的物件,並呼叫這些物件的方法。
Hello World
這裡,我們先通過一個簡單的Hello World例項來對JNI的呼叫流程有一個直觀的印象,然後針對其中的實現原理和細節做分析。
1. 在Java檔案中定義native函式
在此方法宣告中,使用 native 關鍵字的作用是告訴虛擬機器,函式位於共享庫中(即在原生端實現)。
private native String helloWorld();
複製程式碼
2.利用Javah生成標頭檔案
對於native方法的命名規則,函式名根據以下規則構建:
- 在名稱前面加上 Java_。
- 描述與頂級源目錄相關的檔案路徑。
- 使用下劃線代替正斜槓。
- 刪掉 .java 副檔名。
- 在最後一個下劃線後,附加函式名。
按照這些規則,此示例使用的函式名為 Java_com_example_hellojni_HelloJni_stringFromJNI
。 此名稱描述 hellojni/src/com/example/hellojni/HelloJni.java
中一個名為 stringFromJNI()的 Java 函式。我們想通過更簡單的方式,讓寫native函式如同和寫java函式沒有這一步的轉化,那麼可以通過javah來實現。
javah -d ../jni -jni com.chenjensen.myapplication.MainActivity
複製程式碼
- d :標頭檔案輸出目錄
- jni:生成jni檔案
3.根據Javah生成的標頭檔案,實現相應的native函式
JNIEXPORT jstring JNICALL Java_com_chenjensen_myapplication_MainActivity_helloWorld
(JNIEnv *, jobject);
複製程式碼
標頭檔案中生成了我們的java檔案中定義的native方法,也做好了型別轉化,我們只需要新建一個cpp檔案來實現相應的方法即可。
4.cpp檔案
JNIEXPORT jstring JNICALL Java_com_chenjensen_myapplication_MainActivity_helloWorld
(JNIEnv *env, jobject)
{
char *str = "Hello world";
return (*env).NewStringUTF(str);
}
複製程式碼
5.build檔案中編譯支援指定的平臺(arm,x86等)
ndk {
moduleName "hello" //生成的so檔名字,呼叫C程式的程式碼中會用到該名字
abiFilters "armeabi", "armeabi-v7a", "x86" //輸出指定三種平臺下的so庫
}
複製程式碼
這裡指定了生成so檔案的name之後,編譯系統就會從JNI目錄下去尋找相應的c/cpp檔案,來生成相應的so檔案。
6.執行
在Java程式碼中,native方法的執行之前,要提前載入相應的動態庫,然後才可以執行,一般會在該類中通過靜態程式碼塊的方式來載入。應用啟動時,呼叫此函式以載入 .so 檔案。
static {
System.loadLibrary("hello");
}
複製程式碼
這個時候,我們在Java程式碼中呼叫相應的native程式碼就會生效了。
那麼在C/C++檔案中如何呼叫Java呢,這裡的呼叫方式和Java中通過反射查詢一個類的呼叫相似。核心函式為以下幾個。
FindClass(), NewObject(), GetStaticMethodID(),
GetMethodID(), CallStaticObjectMethod(), CallVoidMethod()
複製程式碼
找到相應的類,相應的方法,呼叫相應的類和方法。這裡不在給出具體的程式碼示例。可參考文章末尾給出的相應連結。
如何呼叫
通過上述6個步驟,我們便實現了Java呼叫native函式,藉助了相應的工具,我們可以很快的實現其互相呼叫,但是,工具也遮蔽掉了大量的實現細節,讓這個過程變成黑盒,不瞭解其實現。這個過程中, 當JVM呼叫這些函式,傳遞了一個JNIEnv指標,一個jobject的指標,任何在Java方法中宣告的Java引數。
一個JNI函式看起來類似這樣:
JNIEXPORT void JNICALL Java_ClassName_MethodName
(JNIEnv *env, jobject obj)
{
/*Implement Native Method Here*/
}
複製程式碼
Java和C++之間的呼叫,Java的執行需要在JVM上,因此在呼叫的時候,JVM必須知道要呼叫那一個本地函式,本地函式呼叫Java的時候,也必須要知道應用物件和具體的函式。
JNI中C++和Java的執行是在同一個執行緒,但是其執行緒值是不相同的。 JNIEnv是JNI的使用環境,JNIEnv物件是和執行緒繫結在一起的,在進行呼叫的時候,會傳遞一個JavaVM的指標作為引數,然後通過JavaVM的getEnv函式得到JNIEnv物件的指標。在Java中每次建立一個執行緒,都會生成新的JNIEnv物件。
在分析系統原始碼的時候,我們可以看到很多的java對於native的呼叫,通過對於原始碼的分析,我們發現在系統開機之後,就會有許多的Service程式被啟動,這個時候,而其很多實現都是通過native來實現的,這個時候如何呼叫,讓我們迴歸到系統的啟動過程中。在Zygote程式中首先會呼叫啟動VM。
if (startVm(&mJavaVM, &env, zygote) != 0) {
return;
}
onVmCreated(env);
if (startReg(env) < 0) {
return;
}
複製程式碼
int AndroidRuntime::startReg(JNIEnv* env)
{
if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
env->PopLocalFrame(NULL);
return -1;
}
....
return 0;
}
複製程式碼
static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env)
{
for (size_t i = 0; i < count; i++) {
if (array[i].mProc(env) < 0) {
return -1;
}
}
return 0;
}
複製程式碼
static const RegJNIRec gRegJNI[] = {
REG_JNI(register_com_android_internal_os_RuntimeInit),
REG_JNI(register_android_os_SystemClock),
REG_JNI(register_android_util_EventLog),
REG_JNI(register_android_util_Log),
.....
}
複製程式碼
array[i]是指gRegJNI陣列, 該陣列有100多個成員。其中每一項成員都是通過REG_JNI巨集定義。
#define REG_JNI(name) { name }
複製程式碼
struct RegJNIRec {
int (*mProc)(JNIEnv*);
};
複製程式碼
呼叫mProc,就等價於呼叫其引數名所指向的函式。 例如REG_JNI(register_com_android_internal_os_RuntimeInit).mProc也就是指進入register_com_android_internal_os_RuntimeInit方法,進入這些方法之後,就會是對於該類中的一些native方法和java方法的對映。
int register_com_android_internal_os_RuntimeInit(JNIEnv* env) {
return jniRegisterNativeMethods(env, "com/android/internal/os/RuntimeInit",
gMethods, NELEM(gMethods));
}
複製程式碼
//gMethods:java層方法名與jni層的方法的一一對映關係
static JNINativeMethod gMethods[] = {
{ "nativeFinishInit", "()V",
(void*) com_android_internal_os_RuntimeInit_nativeFinishInit },
{ "nativeZygoteInit", "()V",
(void*) com_android_internal_os_RuntimeInit_nativeZygoteInit },
{ "nativeSetExitWithoutCleanup", "(Z)V",
(void*) com_android_internal_os_RuntimeInit_nativeSetExitWithoutCleanup },
};
複製程式碼
至此就完成了對於native方法和Java方法的對映關聯。
- 另一種載入方式
對於JNI方法的註冊無非是通過兩種方式一個是上述啟動過程中的註冊,一個是在程式中通過System.loadLibrary
的方式進行註冊,這裡,我們以System.loadLibrary
來分析其註冊過程。
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
複製程式碼
public static Runtime getRuntime() {
return currentRuntime;
}
複製程式碼
synchronized void load0(Class fromClass, String filename) {
if (!(new File(filename).isAbsolute())) {
throw new UnsatisfiedLinkError(
"Expecting an absolute path of the library: " + filename);
}
if (filename == null) {
throw new NullPointerException("filename == null");
}
String error = doLoad(filename, fromClass.getClassLoader());
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
}
複製程式碼
String librarySearchPath = null;
if (loader != null && loader instanceof BaseDexClassLoader) {
BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
librarySearchPath = dexClassLoader.getLdLibraryPath();
}
synchronized (this) {
return nativeLoad(name, loader, librarySearchPath);
}
複製程式碼
經過層層呼叫之後來到了nativeLoad方法,這裡對於這段程式碼的分析,目的是為了瞭解,整個JNI的註冊過程和呼叫的時候,JVM是如何找到相應的native方法的。
對於nativeLoad執行的內容,會轉交到classLoader,最終會轉化為系統的呼叫,呼叫dlopen和dlsym函式。
- 呼叫dlopen函式,開啟一個so檔案並建立一個handle;
- 呼叫dlsym()函式,檢視相應so檔案的JNI_OnLoad()函式指標,並執行相應函式。
簡單的說,dlopen、dlsym提供一種動態轉載庫到記憶體的機制,在需要的時候,可以呼叫庫中的方法。
在Java位元組碼中,普通的方法是直接把位元組碼放到code屬性表中,而native方法,與普通的方法通過一個標誌“ACC_NATIVE”區分開來。java在執行普通的方法呼叫的時候,可以通過找方法表,再找到相應的code屬性表,最終解釋執行程式碼。
在將動態庫load進來的時候,首先要做的第一步就是執行該動態庫的JNI_OnLoad
方法,我們需要在該方法中宣告好native和java的關聯,系統中的相關類因為沒有提供該方法,因此需要手動呼叫了各自相應的註冊方法。而在我們寫的demo中,編譯器則為我們做了這個操作,也不需要我們來做。寫好對映關係之後,呼叫registerNativeMethods
方法來將這些方法進行註冊。具體的函式對映和註冊方式如上Runtime所示。
在編譯成的java程式碼中,普通的Java方法會直接指向方法表中具體的方法,而對於native方法則是做了特殊的標記,在執行到native方法時,就會根據我們之前載入進來的native的方法對應表中去查詢相應的方法,然後執行。