有了前面幾篇NDK與JNI開發相關基礎做鋪墊,再來通過代碼說明下這方面具體的操作以及一些重要的細節。那么,就繼續NDK與JNI的學習總結。

  JavaVM和JNIEnv

  在jni.h頭文件中定義了兩種重要的數據結構JavaVM和JNIEnv,并且在C和C++中它們的實現是不同的(通過#if defined(__cplusplus)宏定義實現)。本質都是指向封裝了JNI函數列表的指針。

  JavaVM

  是java虛擬機在jni層的表示。在Android中一個JVM只允許有一個JavaVM對象。可以在線程間共享一個JavaVM對象。

  JavaVM聲明

  在jni中針對C語言環境和C++語言環境的JavaVM實現有所不同。

  C版的JavaVM聲明為:

C++代碼
  1. typedef const struct JNIInvokeInterface* JavaVM;  
  2.   
  3. struct JNIInvokeInterface {  
  4.     void*       reserved0;  
  5.     void*       reserved1;  
  6.     void*       reserved2;  
  7.   
  8.     jint        (*DestroyJavaVM)(JavaVM*);  
  9.     jint        (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);  
  10.     jint        (*DetachCurrentThread)(JavaVM*);  
  11.     jint        (*GetEnv)(JavaVM*, void**, jint);  
  12.     jint        (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);  
  13. };  

  C++版的JavaVM聲明為:

Java代碼
  1. typedef _JavaVM JavaVM;  
  2.   
  3. struct _JavaVM {  
  4.     const struct JNIInvokeInterface* functions;  
  5.   
  6. #if defined(__cplusplus)  
  7.     jint DestroyJavaVM()  
  8.     { return functions->DestroyJavaVM(this); }  
  9.     jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)  
  10.     { return functions->AttachCurrentThread(this, p_env, thr_args); }  
  11.     jint DetachCurrentThread()  
  12.     { return functions->DetachCurrentThread(this); }  
  13.     jint GetEnv(void** env, jint version)  
  14.     { return functions->GetEnv(this, env, version); }  
  15.     jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)  
  16.     { return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }  
  17. #endif /*__cplusplus*/  
  18. };  

  JavaVM獲取方式

  (1)jni動態注冊的方式。在加載動態鏈接庫的時候,JVM會調用JNI_OnLoad(JavaVM* vm, void* reserved),并傳入JavaVM指針:

C++代碼
  1. JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {  
  2.   
  3. }  

  (2)在本地代碼中通過調用jint JNI_CreateJavaVM(JavaVM**, JNIEnv**, void*)來創建。

  JNIEnv

  簡單來說,就是JNIEnv提供了所有JNI函數調用的接口。不能在線程間共享同一個JNIEnv變量,僅在創建它的線程有效,如果要在其它線程訪問JVM,需要調用AttachCurrentThread或AttachCurrentThreadAsDaemon將當前線程與JVM綁定。再通過JavaVM對象的GetEnv來獲取JNIEnv。

  JNIEnv聲明

  與JavaVM類似,JNIEnv在C和C++語言中的聲明也有所不同。

  C版的JavaVM聲明為:

C++代碼
  1. typedef const struct JNINativeInterface* JNIEnv;  
  2.   
  3. struct JNINativeInterface {  
  4.         jint        (*GetVersion)(JNIEnv *);  
  5.         ···  
  6. }  

  C++版的JavaVM聲明為:

