Android架構師之路:JNI與NDK編程-函數注冊與C++調用Java詳解(c++音視頻編碼基礎)
前言小計
1、jni與ndk的基本知識點前面文章都講過了,不懂的,可以在公眾號首頁看;
2、jni中常用的方法比如:類、方法、數組、字符串等前面也講解過了;
3、這篇文章講解jni中函數的注冊和c++調用java的知識點;
一. JNI函數注冊

1、jni函數注解知識點介紹
- JNI技術是Java世界與Native世界的通信橋梁,具體到代碼,Java層的代碼如何同Native層的代碼進行調用的呢?我們都知道,在調用native方法之前,首先要調用System.loadLibrary接口加載一個實現了native方法的動態庫才能正常訪問,否則就會拋出java.lang.UnsatisfiedLinkError異常 。那么,在Java中調用某個native方法時,JVM是通過什么方式,能正確的找到動態庫中C/C++實現的那個native函數呢?
- JVM查找native方法有兩種方式;
- 按照JNI規范的命名規則,調用JNI提供的RegisterNatives函數,將本地函數注冊到JVM中;
- 第一種方式,可用使用javah工具按照Java類中定義的native方法,按照JNI規范的命名規則的方式自動生成Jni本地C/C++頭文件;
- 第二種方式則需要在本地庫的JNI_OnLoad函數中調用RegisterNatives來動態注冊;
- JNI函數注冊是將Java層聲明的Native方法同實際的Native函數綁定起來的實現方式,也就是說,只要通過JNI函數注冊機制注冊了本地方,Java層就可以直接調用定義的這些本地方法了。對應上述JVM查找native方法的兩種方式,JNI函數注冊方式一般分為靜態注冊和動態注冊兩種方式。
2、靜態注冊
原理:根據函數名來建立 java 方法與 JNI 函數的一一對應關系;
實現流程:
- 編寫 java 代碼;
- 利用 javah 指令生成對應的 .h 文件;
- 對 .h 中的聲明進行實現;
弊端:
編寫不方便,JNI 方法名字必須遵循規則且名字很長;
編寫過程步驟多,不方便;
程序運行效率低,因為初次調用native函數時需要根據根據函數名在JNI層中搜索對應的本地函數,然后建立對應關系,這個過程比較耗時;
- public class Test {
- static {
- System.loadLibrary("native-lib");
- }
- public native String textFromJni();
- }
- 使用javah生成對應的本地方法頭文件。
- #include <jni.h>
- #ifndef _Included_test
- #define _Included_test
- #ifdef __cplusplus
- extern "C" {
- #endif
- JNIEXPORT jstring JNICALL Java_test_Test_textFromJni
- (JNIEnv *, jobject);
- #ifdef __cplusplus
- }
- #endif
3、動態注冊
對應與上面的靜態注冊方法,還有一種動態注冊JNI函數的方式,即動態注冊。動態注冊是當Java層調用System.loadLibrary方法加載so庫后,本地庫的JNI_OnLoad函數會被調用,在JNI_OnLoad函數中通過調用RegisterNatives函數來完成本地方法的注冊。
原理:利用 RegisterNatives 方法來注冊 java 方法與 JNI 函數的一一對應關系;
實現流程:
- 利用結構體 JNINativeMethod 數組記錄 java 方法與 JNI 函數的對應關系;
- 實現 JNI_OnLoad 方法,在加載動態庫后,執行動態注冊;
- 調用 FindClass 方法,獲取 java 對象;
- 調用 RegisterNatives 方法,傳入 java 對象,以及 JNINativeMethod 數組,以及注冊數目完成注冊;
優點:
流程更加清晰可控;
效率更高;
其中JNINativeMethod結構體用來描述本地方法結構,其定義如下:
- typedef struct {
- const char* name; // Java方法名
- const char* signature; // Java方法簽名
- void* fnPtr; // jni本地方法對應的函數指針
- } JNINativeMethod;
結構體的第一個參數 name 是java 方法名;
第二個參數 signature 用于描述方法的參數與返回值;
第三個參數 fnPtr 是函數指針,指向 jni 函數;
其中,第二個參數 signature 使用字符串記錄方法的參數與返回值,具體格式形如“()V”、“(II)V”,其中分為兩部分,括號內表示的是參數,括號右側表示的是返回值;
①、數據類型映射
基本數據類型
②. 數組引用類型
如果是一維數組則遵循下表,如果是二維數組或更高維數組則對應的 native 類型為 jobjectArray,域描述符中使用 ‘[’ 的個數表示維數
③. 對象引用類型
對于其它引用類型,即 java 中的對象,其映射規則為
④. 對象數組引用類型
如果是一維數組則遵循下表,如果是二維數組或更高維數組則對應的 native 類型為 jobjectArray,域描述符中使用 ‘[’ 的個數表示維數
在Java文件中定義本地方法,加載本地so庫
- package test.jnitest;
- public class Test {
- static {
- System.loadLibrary("native-lib");
- }
- public native String textFromJni();
- }
在JNI_OnLoad函數中注冊本地方法
- jstring textFromJni(JNIEnv* env, jobject thiz) {
- return env->NewStringUTF("text from jni");
- }
- static JNINativeMethod gMethods[] = {
- {"textFromJni", "()Ljava/lang/String;", (void*)textFromJni}
- };
- int registerMethod(JNIEnv *env) {
- jclass test = env->FindClass("cc/ccbu/jnitest/Test");
- return env->RegisterNatives(test, gMethods, sizeof(gMethods)/ sizeof(gMethods[0]));
- }
- JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
- JNIEnv* env = NULL;
- if (vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) {
- return JNI_ERR;
- }
- if (registerMethod(env) != JNI_OK) {
- return JNI_ERR;
- }
- return JNI_VERSION_1_6;
- }
注意:
在JNI_OnLoad函數的結尾處,我們一定要有返回值,而且必須是JNI_VERSION_1_4 或 JNI_VERSION_1_6,也就是JNI的版本號,我們一定要返回正確的版本號,否則系統也是無法加載的;
4、c++調用java詳解
(1) 找到java對應的Class
(2) 找到要調用的方法的methodID
(3) 在C語言中調用相應方法
①.通過JAVA層的本地方法創建同類對象
步驟:
I.通過對象獲取類
II.通過類獲取類的構造方法的ID
III.基于方法ID和類,創建新對象
- JNIEXPORT void JNICALL JAVA_nativeMethod
- (JNIEnv *env, jobject thiz,jint i){
- ...
- jclass clazz = (*env).GetObjectClass(thiz);
- jmethodID mid = (*env).GetMethodID(clazz,"<init>","()V");
- jobject obj = (*env).NewObject(clazz,mid);
- ...
- return;
- }
②.通過C/C++創建不同類對象
步驟:
I.通過FindClass方法獲取需要的類
II.通過類獲取類的構造方法的ID
III.基于方法ID和類,創建新對象
- JNIEXPORT void JNICALL JAVA_nativeMethod
- (JNIEnv *env, jobject thiz,jint i){
- ...
- jclass clazz = (*env).FindClass("com/x/test/Test");//參數為類路徑
- jmethodID mid = (*env).GetMethodID(clazz,"<init>","()V");
- jobject obj = (*env).NewObject(clazz,mid);
- ...
- return;
- }
③獲取上下文環境JNIEnv
如果找不到上下文JNIEnv就要獲取
- bool AttachCurrentThread(JavaVM* vm, JNIEnv** p_env)
- {
- bool bAttached = false;
- switch(vm->GetEnv((void**)p_env, JNI_VERSION_1_4))
- {
- case JNI_OK:
- break;
- case JNI_EDETACHED:
- if (vm->AttachCurrentThread(p_env, 0) < 0)
- {
- LOGD("%s :test failed!",__func__);
- return false;
- }
- else
- {
- bAttached = true;
- }
- break;
- case JNI_EVERSION:
- LOGE("Invalid java version");
- break;
- }
- return bAttached;
- }
總結
以上總結了JNI中函數注冊的兩種方法,在實際應用中都很常見都用得到的,要理解到位才可以;
下次文章會繼續講解關于JNI的知識點和高級應用
本文轉載自微信公眾號「Android開發編程」,可以通過以下二維碼關注。轉載本文請聯系Android開發編程公眾號。