Android JNI原理分析
引言:分析Android原始碼6.0的過程,一定離不開Java與C/C++程式碼直接的來回跳轉,那麼就很有必要掌握JNI,這是連結Java層和Native層的橋樑,本文涉及相關原始碼:
frameworks/base/core/jni/AndroidRuntime.cpp
libcore/luni/src/main/java/java/lang/System.java
libcore/luni/src/main/java/java/lang/Runtime.java
libnativehelper/JNIHelp.cpp
libnativehelper/include/nativehelper/jni.h
frameworks/base/core/java/android/os/MessageQueue.java
frameworks/base/core/jni/android_os_MessageQueue.cpp
frameworks/base/core/java/android/os/Binder.java
frameworks/base/core/jni/android_util_Binder.cpp
frameworks/base/media/java/android/media/MediaPlayer.java
frameworks/base/media/jni/android_media_MediaPlayer.cpp
一、JNI概述
JNI(Java Native Interface,Java本地介面),用於打通Java層與Native(C/C++)層。這不是Android系統所獨有的,而是Java所有。眾所周知,Java語言是跨平臺的語言,而這跨平臺的背後都是依靠Java虛擬機器,虛擬機器採用C/C++編寫,適配各個系統,通過JNI為上層Java提供各種服務,保證跨平臺性。
相信不少經常使用Java的程式設計師,享受著其跨平臺性,可能全然不知JNI的存在。在Android平臺,讓JNI大放異彩,為更多的程式設計師所熟知,往往為了提供效率或者其他功能需求,就需要NDK開發。上一篇文章Linux系統呼叫(syscall)原理,介紹了打通android上層與底層kernel的樞紐syscall,那麼本文的目的則是介紹打通android上層中Java層與Native的紐帶JNI。
二、JNI查詢方式
Android系統在啟動啟動過程中,先啟動Kernel建立init程式,緊接著由init程式fork第一個橫穿Java和C/C++的程式,即Zygote程式。Zygote啟動過程中會AndroidRuntime.cpp
中的startVm
建立虛擬機器,VM建立完成後,緊接著呼叫startReg
完成虛擬機器中的JNI方法註冊。
2.1 startReg
[–>AndroidRuntime.cpp]
int AndroidRuntime::startReg(JNIEnv* env)
{
//設定執行緒建立方法為javaCreateThreadEtc
androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
env->PushLocalFrame(200);
//程式NI方法的註冊
if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
env->PopLocalFrame(NULL);
return -1;
}
env->PopLocalFrame(NULL);
return 0;
}
register_jni_procs(gRegJNI, NELEM(gRegJNI), env)這行程式碼的作用就是就是迴圈呼叫gRegJNI
陣列成員所對應的方法。
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;
}
gRegJNI
陣列,有100多個成員變數,定義在AndroidRuntime.cpp
:
static const RegJNIRec gRegJNI[] = {
REG_JNI(register_android_os_MessageQueue),
REG_JNI(register_android_os_Binder),
...
};
該陣列的每個成員都代表一個類檔案的jni對映,其中REG_JNI是一個巨集定義,在Zygote中介紹過,該巨集的作用就是呼叫相應的方法。
2.2 如何查詢native方法
當大家在看framework層程式碼時,經常會看到native方法,這是往往需要檢視所對應的C++方法在哪個檔案,對應哪個方法?下面從一個例項出發帶大家如何檢視java層方法所對應的native方法位置。
2.2.1 例項(一)
當分析Android訊息機制原始碼,遇到MessageQueue.java
中有多個native方法,比如:
private native void nativePollOnce(long ptr, int timeoutMillis);
步驟1: MessageQueue.java
的全限定名為android.os.MessageQueue.java,方法名:android.os.MessageQueue.nativePollOnce(),而相對應的native層方法名只是將點號替換為下劃線,可得android_os_MessageQueue_nativePollOnce()
。 Tips: nativePollOnce ==> android_os_MessageQueue_nativePollOnce()
步驟2: 有了native方法,那麼接下來需要知道該native方法所在那個檔案。前面已經介紹過Android系統啟動時就已經註冊了大量的JNI方法,見AndroidRuntime.cpp的gRegJNI
陣列。這些註冊方法命令方式:
register_[包名]_[類名]
那麼MessageQueue.java所定義的jni註冊方法名應該是register_android_os_MessageQueue
,的確存在於gRegJNI陣列,說明這次JNI註冊過程是有開機過程完成的。 該方法在AndroidRuntime.cpp
申明為extern方法:
extern int register_android_os_MessageQueue(JNIEnv* env);
這些extern方法絕大多數位於/framework/base/core/jni/
目錄,大多數情況下native檔案命名方式:
[包名]_[類名].cpp
[包名]_[類名].h
Tips: MessageQueue.java ==> android_os_MessageQueue.cpp
開啟android_os_MessageQueue.cpp
檔案,搜尋android_os_MessageQueue_nativePollOnce方法,這便找到了目標方法:
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj, jlong ptr, jint timeoutMillis) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}
到這裡完成了一次從Java層方法搜尋到所對應的C++方法的過程。
2.2.2 例項(二)
對於native檔案命名方式,有時並非[包名]_[類名].cpp
,比如Binder.java
Binder.java所對應的native檔案:android_util_Binder.cpp
public static final native int getCallingPid();
根據例項(一)方式,找到getCallingPid ==> android_os_Binder_getCallingPid(),並且在AndroidRuntime.cpp中的gRegJNI陣列中找到register_android_os_Binder
。
按例項(一)方式則native文名應該為android_os_Binder.cpp,可是在/framework/base/core/jni/
目錄下找不到該檔案,這是例外的情況。其實真正的檔名為android_util_Binder.cpp
,這就是例外,這一點有些費勁,不明白為何google要如此打破規律的命名。
static jint android_os_Binder_getCallingPid(JNIEnv* env, jobject clazz)
{
return IPCThreadState::self()->getCallingPid();
}
有人可能好奇,既然如何遇到打破常規的檔案命令,怎麼辦?這個並不難,首先,可以嘗試在/framework/base/core/jni/
中搜尋,對於binder.java,可以直接搜尋binder關鍵字,其他也類似。如果這裡也找不到,可以通過grep全域性搜尋android_os_Binder_getCallingPid
這個方法在哪個檔案。
jni存在的常見目錄:
/framework/base/core/jni/
/framework/base/services/core/jni/
/framework/base/media/jni/
2.2.3 例項(三)
前面兩種都是在Android系統啟動之初,便已經註冊過JNI所對應的方法。 那麼如果程式自己定義的jni方法,該如何檢視jni方法所在位置呢?下面以MediaPlayer.java為例,其包名為android.media:
public class MediaPlayer{
static {
System.loadLibrary("media_jni");
native_init();
}
private static native final void native_init();
...
}
通過static靜態程式碼塊中System.loadLibrary方法來載入動態庫,庫名為media_jni
, Android平臺則會自動擴充套件成所對應的libmedia_jni.so
庫。 接著通過關鍵字native
加在native_init方法之前,便可以在java層直接使用native層方法。
接下來便要檢視libmedia_jni.so
庫定義所在檔案,一般都是通過Android.mk
檔案定義LOCAL_MODULE:= libmedia_jni,可以採用grep或者mgrep來搜尋包含libmedia_jni欄位的Android.mk所在路徑。
搜尋可知,libmedia_jni.so位於/frameworks/base/media/jni/Android.mk。用前面例項(一)中的知識來檢視相應的檔案和方法名分別為:
android_media_MediaPlayer.cpp
android_media_MediaPlayer_native_init()
再然後,你會發現果然在該Android.mk所在目錄/frameworks/base/media/jni/
中找到android_media_MediaPlayer.cpp檔案,並在檔案中存在相應的方法:
static void
android_media_MediaPlayer_native_init(JNIEnv *env)
{
jclass clazz;
clazz = env->FindClass("android/media/MediaPlayer");
fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
...
}
Tips:MediaPlayer.java中的native_init方法所對應的native方法位於/frameworks/base/media/jni/
目錄下的android_media_MediaPlayer.cpp
檔案中的android_media_MediaPlayer_native_init
方法。
2.3 小結
JNI作為連線Java世界和C/C++世界的橋樑,很有必要掌握。看完本文,至少能掌握在分析Android原始碼過程中如何查詢native方法。首先要明白native方法名和檔名的命名規律,其次要懂得該如何去搜尋程式碼。 JNI方式註冊無非是Android系統啟動過程中Zygote註冊以及通過System.loadLibrary方式註冊,對於系統啟動過程註冊的,可以通過查詢AndroidRuntime.cpp
中的gRegJNI
是否存在對應的register方法,如果不存在,則大多數情況下是通過LoadLibrary方式來註冊。
三、 JNI原理分析
再進一步來分析,Java層與native層方法是如何註冊並對映的,繼續以MediaPlayer為例。
在檔案MediaPlayer.java中呼叫System.loadLibrary("media_jni")
把libmedia_jni.so動態庫載入到記憶體。接下來,以loadLibrary為起點展開JNI註冊流程的過程分析。
3.1 loadLibrary
[System.java]
public static void loadLibrary(String libName) {
//接下來呼叫Runtime方法
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}
[Runtime.java]
void loadLibrary(String libraryName, ClassLoader loader) {
//loader不會空,則進入該分支
if (loader != null) {
//查詢庫所在路徑
String filename = loader.findLibrary(libraryName);
if (filename == null) {
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
//載入庫
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
//loader為空,則會進入該分支
String filename = System.mapLibraryName(libraryName);
List<String> candidates = new ArrayList<String>();
String lastError = null;
for (String directory : mLibPaths) {
String candidate = directory + filename;
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
//載入庫
String error = doLoad(candidate, loader);
if (error == null) {
return;//載入成功
}
lastError = error;
}
}
if (lastError != null) {
throw new UnsatisfiedLinkError(lastError);
}
throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}
真正載入的工作是由doLoad()
,該方法內部增加同步鎖,保證併發時一致性。
private String doLoad(String name, ClassLoader loader) {
...
synchronized (this) {
return nativeLoad(name, loader, ldLibraryPath);
}
}
nativeLoad()這是一個native方法,再進入ART虛擬機器java_lang_Runtime.cc
,再細講就要深入剖析虛擬機器內部,這裡就不再往下深入了,後續博主有空再展開art虛擬機器系列的文章,這裡直接說結論:
- 呼叫
dlopen
函式,開啟一個so檔案並建立一個handle; - 呼叫
dlsym()
函式,檢視相應so檔案的JNI_OnLoad()
函式指標,並執行相應函式。
總之,System.loadLibrary()的作用就是呼叫相應庫中的JNI_OnLoad()方法。接下來說說JNI_OnLoad()過程。
3.2 JNI_OnLoad
[-> android_media_MediaPlayer.cpp]
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
//【見3.3】 註冊JNI方法
if (register_android_media_MediaPlayer(env) < 0) {
goto bail;
}
...
}
3.3 register_android_media_MediaPlayer
[-> android_media_MediaPlayer.cpp]
static int register_android_media_MediaPlayer(JNIEnv *env) {
//【見3.4】
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaPlayer", gMethods, NELEM(gMethods));
}
其中gMethods
,記錄java層和C/C++層方法的一一對映關係。
static JNINativeMethod gMethods[] = {
{"prepare", "()V", (void *)android_media_MediaPlayer_prepare},
{"_start", "()V", (void *)android_media_MediaPlayer_start},
{"_stop", "()V", (void *)android_media_MediaPlayer_stop},
{"seekTo", "(I)V", (void *)android_media_MediaPlayer_seekTo},
{"_release", "()V", (void *)android_media_MediaPlayer_release},
{"native_init", "()V", (void *)android_media_MediaPlayer_native_init},
...
};
這裡涉及到結構體JNINativeMethod
,其定義在jni.h
檔案:
typedef struct {
const char* name; //Java層native函式名
const char* signature; //Java函式簽名,記錄引數型別和個數,以及返回值型別
void* fnPtr; //Native層對應的函式指標
} JNINativeMethod;
關於函式簽名signature
在下一小節展開說明。
3.4 registerNativeMethods
[-> AndroidRuntime.cpp]
int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods)
{
//【見3.5】
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
jniRegisterNativeMethods該方法是由Android JNI幫助類JNIHelp.cpp
來完成。
3.5 jniRegisterNativeMethods
[-> JNIHelp.cpp]
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) {
JNIEnv* e = reinterpret_cast<JNIEnv*>(env);
scoped_local_ref<jclass> c(env, findClass(env, className));
if (c.get() == NULL) {
e->FatalError("");//無法查詢native註冊方法
}
//【見3.6】 呼叫JNIEnv結構體的成員變數
if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
e->FatalError("");//native方法註冊失敗
}
return 0;
}
3.6 RegisterNatives
[-> jni.h]
struct _JNIEnv {
const struct JNINativeInterface* functions;
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods) { return functions->RegisterNatives(this, clazz, methods, nMethods); }
...
}
functions是指向JNINativeInterface
結構體指標,也就是將呼叫下面方法:
struct JNINativeInterface {
jint (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*,jint);
...
}
再往下深入就到了虛擬機器內部吧,這裡就不再往下深入了。 總之,這個過程完成了gMethods
陣列中的方法的對映關係,比如java層的native_init()方法,對映到native層的android_media_MediaPlayer_native_init()方法。
虛擬機器相關的變數中有兩個非常重要的量JavaVM和JNIEnv:
-
JavaVM
:是指程式虛擬機器環境,每個程式有且只有一個JavaVM例項 -
JNIEnv
:是指執行緒上下文環境,每個執行緒有且只有一個JNIEnv例項,
四、JNI資源
JNINativeMethod結構體中有一個欄位為signature(簽名),再介紹signature格式之前需要掌握各種資料型別在Java層、Native層以及簽名所採用的簽名格式。
4.1 資料型別
4.1.1 基本資料型別
Signature格式 | Java | Native |
---|---|---|
B | byte | jbyte |
C | char | jchar |
D | double | jdouble |
F | float | jfloat |
I | int | jint |
S | short | jshort |
J | long | jlong |
Z | boolean | jboolean |
V | void | void |
4.1.2 陣列資料型別
陣列簡稱則是在前面新增[
:
Signature格式 | Java | Native |
---|---|---|
[B | byte[] | jbyteArray |
[C | char[] | jcharArray |
[D | double[] | jdoubleArray |
[F | float[] | jfloatArray |
[I | int[] | jintArray |
[S | short[] | jshortArray |
[J | long[] | jlongArray |
[Z | boolean[] | jbooleanArray |
4.1.3 複雜資料型別
物件型別簡稱:L+classname +;
Signature格式 | Java | Native |
---|---|---|
Ljava/lang/String; | String | jstring |
L+classname +; | 所有物件 | jobject |
[L+classname +; | Object[] | jobjectArray |
Ljava.lang.Class; | Class | jclass |
Ljava.lang.Throwable; | Throwable | jthrowable |
4.1.4 Signature
有了前面的鋪墊,那麼再來通過例項說說函式簽名: (輸入引數...)返回值引數
,這裡用到的便是前面介紹的Signature格式。
Java函式 | 對應的簽名 |
---|---|
void foo() | ()V |
float foo(int i) | (I)F |
long foo(int[] i) | ([I)J |
double foo(Class c) | (Ljava/lang/Class;)D |
boolean foo(int[] i,String s) | ([ILjava/lang/String;)Z |
String foo(int i) | (I)Ljava/lang/String; |
4.2 其他
(一)垃圾回收 對於Java開發人員來說無需關係垃圾回收,完全由虛擬機器GC來負責垃圾回收,而對於JNI開發人員,對於記憶體釋放需要謹慎處理,需要的時候申請,使用完記得釋放內容,以免發生記憶體洩露。在JNI提供了三種Reference型別,Local Reference(本地引用), Global Reference(全域性引用), Weak Global Reference(全域性弱引用)。其中Global Reference如果不主動釋放,則一直不會釋放;對於其他兩個型別的引用都是釋放的可能性,那是不是意味著不需要手動釋放呢?答案是否定的,不管是這三種型別的那種引用,都儘可能在某個記憶體不再需要時,立即釋放,這對系統更為安全可靠,以減少不可預知的效能與穩定性問題。
另外,ART虛擬機器在GC演算法有所優化,為了減少記憶體碎片化問題,在GC之後有可能會移動物件記憶體的位置,對於Java層程式並沒有影響,但是對於JNI程式可要小心了,對於通過指標來直接訪問記憶體物件是,Dalvik能正確執行的程式,ART下未必能正常執行。
(二)異常處理 Java層出現異常,虛擬機器會直接丟擲異常,這是需要try..catch或者繼續往外throw。但是對於JNI出現異常時,即執行到JNIEnv中某個函式異常時,並不會立即丟擲異常來中斷程式的執行,還可以繼續執行記憶體之類的清理工作,直到返回到Java層時才會丟擲相應的異常。
另外,Dalvik
虛擬機器有些情況下JNI函式出錯可能返回NULL,但ART
虛擬機器在出錯時更多的是丟擲異常。這樣導致的問題就可能是在Dalvik版本能正常執行的程式,在ART虛擬機器上由於沒有正確處理異常而崩潰。
總結
本文主要通過例項,基於Android 6.0原始碼來分析JNI原理,講述JNI核心功能:
- 介紹瞭如何查詢JNI方法,讓大家明白如何從Java層跳轉到Native層;
- 分析了JNI函式註冊流程,進一步加深對JNI的理解;
- 列舉Java與native以及函式簽名方式。
相關文章
- Android系統原始碼分析-JNIAndroid原始碼
- Android 深入理解 JNI(一)JNI 原理與靜態、動態註冊Android
- android: jni socketAndroid
- Android LowMemoryKiller 原理分析Android
- Android studio jniAndroid
- Android JNI 之 Bitmap 操作Android
- JNI原始碼分析(並實現JNI動態註冊)原始碼
- Android JNI的基本使用(CMake)Android
- 【Android Jetpack教程】ViewModel原理分析AndroidJetpackView
- Android深色模式適配原理分析Android模式
- 使用CMake構建Android JNI工程Android
- Android JNI開發系列之配置Android
- Android Studio jni - 入門篇Android
- Android JNI 程式碼自動生成Android
- Android Studio中jni的使用Android
- C的指標(Android之JNI)指標Android
- Android UI 顯示原理分析小結AndroidUI
- Android 常用換膚方式以及原理分析Android
- 在 Android 中使用 JNI 的總結Android
- Android NDK開發之JNI基礎Android
- Android JNI 中的執行緒操作Android執行緒
- Android中的JNI入門實戰Android
- C中資料型別(Android之JNI)資料型別Android
- Android Framework層JNI的使用淺析AndroidFramework
- android使用JNI呼叫C,C++程式AndroidC++
- (連載)Android 8.0 : Android虛擬機器之JNIAndroid虛擬機
- Android進階5:SurfaceView實現原理分析AndroidView
- Android Hook框架Xposed原理與原始碼分析AndroidHook框架原始碼
- Android 的 Handler 機制實現原理分析Android
- Android AsyncTask運作原理和原始碼分析Android原始碼
- Android:JNI 與 NDK到底是什麼?Android
- Android JNI 篇 - 編譯 bilibili/ijkPlayerAndroid編譯
- Android 通過JNI實現守護程式Android
- 第一個C程式HelloWold(Android之JNI)C程式Android
- 指標常見問題(Android之JNI)指標Android
- Android - JNI加入標準C++檔案AndroidC++
- Android JNI和NDK有什麼區別Android
- [Android開發]Mac下NDK開發(JNI)AndroidMac