C++代碼
  1. typedef _JNIEnv JNIEnv;  
  2.   
  3. struct _JNIEnv {  
  4.     /* do not rename this; it does not seem to be entirely opaque */  
  5.     const struct JNINativeInterface* functions;  
  6.   
  7. #if defined(__cplusplus)  
  8.   
  9.     jint GetVersion()  
  10.     { return functions->GetVersion(this); }  
  11.   
  12.     ...  
  13. }  

  jobject、jclass、jmethodID和jfieldID

  jobject:

  是JNI對原始java.lang.Object的映射。可以通過調用NewObject來獲得一個jobject對象。例如:

  env->NewObject(jclass clazz, jmethodID methodID, ...)

  jclass:

  是JNI對原始java.lang.Class的映射。可以通過調用FindClass來獲得jclass對象。例如:

  jclass intArrayClass = env->FindClass("[I");

  jmethodID:

  獲取對應類成員方法的方法id。可以通過調用GetMethodID來獲取。例如:

  jmethodID myMethodId = env->(jclass clazz, const char *name, const char *sig);

  jfieldID:

  獲取對應類成員變量的字段id。可以通過調用GetFieldID來獲得。例如:

  jfieldID nameFieldId = env->GetFieldID(jclass clazz, const char *name, const char *sig)

  本地庫調用

  JNI的加載本地庫中的代碼,步驟簡述如下(同時,也是Android推薦的做法):

  (1)在java類的靜態塊中調用System.loadLibrary來加載動態庫,若動態庫的名字為libcocos2dx.so,那么,調用為:

Java代碼
  1. static {  
  2.     System.loadLibrary("cocos2dx");  
  3. }  

  (2)在本地代碼中實現JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);方法。

  (3)在該JNI_OnLoad方法中,調用env->RegisterNatives(jclass clazz, const JNINativeMethod *methods, jint nMethods)注冊所有本地的實現方法。推薦將方法聲明為靜態的,這樣不會占據設備上的符號表的空間。

  JNI通信

  JNI的通信過程,其實就是原生Java與底層C/C++數據傳遞的過程。這里簡單歸納下,數據傳遞分為以下這幾種:

  • 傳遞基本數據類型(例如:int,float等)

  • 傳遞對象(例如:String,Object,自定義類MyObject等)

  • 傳遞數組(例如:int[], String[]等)

  • 傳遞集合對象(例如:ArrayList,HashMap等)

  而調用方式有可以分為:

  (1)java調用native方法

  (2)native調用java靜態方法,非靜態方法(成員方法),以及獲取java類的成員變量。

  下面按照實現方式的不同結合以上要點,通過一個例子代碼來說明下具體是如何實現的。

  (1)靜態注冊的方式

  工程結構如下:(這里只列舉出主要說明的項)

XML/HTML代碼
  1. JNISample1    
  2.   │── build.gradle  
  3.   │── CMakeLists.txt   
  4.   └── app   
  5.       ├── build.gradle  
  6.       ├── CMakeLists.txt  
  7.       └── src   
  8.           ├── cpp  
  9.           │    ├── JNIUtils.h  
  10.           │    └── JNIUtils.cpp  
  11.           └── com.alphagl.main  
  12.                     ├── JNIUtils.java  
  13.                     ├── MainActivity.Java  
  14.                     └── Person.java  

  代碼如下:(這里做了下簡化,去掉些注釋以及單元測試部分的代碼)

  MainActivity.java:

Java代碼
  1. package com.alphagl.main;  
  2.   
  3. import android.app.Activity;  
  4. import android.os.Bundle;  
  5. import android.util.Log;  
  6.   
  7. public class MainActivity extends Activity {  
  8.   
  9.     static {  
  10.         System.loadLibrary("native-lib");  
  11.     }  
  12.   
  13.     protected void onCreate(Bundle savedInstanceState) {  
  14.         super.onCreate(savedInstanceState);  
  15.         setContentView(R.layout.activity_main);  
  16.   
  17.         Log.i("MainActivity""getStringFromJNI ============= " + JNIUtils.getStringFromJNI());  
  18.         Log.i("MainActivity""getIntArrayFromJNI ============= " + JNIUtils.getIntArrayFromJNI()[0] + "," + JNIUtils.getIntArrayFromJNI()[1]);  
  19.         JNIUtils.setPersonToJNI(new Person(18"jobs"));  
  20.         Log.i("MainActivity""getPersonFromJNI ============= " + JNIUtils.getPersonFromJNI().getAge()+ "," + JNIUtils.getPersonFromJNI().getName());  
  21.     }  
  22. }  

  Person.java:(封裝的自定義對象)

