JAVA調用C/C++在Java語言里面本來就有的,并非Android獨有的,即JNI。JNI就是Java調用C++的規范。
JNI 概述
JNI,全稱為Java Native Interface,即Java本地接口,JNI是Java調用Native語言的一種特性,通過JNI可以使JAVA和 C/C++進行交互。
Java語言是跨平臺的語言,而這跨平臺的背后都是依靠Java虛擬機,虛擬機采用C/C++編寫,適配各個系統,通過JNI為上層Java提供各種服務,保證跨平臺性。
在Java語言出現前,就有很多程序和庫都是由Native語言寫的,如果想重復利用這些庫,就可以所使用JNI來實現。在Android平臺上,JNI就是一座將Java世界和Native世界聯通的一座橋梁。

jni.png
通過JNI,Java世界和Native世界的代碼就可以相互訪問了。JNI實例:Camera
最新有在看系統的Camera相關,所以從系統Camera角度來分析下JNI的應用,下面講的實例基于Camera2
Android5.0(21)之后android.hardware.Camera就被廢棄了,取而代之的是全新的android.hardware.Camera2
相關代碼:
frameworks/base/core/jni/AndroidRuntime.cpp frameworks/base/core/java/android/hardware/camera2/impl/CameraMetadataNative.java frameworks/base/core/jni/android_hardware_camera2_CameraMetadata.cpp
Camera2 Java層對應的是CameraMetadataNative.java,Native層對應的是android_hardware_camera2_CameraMetadata.cpp
Java層CameraMetadataNative
相關代碼在CameraMetadataNative.java
Camera2使用CameraManager(攝像頭管理器)進行控制,CameraManager具體的操作會通過CameraMetadataNative來執行。
CameraMetadataNative的初始化
public class CameraMetadataNative implements Parcelable static { /* * We use a class initializer to allow the native code to cache some field offsets */ nativeClassInit(); registerAllMarshalers(); } private static native void nativeClassInit(); }
靜態方法初始化調用了Native層的方法nativeClassInit,這個方法對應的Native層具體實現,是在android_hardware_camera2_CameraMetadata.cpp
Native層CameraMetadata
Native層相關代碼在android_hardware_camera2_CameraMetadata.cpp
Native方法初始化
static const JNINativeMethod gCameraMetadataMethods[] = { // static methods { "nativeClassInit", "()V", (void *)CameraMetadata_classInit }, //和Java層nativeClassInit()對應 { "nativeGetAllVendorKeys", "(Ljava/lang/Class;)Ljava/util/ArrayList;", (void *)CameraMetadata_getAllVendorKeys}, { "nativeGetTagFromKey", "(Ljava/lang/String;)I", (void *)CameraMetadata_getTagFromKey }, { "nativeGetTypeFromTag", "(I)I", (void *)CameraMetadata_getTypeFromTag }, { "nativeSetupGlobalVendorTagDescriptor", "()I", (void*)CameraMetadata_setupGlobalVendorTagDescriptor }, // instance methods { "nativeAllocate", "()J", (void*)CameraMetadata_allocate }, { "nativeAllocateCopy", "(L" CAMERA_METADATA_CLASS_NAME ";)J", (void *)CameraMetadata_allocateCopy }, { "nativeIsEmpty", "()Z", (void*)CameraMetadata_isEmpty }, { "nativeGetEntryCount", "()I", (void*)CameraMetadata_getEntryCount }, { "nativeClose", "()V", (void*)CameraMetadata_close }, { "nativeSwap", "(L" CAMERA_METADATA_CLASS_NAME ";)V", (void *)CameraMetadata_swap }, { "nativeReadValues", "(I)[B", (void *)CameraMetadata_readValues }, { "nativeWriteValues", "(I[B)V", (void *)CameraMetadata_writeValues }, { "nativeDump", "()V", (void *)CameraMetadata_dump }, // Parcelable interface { "nativeReadFromParcel", "(Landroid/os/Parcel;)V", (void *)CameraMetadata_readFromParcel }, { "nativeWriteToParcel", "(Landroid/os/Parcel;)V", (void *)CameraMetadata_writeToParcel }, };
gCameraMetadataMethods什么時候會被加載?
int register_android_hardware_camera2_CameraMetadata(JNIEnv *env) { ...... // Register native functions return RegisterMethodsOrDie(env, CAMERA_METADATA_CLASS_NAME, gCameraMetadataMethods, NELEM(gCameraMetadataMethods)); } ...... static inline int RegisterMethodsOrDie(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) { int res = AndroidRuntime::registerNativeMethods(env, className, gMethods, numMethods); LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods."); return res; }
register_android_hardware_camera2_CameraMetadata何時會被調用到,這個就需要了解下JNI的查找方式。
JNI查找方式
Android系統在啟動啟動過程中,先啟動Kernel創建init進程,緊接著由init進程fork第一個橫穿Java和C/C++的進程,即Zygote進程。Zygote啟動過程中會AndroidRuntime.cpp中的startVm創建虛擬機,VM創建完成后,緊接著調用startReg完成虛擬機中的JNI方法注冊。
剛才CameraMetadata中register_android_hardware_camera2_CameraMetadata方法,在AndroidRuntime.cpp的聲明:
extern int register_android_hardware_camera2_CameraMetadata(JNIEnv *env);
然后在gRegJNI中的靜態聲明
static const RegJNIRec gRegJNI[] = { ...... REG_JNI(register_android_hardware_camera2_CameraMetadata), ...... }
gRegJNI方法在startReg中被調用
/*static*/ int AndroidRuntime::startReg(JNIEnv* env) { ATRACE_NAME("RegisterAndroidNatives"); androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc); ALOGV("--- registering native functions ---n"); env->PushLocalFrame(200); if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) { env->PopLocalFrame(NULL); return -1; } env->PopLocalFrame(NULL); //createJavaThread("fubar", quickTest, (void*) "hello"); 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) { #ifndef NDEBUG ALOGD("----------!!! %s failed to loadn", array[i].mName); #endif return -1; } } return 0; }
這樣android_hardware_camera2_CameraMetadata.cpp中的int register_android_hardware_camera2_CameraMetadata(JNIEnv *env)就會被調用到。
除了這種Android系統啟動時,就注冊JNI所對應的方法。還有一種就是程序自定義的JNI方法,以 MediePlay 為例:
相關代碼路徑
frameworks/base/media/java/android/media/MediaPlayer.java frameworks/base/media/jni/android_media_MediaPlayer.cpp
MediaPlayer聲明:
public class MediaPlayer extends PlayerBase implements SubtitleController.Listener { ...... private static native final void native_init(); ...... static { System.loadLibrary("media_jni"); native_init(); } }
靜態代碼塊中使用System.loadLibrary加載動態庫,media_jni在Android平臺對應的是libmedia_jni.so庫。
在jni目錄/frameworks/base/media/jni/Android.mk中有相應的聲明:
LOCAL_SRC_FILES:= android_media_MediaPlayer.cpp ... LOCAL_MODULE:= libmedia_jni
在android_media_MediaPlayer.cpp找到對應的Native(natvie_init)方法:
static void android_media_MediaPlayer_native_init(JNIEnv *env) { jclass clazz; clazz = env->FindClass("android/media/MediaPlayer"); if (clazz == NULL) { return; } ...... }
JNI注冊的方法就是上面描述的兩種方法:
- 在Android系統啟動時注冊,在AndroidRuntime.cpp中的gRegJNI方法中聲明
- 使用System.loadLibrary()方式注冊
JNI基礎
上面一節主要描述了系統中Java層和Native層交互和實現,并沒有對JNI的基礎理論,流程進行分析
JNI命名規則
JNI方法名規范 :
返回值 + Java前綴 + 全路徑類名 + 方法名 + 參數① JNIEnv + 參數② jobject + 其它參數
簡單的一個例子,返回一個字符串
extern "C" JNIEXPORT jstring JNICALL Java_com_yeungeek_jnisample_NativeHelper_stringFromJNI(JNIEnv *env, jclass jclass1) { LOGD("##### from c"); return env->NewStringUTF("Hello JNI"); }
- 返回值:jstring
- 全路徑類名:com_yeungeek_jnisample_NativeHelper
- 方法名:stringFromJNI
JNI開發流程
- 在Java中先聲明一個native方法
- 編譯Java源文件javac得到.class文件
- 通過javah -jni命令導出JNI的.h頭文件
- 使用Java需要交互的本地代碼,實現在Java中聲明的Native方法(如果Java需要與C++交互,那么就用C++實現Java的Native方法。)
- 將本地代碼編譯成動態庫(windows系統下是.dll文件,如果是linux系統下是.so文件,如果是mac系統下是.jnilib)
- 通過Java命令執行Java程序,最終實現Java調用本地代碼。
數據類型
基本數據類型

引用數據類型

方法簽名
JNI的方法簽名的格式:
(參數簽名格式...)返回值簽名格式
demo的native 方法:
public static native java.lang.String stringFromJNI();
可以通過javap命令生成方法簽名``:
()Ljava/lang/String;
JNI原理
Java語言的執行環境是Java虛擬機(JVM),JVM其實是主機環境中的一個進程,每個JVM虛擬機都在本地環境中有一個JavaVM結構體,該結構體在創建Java虛擬機時被返回,在JNI環境中創建JVM的函數為JNI_CreateJavaVM。
JNI 定義了兩個關鍵數據結構,即“JavaVM”和“JNIEnv”,兩者本質上都是指向函數表的二級指針。
JavaVM
JavaVM是Java虛擬機在JNI層的代表,JavaVM 提供了“調用接口”函數,您可以利用此類函數創建和銷毀 JavaVM。理論上,每個進程可以包含多個JavaVM,但AnAndroid只允許每個進程包含一個JavaVM。
JNIEnv
JNIEnv是一個線程相關的結構體,該結構體代表了Java在本線程的執行環境。JNIEnv 提供了大多數 JNI 函數。您的原生函數均會接收 JNIEnv 作為第一個參數。
JNIEnv作用:
- 調用Java函數
- 操作Java代碼
JNIEnv定義(jni.h):
libnativehelper/include/nativehelper/jni.h
#if defined(__cplusplus) typedef _JNIEnv JNIEnv; typedef _JavaVM JavaVM; #else typedef const struct JNINativeInterface* JNIEnv; typedef const struct JNIInvokeInterface* JavaVM; #endif
定義中可以看到JavaVM,Android中一個進程只會有一個JavaVM,一個JVM對應一個JavaVM結構,而一個JVM中可能創建多個Java線程,每個線程對應一個JNIEnv結構

javavm.png
注冊JNI函數
Java世界和Native世界的方法是如何關聯的,就是通過JNI函數注冊來實現。JNI函數注冊有兩種方式:
靜態注冊
這種方法就是通過函數名來找對應的JNI函數,可以通過javah命令行來生成JNI頭文件
javah com.yeungeek.jnisample.NativeHelper
生成對應的com_yeungeek_jnisample_NativeHelper.h文件,生成對應的JNI函數,然后在實現這個函數就可以了
/* * Class: com_yeungeek_jnisample_NativeHelper * Method: stringFromJNI * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_yeungeek_jnisample_NativeHelper_stringFromJNI (JNIEnv *, jclass);
靜態注冊方法中,Native是如何找到對應的JNI函數,在JNI查找方式中介紹系統的流程,并沒有詳細說明靜態注冊的查找。這里簡單說明下這個過程(以上面的聲明為例子s):
當Java調用native stringFromJNI函數時,會從對應JNI庫中查找Java_com_yeungeek_jnisample_NativeHelper_stringFromJNI函數,如果沒有找到,就會報錯。
靜態注冊方法,就是根據函數名來關聯Java函數和JNI函數,JNI函數需要遵循特定的格式,這其中就有一些缺點:
- 聲明了native方法的Java類,需要通過javah來生成頭文件
- JNI函數名稱非常長
- 第一次調用native函數,需要通過函數名來搜索關聯對應的JNI函數,效率比較低
如何解決這些問題,讓native函數,提前知道JNI函數,就可以解決這個問題,這個過程就是動態注冊。
動態注冊
動態注冊在前面的Camera例子中,已經有涉及到,JNI函數classInit的聲明。
static const JNINativeMethod gCameraMetadataMethods[] = { // static methods { "nativeClassInit", "()V", (void *)CameraMetadata_classInit }, //和Java層nativeClassInit()對應 ...... }
JNI中有一種結構用來記錄Java的Native方法和JNI方法的關聯關系,它就是JNINativeMethod,它在jni.h中被定義:
typedef struct { const char* name; //Java層native函數名 const char* signature; //Java函數簽名,記錄參數類型和個數,以及返回值類型 void* fnPtr; //Native層對應的函數指針 } JNINativeMethod;
在JNI查找方式說到,JNI注冊的兩種時間,第一種已經介紹過了,我們自定義的native函數,基本都是會使用System.loadLibrary(“xxx”),來進行JNI函數的關聯。
loadLibrary(Android7.0)
public static void loadLibrary(String libname) { Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname); }
調用到Runtime(libcore/ojluni/src/main/java/java/lang/Runtime.java)的loadLibrary0方法:
synchronized void loadLibrary0(ClassLoader loader, String libname) { ...... String libraryName = libname; if (loader != null) { String filename = loader.findLibrary(libraryName); if (filename == null) { // It's not necessarily true that the ClassLoader used // System.mapLibraryName, but the default setup does, and it's // misleading to say we didn't find "libMyLibrary.so" when we // actually searched for "liblibMyLibrary.so.so". throw new UnsatisfiedLinkError(loader + " couldn't find "" + System.mapLibraryName(libraryName) + """); } //doLoad String error = doLoad(filename, loader); if (error != null) { throw new UnsatisfiedLinkError(error); } return; } //loader 為 null ...... for (String directory : getLibPaths()) { String candidate = directory + filename; candidates.add(candidate); if (IoUtils.canOpenReadOnly(candidate)) { String error = doLoad(candidate, loader); if (error == null) { return; // We successfully loaded the library. Job done. } lastError = error; } } ...... }
doLoad
private String doLoad(String name, ClassLoader loader) { //調用 native 方法 synchronized (this) { return nativeLoad(name, loader, librarySearchPath); } }
nativeLoad
進入到虛擬機代碼/libcore/ojluni/src/main/native/Runtime.c
JNIEXPORT jstring JNICALL Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename, jobject javaLoader, jstring javaLibrarySearchPath) { return JVM_NativeLoad(env, javaFilename, javaLoader, javaLibrarySearchPath); }
然后調用JVM_NativeLoad,JVM_NativeLoad方法申明在jvm.h中,實現在OpenjdkJvm.cc(/art/runtime/openjdkjvm/OpenjdkJvm.cc)
JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env, jstring javaFilename, jobject javaLoader, jstring javaLibrarySearchPath) { ScopedUtfChars filename(env, javaFilename); if (filename.c_str() == NULL) { return NULL; } std::string error_msg; { art::JavaVMExt* vm = art::Runtime::Current()->GetJavaVM(); bool success = vm->LoadNativeLibrary(env, filename.c_str(), javaLoader, javaLibrarySearchPath, &error_msg); if (success) { return nullptr; } } // Don't let a pending exception from JNI_OnLoad cause a CheckJNI issue with NewStringUTF. env->ExceptionClear(); return env->NewStringUTF(error_msg.c_str()); }
LoadNativeLibrary
調用JavaVMExt的LoadNativeLibrary方法,方法在(art/runtime/java_vm_ext.cc)中,這個方法代碼非常多,選取主要的部分進行分析
bool JavaVMExt::LoadNativeLibrary(JNIEnv* env, const std::string& path, jobject class_loader, jstring library_path, std::string* error_msg) { ...... bool was_successful = false; //加載so庫中查找JNI_OnLoad方法,如果沒有系統就認為是靜態注冊方式進行的,直接返回true,代表so庫加載成功, //如果找到JNI_OnLoad就會調用JNI_OnLoad方法,JNI_OnLoad方法中一般存放的是方法注冊的函數, //所以如果采用動態注冊就必須要實現JNI_OnLoad方法,否則調用java中申明的native方法時會拋出異常 void* sym = library->FindSymbol("JNI_OnLoad", nullptr); if (sym == nullptr) { VLOG(jni) << "[No JNI_OnLoad found in "" << path << ""]"; was_successful = true; } else { // Call JNI_OnLoad. We have to override the current class // loader, which will always be "null" since the stuff at the // top of the stack is around Runtime.loadLibrary(). (See // the comments in the JNI FindClass function.) ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride())); self->SetClassLoaderOverride(class_loader); VLOG(jni) << "[Calling JNI_OnLoad in "" << path << ""]"; typedef int (*JNI_OnLoadFn)(JavaVM*, void*); JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym); //調用JNI_OnLoad方法 int version = (*jni_on_load)(this, nullptr); if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) { // Make sure that sigchain owns SIGSEGV. EnsureFrontOfChain(SIGSEGV); } self->SetClassLoaderOverride(old_class_loader.get()); } ...... }
代碼里的主要邏輯:
- 加載so庫中查找JNI_OnLoad方法,如果沒有系統就認為是靜態注冊方式進行的,直接返回true,代表so庫加載成功
- 如果找到JNI_OnLoad就會調用JNI_OnLoad方法,JNI_OnLoad方法中一般存放的是方法注冊的函數
- 所以如果采用動態注冊就必須要實現JNI_OnLoad方法,否則調用Java中的native方法時會拋出異常
jclass、jmethodID和jfieldID
如果要通過原生代碼訪問對象的字段,需要執行以下操作:
- 使用 FindClass 獲取類的類對象引用
- 使用 GetFieldID 獲取字段的字段 ID
- 使用適當內容獲取字段的內容,例如 GetIntField
具體的使用,放在第二篇文章中講解
JNI的引用
JNI規范中定義了三種引用:
- 局部引用(Local Reference)
- 全局引用(Global Reference)
- 弱全局引用(Weak Global Reference)
局部引用
也叫本地引用,在 JNI層函數使用的非全局引用對象都是Local Reference,最大的特點就是,JNI 函數返回后,這些聲明的引用可能就會被垃圾回收
全局引用
這種聲明的對象,不會主動釋放資源,不會被垃圾回收
弱全局引用
一種特殊的全局引用,在運行過程中可能被回收,使用之前需要判斷下是否為空
參考
- Android:清晰講解JNI 與 NDK(含實例教學)
- Android JNI學習
- Android JNI原理分析
- Android深入理解JNI(一)JNI原理與靜態、動態注冊
- JNI Tips