Android系統(tǒng)源碼分析-JNI
因為在接下來的源碼分析中將涉及大量的Java和Native的互相調用怖糊。當然對于我們的代碼分析沒有什么影響帅容,但是,這樣一個黑盒子擺在面前伍伤,對于其實現(xiàn)原理還是充滿了好奇心并徘。本篇將從JNI最基本的概念到簡單的代碼實例和其實現(xiàn)原理逐步展開。序言
1.JNI
JNI(Java Native Interface,Java本地接口)是一種編程框架使得Java虛擬機中的Java程序可以調用本地應用/或庫,也可以被其他程序調用扰魂。 本地程序一般是用其它語言C麦乞,C++或匯編語言編寫的, 并且被編譯為基于本機硬件和操作系統(tǒng)的程序。在Android平臺劝评,為了更方便開發(fā)者的使用和增強其功能性姐直,Android提供了NDK來更方便開發(fā)者的開發(fā)。
2.為什么要有JNI蒋畜?
JNI允許程序員用其他編程語言來解決用純粹的Java代碼不好處理的情況, 例如, Java標準庫不支持的平臺相關功能或者程序庫声畏。也用于改造已存在的用其它語言寫的程序, 供Java程序調用。許多基于JNI的標準庫提供了很多功能給程序員使用, 例如文件I/O姻成、音頻相關的功能插龄。當然,也有各種高性能的程序科展,以及平臺相關的API實現(xiàn), 允許所有Java應用程序安全并且平臺獨立地使用這些功能均牢。Java層可以用來負責UI功能實現(xiàn),而C++負責進行計算操作辛润。
JNI框架允許Native方法調用Java對象膨处,就像Java程序訪問Native對象一樣方便见秤。Native方法可以創(chuàng)建Java對象砂竖,讀取這些對象, 并調用Java對象執(zhí)行某些方法。當然Native方法也可以讀取由Java程序自身創(chuàng)建的對象,并調用這些對象的方法鹃答。
3.Hello World
這里乎澄,我們先通過一個簡單的Hello World實例來對JNI的調用流程有一個直觀的印象,然后針對其中的實現(xiàn)原理和細節(jié)做分析测摔。
在Java文件中定義native函數(shù)
在此方法聲明中置济,使用 native 關鍵字的作用是告訴虛擬機解恰,函數(shù)位于共享庫中(即在原生端實現(xiàn))。
private?native?String?helloWorld();
利用Javah生成頭文件
對于native方法的命名規(guī)則浙于,函數(shù)名根據(jù)以下規(guī)則構建:
在名稱前面加上 Java_护盈。
描述與頂級源目錄相關的文件路徑。
使用下劃線代替正斜杠羞酗。
刪掉 .java 文件擴展名腐宋。
在最后一個下劃線后,附加函數(shù)名檀轨。
按照這些規(guī)則胸竞,此示例使用的函數(shù)名為Java_com_example_hellojni_HelloJni_stringFromJNI。 此名稱描述hellojni/src/com/example/hellojni/HelloJni.java中一個名為 stringFromJNI()的 Java 函數(shù)参萄。我們想通過更簡單的方式卫枝,讓寫native函數(shù)如同和寫java函數(shù)沒有這一步的轉化,那么可以通過javah來實現(xiàn)讹挎。
javah -d ../jni -jni?com.chenjensen.myapplication.MainActivity
d :頭文件輸出目錄
jni:生成jni文件
根據(jù)Javah生成的頭文件校赤,實現(xiàn)相應的native函數(shù)
JNIEXPORT jstring JNICALL?
Java_com_chenjensen_myapplication_MainActivity_helloWorld
(JNIEnv *, jobject)
;
頭文件中生成了我們的java文件中定義的native方法,也做好了類型轉化筒溃,我們只需要新建一個cpp文件來實現(xiàn)相應的方法即可痒谴。
cpp文件
JNIEXPORT jstring JNICALL
Java_com_chenjensen_myapplication_MainActivity_helloWorld
(JNIEnv *env, jobject)
{
char?*str =?"Hello world";
return?(*env).NewStringUTF(str);
}
build文件中編譯支持指定的平臺(arm,x86等)
ndk?{
?moduleName?"hello"??
abiFilters?"armeabi",?"armeabi-v7a",?"x86"?}
這里指定了生成so文件的name之后铡羡,編譯系統(tǒng)就會從JNI目錄下去尋找相應的c/cpp文件积蔚,來生成相應的so文件。
執(zhí)行
在Java代碼中烦周,native方法的執(zhí)行之前尽爆,要提前加載相應的動態(tài)庫,然后才可以執(zhí)行读慎,一般會在該類中通過靜態(tài)代碼塊的方式來加載漱贱。應用啟動時,調用此函數(shù)以加載 .so 文件夭委。
static?{
System.loadLibrary("hello");
}
這個時候幅狮,我們在Java代碼中調用相應的native代碼就會生效了。
那么在C/C++文件中如何調用Java呢株灸,這里的調用方式和Java中通過反射查找一個類的調用相似崇摄。核心函數(shù)為以下幾個。
FindClass(),?
NewObject(),?
GetStaticMethodID(),?
GetMethodID(),?
CallStaticObjectMethod(),
CallVoidMethod()
找到相應的類慌烧,相應的方法逐抑,調用相應的類和方法。這里不在給出具體的代碼示例屹蚊〔薨保可參考文章末尾給出的相應鏈接进每。
4.如何調用
通過上述6個步驟,我們便實現(xiàn)了Java調用native函數(shù)命斧,借助了相應的工具田晚,我們可以很快的實現(xiàn)其互相調用,但是国葬,工具也屏蔽掉了大量的實現(xiàn)細節(jié)肉瓦,讓這個過程變成黑盒,不了解其實現(xiàn)胃惜。這個過程中泞莉,
當JVM調用這些函數(shù),傳遞了一個JNIEnv指針船殉,一個jobject的指針鲫趁,任何在Java方法中聲明的Java參數(shù)。
一個JNI函數(shù)看起來類似這樣:
JNIEXPORT?void?JNICALL?Java_ClassName_MethodName
(JNIEnv *env, jobject obj)
{
}
Java和C++之間的調用利虫,Java的執(zhí)行需要在JVM上挨厚,因此在調用的時候,JVM必須知道要調用那一個本地函數(shù)糠惫,本地函數(shù)調用Java的時候疫剃,也必須要知道應用對象和具體的函數(shù)。
JNI中C++和Java的執(zhí)行是在同一個線程硼讽,但是其線程值是不相同的巢价。
JNIEnv是JNI的使用環(huán)境,JNIEnv對象是和線程綁定在一起的固阁,在進行調用的時候壤躲,會傳遞一個JavaVM的指針作為參數(shù),然后通過JavaVM的getEnv函數(shù)得到JNIEnv對象的指針备燃。在Java中每次創(chuàng)建一個線程碉克,都會生成新的JNIEnv對象。
在分析系統(tǒng)源碼的時候并齐,我們可以看到很多的java對于native的調用漏麦,通過對于源碼的分析,我們發(fā)現(xiàn)在系統(tǒng)開機之后况褪,就會有許多的Service進程被啟動撕贞,這個時候,而其很多實現(xiàn)都是通過native來實現(xiàn)的窝剖,這個時候如何調用麻掸,讓我們回歸到系統(tǒng)的啟動過程中酥夭。在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數(shù)組, 該數(shù)組有100多個成員脊奋。其中每一項成員都是通過REG_JNI宏定義。
#define?REG_JNI(name){?
name?
}
struct?RegJNIRec?{
int?(*mProc)(JNIEnv*);
};
調用mProc疙描,就等價于調用其參數(shù)名所指向的函數(shù)诚隙。 例如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));
}
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方法的映射關聯(lián)。
另一種加載方式
對于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);
}
經(jīng)過層層調用之后來到了nativeLoad方法脉执,這里對于這段代碼的分析,目的是為了了解戒劫,整個JNI的注冊過程和調用的時候半夷,JVM是如何找到相應的native方法的。
對于nativeLoad執(zhí)行的內(nèi)容迅细,會轉交到classLoader巫橄,最終會轉化為系統(tǒng)的調用,調用dlopen和dlsym函數(shù)茵典。
調用dlopen函數(shù)湘换,打開一個so文件并創(chuàng)建一個handle;
調用dlsym()函數(shù)统阿,查看相應so文件的JNI_OnLoad()函數(shù)指針枚尼,并執(zhí)行相應函數(shù)。
簡單的說砂吞,dlopen署恍、dlsym提供一種動態(tài)轉載庫到內(nèi)存的機制,在需要的時候蜻直,可以調用庫中的方法盯质。
在Java字節(jié)碼中,普通的方法是直接把字節(jié)碼放到code屬性表中概而,而native方法呼巷,與普通的方法通過一個標志“ACC_NATIVE”區(qū)分開來。java在執(zhí)行普通的方法調用的時候赎瑰,可以通過找方法表王悍,再找到相應的code屬性表,最終解釋執(zhí)行代碼餐曼。
在將動態(tài)庫load進來的時候压储,首先要做的第一步就是執(zhí)行該動態(tài)庫的JNI_OnLoad方法鲜漩,我們需要在該方法中聲明好native和java的關聯(lián),系統(tǒng)中的相關類因為沒有提供該方法集惋,因此需要手動調用了各自相應的注冊方法孕似。而在我們寫的demo中,編譯器則為我們做了這個操作刮刑,也不需要我們來做喉祭。寫好映射關系之后,調用registerNativeMethods方法來將這些方法進行注冊雷绢。具體的函數(shù)映射和注冊方式如上Runtime所示泛烙。
在編譯成的java代碼中,普通的Java方法會直接指向方法表中具體的方法翘紊,而對于native方法則是做了特殊的標記胶惰,在執(zhí)行到native方法時,就會根據(jù)我們之前加載進來的native的方法對應表中去查找相應的方法霞溪,然后執(zhí)行孵滞。
作者 |?Jensen_czx
地址 |?http://www.reibang.com/p/e6b9611f3045
聲明 |?本文是 Jensen_czx?原創(chuàng),已獲授權發(fā)布鸯匹,未經(jīng)原作者允許請勿轉載