Java代碼
  1. package com.alphagl.main;  
  2.   
  3. import android.util.Log;  
  4.   
  5. public class Person {  
  6.     private int age;  
  7.     private String name;  
  8.   
  9.     public Person(int age, String name) {  
  10.         this.age = age;  
  11.         this.name = name;  
  12.     }  
  13.   
  14.     public void setAge(int age) {  
  15.         this.age = age;  
  16.     }  
  17.   
  18.     public int getAge() {  
  19.         return age;  
  20.     }  
  21.   
  22.     public void setName(String name) {  
  23.         this.name = name;  
  24.     }  
  25.   
  26.     public String getName() {  
  27.         return name;  
  28.     }  
  29.   
  30.     public void printPerson() {  
  31.         Log.d("MainActivity""age ======== " + age + "," + "name ======== " + name);  
  32.     }  
  33. }  

  JNIUtils.java:

Java代碼
  1. package com.alphagl.main;  
  2.   
  3. public class JNIUtils {  
  4.     public static native String getStringFromJNI();  
  5.     public static native int[] getIntArrayFromJNI();  
  6.     public static native void setPersonToJNI(Person person);  
  7.     public static native Person getPersonFromJNI();  
  8. }  

  JNIUtils.h:

C++代碼
  1. #include <jni.h>  
  2. #include <stdio.h>  
  3.   
  4. #ifndef _Included_com_alphagl_main_JNIUtils  
  5. #define _Included_com_alphagl_main_JNIUtils  
  6. #ifdef __cplusplus  
  7. extern "C" {  
  8. #endif  
  9.   
  10. JNIEXPORT jstring JNICALL Java_com_alphagl_main_JNIUtils_getStringFromJNI  
  11.   (JNIEnv *, jclass);  
  12.   
  13.   
  14. JNIEXPORT jintArray JNICALL Java_com_alphagl_main_JNIUtils_getIntArrayFromJNI  
  15.   (JNIEnv *, jclass);  
  16.   
  17.   
  18. JNIEXPORT void JNICALL Java_com_alphagl_main_JNIUtils_setPersonToJNI  
  19.   (JNIEnv *, jclass, jobject);  
  20.   
  21.   
  22. JNIEXPORT jobject JNICALL Java_com_alphagl_main_JNIUtils_getPersonFromJNI  
  23.   (JNIEnv *, jclass);  
  24.   
  25. #ifdef __cplusplus  
  26. }  
  27. #endif  
  28. #endif  

  JNIUtils.cpp

