由於在接下來的源碼分析中將涉及大量的Java和Native的互相調用。固然對於咱們的代碼分析沒有什麼影響,可是,這樣一個黑盒子擺在面前,對於其實現原理仍是充滿了好奇心。本篇將從JNI最基本的概念到簡單的代碼實例和其實現原理逐步展開。java
JNI(Java Native Interface,Java本地接口)是一種編程框架使得Java虛擬機中的Java程序能夠調用本地應用/或庫,也能夠被其餘程序調用。 本地程序通常是用其它語言C,C++或彙編語言編寫的, 而且被編譯爲基於本機硬件和操做系統的程序。在Android平臺,爲了更方便開發者的使用和加強其功能性,Android提供了NDK來更方便開發者的開發。android
JNI容許程序員用其餘編程語言來解決用純粹的Java代碼很差處理的狀況, 例如, Java標準庫不支持的平臺相關功能或者程序庫。也用於改造已存在的用其它語言寫的程序, 供Java程序調用。許多基於JNI的標準庫提供了不少功能給程序員使用, 例如文件I/O、音頻相關的功能。固然,也有各類高性能的程序,以及平臺相關的API實現, 容許全部Java應用程序安全而且平臺獨立地使用這些功能。Java層能夠用來負責UI功能實現,而C++負責進行計算操做。git
JNI框架容許Native方法調用Java對象,就像Java程序訪問Native對象同樣方便。Native方法能夠建立Java對象,讀取這些對象, 並調用Java對象執行某些方法。固然Native方法也能夠讀取由Java程序自身建立的對象,並調用這些對象的方法。程序員
這裏,咱們先經過一個簡單的Hello World實例來對JNI的調用流程有一個直觀的印象,而後針對其中的實現原理和細節作分析。編程
在此方法聲明中,使用 native 關鍵字的做用是告訴虛擬機,函數位於共享庫中(即在原生端實現)。數組
private native String helloWorld();
複製代碼
對於native方法的命名規則,函數名根據如下規則構建:安全
按照這些規則,此示例使用的函數名爲 Java_com_example_hellojni_HelloJni_stringFromJNI
。 此名稱描述 hellojni/src/com/example/hellojni/HelloJni.java
中一個名爲 stringFromJNI()的 Java 函數。咱們想經過更簡單的方式,讓寫native函數如同和寫java函數沒有這一步的轉化,那麼能夠經過javah來實現。bash
javah -d ../jni -jni com.chenjensen.myapplication.MainActivity
複製代碼
JNIEXPORT jstring JNICALL Java_com_chenjensen_myapplication_MainActivity_helloWorld
(JNIEnv *, jobject);
複製代碼
頭文件中生成了咱們的java文件中定義的native方法,也作好了類型轉化,咱們只須要新建一個cpp文件來實現相應的方法便可。app
JNIEXPORT jstring JNICALL Java_com_chenjensen_myapplication_MainActivity_helloWorld
(JNIEnv *env, jobject)
{
char *str = "Hello world";
return (*env).NewStringUTF(str);
}
複製代碼
ndk {
moduleName "hello" //生成的so文件名字,調用C程序的代碼中會用到該名字
abiFilters "armeabi", "armeabi-v7a", "x86" //輸出指定三種平臺下的so庫
}
複製代碼
這裏指定了生成so文件的name以後,編譯系統就會從JNI目錄下去尋找相應的c/cpp文件,來生成相應的so文件。框架
在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、dlsym提供一種動態轉載庫到內存的機制,在須要的時候,能夠調用庫中的方法。
在Java字節碼中,普通的方法是直接把字節碼放到code屬性表中,而native方法,與普通的方法經過一個標誌「ACC_NATIVE」區分開來。java在執行普通的方法調用的時候,能夠經過找方法表,再找到相應的code屬性表,最終解釋執行代碼。
在將動態庫load進來的時候,首先要作的第一步就是執行該動態庫的JNI_OnLoad
方法,咱們須要在該方法中聲明好native和java的關聯,系統中的相關類由於沒有提供該方法,所以須要手動調用了各自相應的註冊方法。而在咱們寫的demo中,編譯器則爲咱們作了這個操做,也不須要咱們來作。寫好映射關係以後,調用registerNativeMethods
方法來將這些方法進行註冊。具體的函數映射和註冊方式如上Runtime所示。
在編譯成的java代碼中,普通的Java方法會直接指向方法表中具體的方法,而對於native方法則是作了特殊的標記,在執行到native方法時,就會根據咱們以前加載進來的native的方法對應表中去查找相應的方法,而後執行。