研究了一下Android JNI,有幾個知識點不太懂
本文轉載自微信公眾號「程序喵大人」,作者程序喵大人 。轉載本文請聯系程序喵大人公眾號。
Java線程與Native(OS)線程的區別
聯系:Java線程其實是一層OS線程的封裝,本質上就是OS線程。【以前版本的Java線程不是OS線程,是JVM構造的用戶態線程(Green Thread),不能充分利用CPU,后期已經更改為使用OS線程實現。】【參考https://mp.weixin.qq.com/s/Gxqnf5vjyaI8eSYejm7zeQ】
區別:
Java線程可以直接拿到JNIEnv,OS線程需要先attach到JVM,才可以拿到JNIEnv。【個人理解區別在于是否attach了JVM】
- jint AttachCurrentThread(JavaVM *vm, void **p_env, void *thr_args);
Java線程可以FindClass成功,OS線程則FindClass失敗,原因是兩者的ClassLoader不同,OS線程AttachCurrentThread后持有的ClassLoader是系統的ClassLoader,如果想要FindClass成功,需要在JNI_Onload時獲取一份當前庫的ClassLoader保存起來,下次FindClass時使用此ClassLoader去操作。
- static jobject g_class_loader = NULL;
- static jmethodID g_find_class_method = NULL;
- void on_load() {
- JNIEnv *env = get_jni_env();
- if (!env) {
- return;
- }
- jclass capture_class = (*env)->FindClass(env, "com/captureandroid/BMMCaptureEngine");
- jclass class_class = (*env)->GetObjectClass(env, capture_class);
- jclass class_loader_class = (*env)->FindClass(env, "java/lang/ClassLoader");
- jmethodID class_loader_mid = (*env)->GetMethodID(env, class_class, "getClassLoader", "()Ljava/lang/ClassLoader;");
- jobject local_class_loader = (*env)->CallObjectMethod(env, capture_class, class_loader_mid);
- g_class_loader = (*env)->NewGlobalRef(env, local_class_loader);
- g_find_class_method =
- (*env)->GetMethodID(env, class_loader_class, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;");
- }
- jclass find_class(const char *name) {
- JNIEnv *env = bmm_util_get_jni_env();
- if (!env) {
- return NULL;
- }
- jclass ret = (*env)->FindClass(env, name);
- jthrowable exception = (*env)->ExceptionOccurred(env);
- if (exception) {
- (*env)->ExceptionClear(env);
- jstring name_str = (*env)->NewStringUTF(env, name);
- ret = (jclass)(*env)->CallObjectMethod(env, g_class_loader, g_find_class_method, name_str);
- (*env)->DeleteLocalRef(env, name_str);
- }
- return ret;
- }
JNI的作用
貼出別人翻譯的【官方文檔https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/design.html#wp16696】的一段話:
JNI最重要的設計目標就是在不同操作系統上的JVM之間提供二進制兼容,做到一個本地庫不需要重新編譯就可以運行不同的系統的JVM上面。為了達到這一點兒,JNI設計時不能關心JVM的內部實現,因為JVM的內部實現機制在不斷地變,而我們必須保持JNI接口的穩定。JNI的第二個設計目標就是高效。我們可能會看到,有時為了滿足第一個目標,可能需要犧牲一點兒效率,因此,我們需要在平臺無關和效率之間做一些選擇。最后,JNI必須是一個完整的體系。它必須提供足夠多的JVM功能讓本地程序完成一些有用的任務。JNI不能只針對一款特定的JVM,而是要提供一系列標準的接口讓程序員可以把他們的本地代碼庫加載到不同的JVM中去。有時,調用特定JVM下實現的接口可以提供效率,但更多的情況下,我們需要用更通用的接口來解決問題。
JNIEnv和JavaVM
就是個函數指針。
下圖是JNIEnv的指針結構:
JNIEnv其實是一個指向本地線程數據的接口指針,指針里面包含指向函數接口的指針,每一個接口函數在這表中都有一個預定義的偏移位置,類似C++虛函數表。
代碼如下:
- typedef const struct JNINativeInterface *JNIEnv;
- struct JNINativeInterface {
- void* reserved0;
- void* reserved1;
- void* reserved2;
- void* reserved3;
- jint (*GetVersion)(JNIEnv *);
- jclass (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*,
- jsize);
- jclass (*FindClass)(JNIEnv*, const char*);
- jobject (*AllocObject)(JNIEnv*, jclass);
- jobject (*NewObject)(JNIEnv*, jclass, jmethodID, ...);
- jobject (*NewObjectV)(JNIEnv*, jclass, jmethodID, va_list);
- jobject (*NewObjectA)(JNIEnv*, jclass, jmethodID, const jvalue*);
- ...
- };
- JavaVM類似
- struct JNIInvokeInterface {
- void* reserved0;
- void* reserved1;
- void* reserved2;
- jint (*DestroyJavaVM)(JavaVM*);
- jint (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
- jint (*DetachCurrentThread)(JavaVM*);
- jint (*GetEnv)(JavaVM*, void**, jint);
- jint (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
- };
- typedef const struct JNIInvokeInterface* JavaVM;
知識點1:為什么使用函數表而不是寫死某些函數項?
可將JNI命名空間與本地代碼分離,一個虛擬機可以提供多個版本的JNI函數表,用于不同場景。例如,虛擬機可支持兩種JNI函數表:
一個用于調試,做較多的錯誤檢查。
一個用于發布,做較少的錯誤檢查,更高效。
知識點2:JNIEnv是thread-local,只在當前線程有效,Native方法不能將JNIenv從當前線程傳遞到另一個線程。不能跨線程使用JNIEnv【至于JNIEnv為什么設計成thread-local,沒搞明白】。
知識點3:線程間雖然不共享JNIEnv,但是共享JavaVM,然后可以通過GetEnv獲取到當前線程的JNIEnv。
jint GetEnv(JavaVM *vm, void **env, jint version);
知識點4:Native方法接收JNI接口指針作為參數。虛擬機保證在同一個線程傳入Native方法的是相同的JNIEnv。如果不同線程調用Native方法,傳入他們的JNIEnv不同。但JNIEnv間接指向的函數表在多個線程間是共享的。
知識點5:為什么在C語言中調用Native方法需要將JNIEnv當作參數傳遞,而C++中卻不需要?
- // C語言
- jstring model_path = (*env)->NewStringUTF(env, path);
- // C++
- jstring model_path = env->NewStringUTF(path);
前面列出的JNIEnv是C語言形式,Java還單獨為C++封裝了一層JNIEnv,簡化版代碼:
- struct _JNIEnv {
- /* do not rename this; it does not seem to be entirely opaque */
- const struct JNINativeInterface* functions;
- #if defined(__cplusplus)
- jint GetVersion()
- { return functions->GetVersion(this); }
- jclass FindClass(const char* name)
- { return functions->FindClass(this, name); }
- #endif
- }
其實本質上還是調用的C語言那種形式的接口。
JNI中數據如何傳遞
這里不詳細介紹了,大體就是int,float這種基本類型采用拷貝,對象和byte數組等使用引用形式,所以其實Java層的byte字節流數據傳到Native層基本不耗時,不會發生拷貝【但是Native層如果想使用持有這塊數據,那就得自己拷貝一份了】。
還有些GlobalReference、LocalReference以及為什么要Delete LocalReference的這類知識點,這些比較基礎,就不介紹了,估計大家也都懂。
推薦閱讀
https://www.cnblogs.com/kexinxin/p/11689641.html
ndk官方文檔
https://developer.android.com/ndk/guides
參考資料
http://luori366.github.io/JNI_doc/jni_design_theory.html
https://www.cnblogs.com/kexinxin/p/11689641.html
https://developer.android.com/ndk/guides