C++代碼
  1. #include "JNIUtils.h"  
  2. #include <android/log.h>  
  3.   
  4. #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "MainActivity", __VA_ARGS__)  
  5. #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "MainActivity", __VA_ARGS__)  
  6. #define LOGE(...) __android_log_print(ANDROID_LOG_ERROE, "MainActivity", __VA_ARGS__)  
  7.   
  8.   
  9. JNIEXPORT jstring JNICALL Java_com_alphagl_main_JNIUtils_getStringFromJNI (JNIEnv *env, jclass jcls) {  
  10.     LOGD(" ====================== getStringFromJNI");  
  11.     // 構造一個String字符串  
  12.     return env->NewStringUTF("Hello from jni");  
  13. }  
  14.   
  15.   
  16. JNIEXPORT jintArray JNICALL Java_com_alphagl_main_JNIUtils_getIntArrayFromJNI (JNIEnv *env, jclass jcls) {  
  17.     LOGD(" ====================== getIntArrayFromJNI");  
  18.     // 構造一個int[]數組  
  19.     jintArray intArray = env->NewIntArray(2);  
  20.     int size[]={640, 960};  
  21.     // 給int[]數組賦值  
  22.     env->SetIntArrayRegion(intArray, 0, 2, size);  
  23.   
  24.     return intArray;  
  25. }  
  26.   
  27.   
  28. JNIEXPORT void JNICALL Java_com_alphagl_main_JNIUtils_setPersonToJNI (JNIEnv *env, jclass jcls, jobject jobj) {  
  29.     LOGD(" ====================== setPersonToJNI");  
  30.     jclass jperson = env->GetObjectClass(jobj);  
  31.     if (jperson != NULL) {  
  32.         // 獲取Person對象的age字段id  
  33.         jfieldID ageFieldId = env->GetFieldID(jperson, "age""I");  
  34.         // 獲取Person對象的name字段id  
  35.         jfieldID nameFieldId = env->GetFieldID(jperson, "name""Ljava/lang/String;");  
  36.   
  37.         // 獲取Person的age成員變量  
  38.         jint age = env->GetIntField(jobj, ageFieldId);  
  39.         // 獲取Person的name成員變量  
  40.         jstring name = (jstring)env->GetObjectField(jobj, nameFieldId);  
  41.   
  42.         const char *c_name = env->GetStringUTFChars(name, NULL);  
  43.   
  44.         // 打印從Java傳遞過來的Person對象的age和name變量  
  45.         LOGD("age ===== %d, name ===== %s", age, c_name);  
  46.     }  
  47.   
  48.     // 以下是從JNI構造Java對象,并調用Java類中的成員方法,僅用作演示  
  49.     // 獲取Person對象的class  
  50.     jclass jstu = env->FindClass("com/alphagl/main/Person");  
  51.     // 獲取Person對象的構造方法的方法id  
  52.     jmethodID personMethodId = env->GetMethodID(jperson, "<init>""(ILjava/lang/String;)V");  
  53.     // 構造一個String字符串  
  54.     jstring name = env->NewStringUTF("bill");  
  55.   
  56.     // 構造一個Person對象  
  57.     jobject  jPersonObj = env->NewObject(jstu, personMethodId, 30, name);  
  58.     // 獲取Person對象的printPerson成員方法的方法id  
  59.     jmethodID jid = env->GetMethodID(jstu, "printPerson""()V");  
  60.     // 調用java的printPerson方法  
  61.     env->CallVoidMethod(jPersonObj, jid);  
  62. }  
  63.   
  64.   
  65. JNIEXPORT jobject JNICALL Java_com_alphagl_main_JNIUtils_getPersonFromJNI(JNIEnv *env, jclass jcls) {  
  66.     LOGD(" ====================== getPersonFromJNI");  
  67.     // 獲取Person對象的class  
  68.     jclass jstudent = env->FindClass("com/alphagl/main/Person");  
  69.     // 獲取Person對象的構造方法的方法id  
  70.     jmethodID studentMethodId = env->GetMethodID(jstudent, "<init>""(ILjava/lang/String;)V");  
  71.     // 構造一個String字符串  
  72.     jstring name = env->NewStringUTF("john");  
  73.     // 構造一個Person對象  
  74.     jobject  jstudentObj = env->NewObject(jstudent, studentMethodId, 20, name);  
  75.   
  76.     return jstudentObj;  
  77. }  

  這里再提一下,如上`JNIUtils.java`類中定義好了native方法,如何根據對象的方法簽名生成對應的C/C++方法的聲明。這部分內容在Android游戲開發實踐(1)之NDK與JNI開發01 已經提到過,我們可以借助javah來根據編譯后的.class生成對于的頭文件。

  普通做法是:

Android游戲開發實踐之NDK與JNI開發04

  在AndroidStudio中可以:

  Tools-> External Tools -> 添加

Android游戲開發實踐之NDK與JNI開發04

  (1)javah所在的路徑

  (2)命令行參數

  (3)頭文件生成的路徑

Android游戲開發實踐之NDK與JNI開發04

  在聲明了native方法的類,右鍵執行javah即可。

  (2)動態注冊的方式

  工程結構如下:(這里只列舉出主要說明的項)

XML/HTML代碼
  1. JNISample2    
  2.   │── build.gradle  
  3.   │── CMakeLists.txt   
  4.   └── app   
  5.       ├── build.gradle  
  6.       ├── CMakeLists.txt  
  7.       └── src   
  8.           ├── cpp  
  9.           │   └── JNIUtils.cpp  
  10.           │      
  11.           └── com.alphagl.main  
  12.                     ├── JNIUtils.java  
  13.                     ├── MainActivity.Java  
  14.                     └── Person.java  

  這里主要看下不同的代碼部分,即JNIUtils.cpp。

  JNIUtils.cpp:

C++代碼
  1. #include <jni.h>  
  2. #include <string>  
  3. #include <android/log.h>  
  4.   
  5. #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "MainActivity", __VA_ARGS__)  
  6. #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "MainActivity", __VA_ARGS__)  
  7. #define LOGE(...) __android_log_print(ANDROID_LOG_ERROE, "MainActivity", __VA_ARGS__)  
  8.   
  9. #define CLASSNAME "com/alphagl/main/JNIUtils"  
  10.   
  11. static jstring getStringFromJNI_native(JNIEnv *env, jclass jcls) {  
  12.     LOGD(" ====================== getStringFromJNI");  
  13.     // 構造一個String字符串  
  14.     return env->NewStringUTF("Hello from jni");  
  15. }  
  16.   
  17. static jarray getIntArrayFromJNI_native(JNIEnv *env, jclass jcls) {  
  18.     LOGD(" ====================== getIntArrayFromJNI");  
  19.     // 構造一個int[]數組  
  20.     jintArray intArray = env->NewIntArray(2);  
  21.     int size[]={640, 960};  
  22.     // 給int[]數組賦值  
  23.     env->SetIntArrayRegion(intArray, 0, 2, size);  
  24.   
  25.     return intArray;  
  26. }  
  27.   
  28. static void setJniPerson_native(JNIEnv *env, jclass jcls, jobject jobj) {  
  29.     LOGD(" ====================== setPersonToJNI");  
  30.     jclass jperson = env->GetObjectClass(jobj);  
  31.     if (jperson != NULL) {  
  32.         // 獲取Person對象的age字段id  
  33.         jfieldID ageFieldId = env->GetFieldID(jperson, "age""I");  
  34.         // 獲取Person對象的name字段id  
  35.         jfieldID nameFieldId = env->GetFieldID(jperson, "name""Ljava/lang/String;");  
  36.   
  37.         // 獲取Person的age成員變量  
  38.         jint age = env->GetIntField(jobj, ageFieldId);  
  39.         // 獲取Person的name成員變量  
  40.         jstring name = (jstring)env->GetObjectField(jobj, nameFieldId);  
  41.   
  42.         const char *c_name = env->GetStringUTFChars(name, NULL);  
  43.   
  44.         // 打印從Java傳遞過來的Person對象的age和name變量  
  45.         LOGD("age ===== %d, name ===== %s", age, c_name);  
  46.     }  
  47.   
  48.     // 以下是從JNI構造Java對象,并調用Java類中的成員方法,僅用作演示  
  49.     // 獲取Person對象的class  
  50.     jclass jstu = env->FindClass("com/alphagl/main/Person");  
  51.     // 獲取Person對象的構造方法的方法id  
  52.     jmethodID personMethodId = env->GetMethodID(jperson, "<init>""(ILjava/lang/String;)V");  
  53.     // 構造一個String字符串  
  54.     jstring name = env->NewStringUTF("bill");  
  55.   
  56.     // 構造一個Person對象  
  57.     jobject  jPersonObj = env->NewObject(jstu, personMethodId, 30, name);  
  58.     // 獲取Person對象的printPerson成員方法的方法id  
  59.     jmethodID jid = env->GetMethodID(jstu, "printPerson""()V");  
  60.     // 調用java的printPerson方法  
  61.     env->CallVoidMethod(jPersonObj, jid);  
  62. }  
  63.   
  64. static jobject getJniPerson_native(JNIEnv *env, jclass jcls) {  
  65.     LOGD(" ====================== getPersonFromJNI");  
  66.     // 獲取Person對象的class  
  67.     jclass jstudent = env->FindClass("com/alphagl/main/Person");  
  68.     // 獲取Person對象的構造方法的方法id  
  69.     jmethodID studentMethodId = env->GetMethodID(jstudent, "<init>""(ILjava/lang/String;)V");  
  70.     // 構造一個String字符串  
  71.     jstring name = env->NewStringUTF("john");  
  72.     // 構造一個Person對象  
  73.     jobject  jstudentObj = env->NewObject(jstudent, studentMethodId, 20, name);  
  74.   
  75.     return jstudentObj;  
  76. }  
  77.   
  78. static JNINativeMethod gMethods[] = {  
  79.         {"getStringFromJNI""()Ljava/lang/String;", (void*)getStringFromJNI_native},  
  80.         {"getIntArrayFromJNI""()[I", (void*)getIntArrayFromJNI_native},  
  81.         {"setPersonToJNI""(Lcom/alphagl/main/Person;)V", (void*)setJniPerson_native},  
  82.         {"getPersonFromJNI""()Lcom/alphagl/main/Person;", (void*)getJniPerson_native}  
  83. };  
  84.   
  85. static jint registerNativeMethods(JNIEnv *env, const char* className, JNINativeMethod *gMethods, int numMethods) {  
  86.     jclass jcls;  
  87.     jcls = env->FindClass(className);  
  88.     if (jcls == NULL) {  
  89.         return JNI_FALSE;  
  90.     }  
  91.   
  92.     if (env->RegisterNatives(jcls, gMethods, numMethods) < 0) {  
  93.         return JNI_FALSE;  
  94.     }  
  95.   
  96.     return JNI_TRUE;  
  97. }  
  98.   
  99. static jint registerNative(JNIEnv *env) {  
  100.     return registerNativeMethods(env, CLASSNAME, gMethods, sizeof(gMethods) / sizeof(gMethods[0]));  
  101. }  
  102.   
  103. JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {  
  104.     JNIEnv *env = NULL;  
  105.     if ((vm->GetEnv((void**)&env, JNI_VERSION_1_6)) != JNI_OK) {  
  106.         return JNI_ERR;  
  107.     }  
  108.   
  109.     if (!registerNative(env)) {  
  110.         return JNI_ERR;  
  111.     }  
  112.   
  113.     return JNI_VERSION_1_6;  
  114. }  

  最后的執行結果為:

Android游戲開發實踐之NDK與JNI開發04

  兩種實現方式比較:

  (1)動態注冊中,可以不用聲明形如Java_packageName_className_methodName格式的方法。

  (2)動態注冊中,要重寫JNI_OnLoad方法,手動調用RegisterNatives來注冊本地方法,以及聲明在JNINativeMethod中。

  (3)動態注冊,明顯這種方式更靈活,但對代碼要求更高,推薦使用這種方式。

  以上示例代碼都已上傳Github,有需要的可以自行查看。

  https://github.com/cnsuperx/android-jni-example

  JNI調試

  如果安裝了LLVM環境的話,直接將Jni Debuggable選項打開即可。環境搭建可以參考Android游戲開發實踐(1)之NDK與JNI開發03。

Android游戲開發實踐之NDK與JNI開發04

  接著直接在C或C++代碼中設置斷點即可。

本文發布:Android開發網
本文地址:http://www.ojizl5.fun/android/game/713.html
2017年9月13日
發布:雞啄米 分類:Android游戲開發 瀏覽: 評論:0