Android Game Development Practice (1) NDK and JNI Development 04
With the previous several articles NDK and JNI development-related foundation to pave the way, and then through the code to explain the specific operation of this area and some important details. Then, continue to learn NDK and JNI summary.
Author: AlphaGL. Copyright is reserved. The original link is welcome to be reproduced.
Portal:
Android Game Development Practice (1) NDK and JNI Development 01
Android Game Development Practice (1) NDK and JNI Development 02
Android Game Development Practice (1) NDK and JNI Development 03
Java VM and JNIEnv
Two important data structures JavaVM and JNIEnv are defined in the jni.h header file, and their implementations are different in C and C++ (through if defined (cplusplus) macro definitions). Essentially, they are pointers that encapsulate the list of JNI functions.
JavaVM
Is the representation of the java virtual machine in the jni layer. In Android, a JVM allows only one java VM object. You can share a java VM object between threads.
JavaVM statement
Java VM implementations for C language environment and C++ language environment are different in jni.
The C version of Java VM is declared as:
typedef const struct JNIInvokeInterface* 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*); };
The C++ version of Java VM is declared as:
typedef _JavaVM JavaVM; struct _JavaVM { const struct JNIInvokeInterface* functions; #if defined(__cplusplus) jint DestroyJavaVM() { return functions->DestroyJavaVM(this); } jint AttachCurrentThread(JNIEnv** p_env, void* thr_args) { return functions->AttachCurrentThread(this, p_env, thr_args); } jint DetachCurrentThread() { return functions->DetachCurrentThread(this); } jint GetEnv(void** env, jint version) { return functions->GetEnv(this, env, version); } jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args) { return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); } #endif /*__cplusplus*/ };
Java VM acquisition
(1)jni dynamic registration mode. When loading dynamic link libraries, the JVM calls JNI_OnLoad(JavaVM* vm, void* reserved) and passes in the JavaVM pointer:
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
}
(2) Create in local code by calling jint JNI_Create Java VM (Java VM**, JNIEnv**, void*).
JNIEnv
Simply put, JNIEnv provides an interface for all JNI function calls. You cannot share the same JNIEnv variable between threads, only the thread that created it is valid. If you want to access the JVM in other threads, you need to call AttachCurrentThread or AttachCurrentThreadAsDaemon to bind the current thread to the JVM. Then get JNIEnv through GetEnv of JavaVM object.
JNIEnv statement
Similar to Java VM, JNIEnv has different declarations in C and C++ languages.
The C version of Java VM is declared as:
typedef const struct JNINativeInterface* JNIEnv; struct JNINativeInterface { jint (*GetVersion)(JNIEnv *); ··· }
The C++ version of Java VM is declared as:
typedef _JNIEnv 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); } ... }
jobject, jclass, jmethod ID and jfield ID
jobject:
JNI is a mapping of the original java.lang.Object. You can get a job object by calling NewObject. For example:
env->NewObject(jclass clazz, jmethodID methodID, ...)
jclass:
JNI is a mapping of the original java.lang.Class. The jclass object can be obtained by calling FindClass. For example:
jclass intArrayClass = env->FindClass("[I");
jmethodID:
Gets the method id of the corresponding class member method. You can get it by calling GetMethodID. For example:
jmethodID myMethodId = env->(jclass clazz, const char *name, const char *sig);
jfieldID:
Gets the field id of the corresponding class member variable. You can get it by calling GetField id. For example:
jfieldID nameFieldId = env->GetFieldID(jclass clazz, const char *name, const char *sig)
Local library calls
JNI loads the code in the local library. The steps are outlined below (and recommended by Android):
(1) Call System.load Library to load dynamic library in static block of java class. If the name of dynamic library is libcocos2dx.so, then the call is:
static { System.loadLibrary("cocos2dx"); }
(2) Implement JNIEXPORT jint JNICALL JNI_OnLoad (Java VM* vm, void* reserved) in local code; method.
(3) In this JNI_OnLoad method, all local implementation methods are registered by calling env-> Register Natives (jclass clazz, const JNI Native Method * methods, jint Methods). It is recommended that methods be declared static so that they do not occupy the space of the symbol table on the device.
JNI communication
The communication process of JNI is actually the process of data transfer between native Java and underlying C/C++. In brief, data transmission can be divided into the following categories:
- Passing basic data types (such as int, float, etc.)
- Delivery objects (for example: String, Object, custom class MyObject, etc.)
- Pass-through arrays (for example: int [], String [], etc.)
- Pass collection objects (e.g. ArrayList, HashMap, etc.)
The invocation methods can be divided into:
(1)java calls native methods
(2)native calls java static methods, non-static methods (member methods), and obtains member variables of java classes.
Below according to the different ways of implementation combined with the above points, through an example code to illustrate how to achieve the specific below.
(1) The way of static registration
The structure of the project is as follows: (Here only the main items are listed)
JNISample1 │── build.gradle │── CMakeLists.txt └── app ├── build.gradle ├── CMakeLists.txt └── src ├── cpp │ ├── JNIUtils.h │ └── JNIUtils.cpp └── com.alphagl.main ├── JNIUtils.java ├── MainActivity.Java └── Person.java
The code is as follows: (Here we simplify, remove some comments and the unit test part of the code)
MainActivity.java:
package com.alphagl.main; import android.app.Activity; import android.os.Bundle; import android.util.Log; public class MainActivity extends Activity { static { System.loadLibrary("native-lib"); } protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.i("MainActivity", "getStringFromJNI ============= " + JNIUtils.getStringFromJNI()); Log.i("MainActivity", "getIntArrayFromJNI ============= " + JNIUtils.getIntArrayFromJNI()[0] + "," + JNIUtils.getIntArrayFromJNI()[1]); JNIUtils.setPersonToJNI(new Person(18, "jobs")); Log.i("MainActivity", "getPersonFromJNI ============= " + JNIUtils.getPersonFromJNI().getAge()+ "," + JNIUtils.getPersonFromJNI().getName()); } }
Person.java:(Encapsulated custom objects)
package com.alphagl.main; import android.util.Log; public class Person { private int age; private String name; public Person(int age, String name) { this.age = age; this.name = name; } public void setAge(int age) { this.age = age; } public int getAge() { return age; } public void setName(String name) { this.name = name; } public String getName() { return name; } public void printPerson() { Log.d("MainActivity", "age ======== " + age + "," + "name ======== " + name); } }
JNIUtils.java:
package com.alphagl.main; public class JNIUtils { public static native String getStringFromJNI(); public static native int[] getIntArrayFromJNI(); public static native void setPersonToJNI(Person person); public static native Person getPersonFromJNI(); }
JNIUtils.h:
#include <jni.h> #include <stdio.h> #ifndef _Included_com_alphagl_main_JNIUtils #define _Included_com_alphagl_main_JNIUtils #ifdef __cplusplus extern "C" { #endif JNIEXPORT jstring JNICALL Java_com_alphagl_main_JNIUtils_getStringFromJNI (JNIEnv *, jclass); JNIEXPORT jintArray JNICALL Java_com_alphagl_main_JNIUtils_getIntArrayFromJNI (JNIEnv *, jclass); JNIEXPORT void JNICALL Java_com_alphagl_main_JNIUtils_setPersonToJNI (JNIEnv *, jclass, jobject); JNIEXPORT jobject JNICALL Java_com_alphagl_main_JNIUtils_getPersonFromJNI (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif
JNIUtils.cpp
#include "JNIUtils.h" #include <android/log.h> #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "MainActivity", __VA_ARGS__) #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "MainActivity", __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROE, "MainActivity", __VA_ARGS__) JNIEXPORT jstring JNICALL Java_com_alphagl_main_JNIUtils_getStringFromJNI (JNIEnv *env, jclass jcls) { LOGD(" ====================== getStringFromJNI"); // Construct a String string return env->NewStringUTF("Hello from jni"); } JNIEXPORT jintArray JNICALL Java_com_alphagl_main_JNIUtils_getIntArrayFromJNI (JNIEnv *env, jclass jcls) { LOGD(" ====================== getIntArrayFromJNI"); // Construct an int [] array jintArray intArray = env->NewIntArray(2); int size[]={640, 960}; // Assign values to int [] arrays env->SetIntArrayRegion(intArray, 0, 2, size); return intArray; } JNIEXPORT void JNICALL Java_com_alphagl_main_JNIUtils_setPersonToJNI (JNIEnv *env, jclass jcls, jobject jobj) { LOGD(" ====================== setPersonToJNI"); jclass jperson = env->GetObjectClass(jobj); if (jperson != NULL) { // Get the age field id of the Person object jfieldID ageFieldId = env->GetFieldID(jperson, "age", "I"); // Get the name field id of the Person object jfieldID nameFieldId = env->GetFieldID(jperson, "name", "Ljava/lang/String;"); // Get Person's age member variable jint age = env->GetIntField(jobj, ageFieldId); // Get Person's name member variable jstring name = (jstring)env->GetObjectField(jobj, nameFieldId); const char *c_name = env->GetStringUTFChars(name, NULL); // Print age and name variables of Person objects passed from Java LOGD("age ===== %d, name ===== %s", age, c_name); } // The following is to construct Java objects from JNI and call member methods in Java classes for demonstration purposes only // Get the class of Person object jclass jstu = env->FindClass("com/alphagl/main/Person"); // Getting the method id of the construction method of Person object jmethodID personMethodId = env->GetMethodID(jperson, "<init>", "(ILjava/lang/String;)V"); // Construct a String string jstring name = env->NewStringUTF("bill"); // Construct a Person object jobject jPersonObj = env->NewObject(jstu, personMethodId, 30, name); // Get the method id of the printPerson member method of the Person object jmethodID jid = env->GetMethodID(jstu, "printPerson", "()V"); // Calling java's printPerson method env->CallVoidMethod(jPersonObj, jid); } JNIEXPORT jobject JNICALL Java_com_alphagl_main_JNIUtils_getPersonFromJNI(JNIEnv *env, jclass jcls) { LOGD(" ====================== getPersonFromJNI"); // Get the class of Person object jclass jstudent = env->FindClass("com/alphagl/main/Person"); // Getting the method id of the construction method of Person object jmethodID studentMethodId = env->GetMethodID(jstudent, "<init>", "(ILjava/lang/String;)V"); // Construct a String string jstring name = env->NewStringUTF("john"); // Construct a Person object jobject jstudentObj = env->NewObject(jstudent, studentMethodId, 20, name); return jstudentObj; }
Here again, as defined in the JNIUtils.java class above, how to generate the corresponding C/C++ method declaration according to the method signature of the object. This part is about Android Game Development Practice (1) NDK and JNI Development 01 As mentioned, we can use javah to generate header files from compiled. class.
The common practice is:
In Android Studio, you can:
Tools - > External Tools - > Add
(1) The path of javah
(2) Command line parameters
(3) Path of header file generation
In declaring the class of the native method, right-click to execute javah.
(2) The way of dynamic registration
The structure of the project is as follows: (Here only the main items are listed)
JNISample2 │── build.gradle │── CMakeLists.txt └── app ├── build.gradle ├── CMakeLists.txt └── src ├── cpp │ └── JNIUtils.cpp │ └── com.alphagl.main ├── JNIUtils.java ├── MainActivity.Java └── Person.java
Here's a look at the different parts of the code, JNIUtils.cpp.
JNIUtils.cpp:
#include <jni.h> #include <string> #include <android/log.h> #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "MainActivity", __VA_ARGS__) #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "MainActivity", __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROE, "MainActivity", __VA_ARGS__) #define CLASSNAME "com/alphagl/main/JNIUtils" static jstring getStringFromJNI_native(JNIEnv *env, jclass jcls) { LOGD(" ====================== getStringFromJNI"); // Construct a String string return env->NewStringUTF("Hello from jni"); } static jarray getIntArrayFromJNI_native(JNIEnv *env, jclass jcls) { LOGD(" ====================== getIntArrayFromJNI"); // Construct an int [] array jintArray intArray = env->NewIntArray(2); int size[]={640, 960}; // Assign values to int [] arrays env->SetIntArrayRegion(intArray, 0, 2, size); return intArray; } static void setJniPerson_native(JNIEnv *env, jclass jcls, jobject jobj) { LOGD(" ====================== setPersonToJNI"); jclass jperson = env->GetObjectClass(jobj); if (jperson != NULL) { // Get the age field id of the Person object jfieldID ageFieldId = env->GetFieldID(jperson, "age", "I"); // Get the name field id of the Person object jfieldID nameFieldId = env->GetFieldID(jperson, "name", "Ljava/lang/String;"); // Get Person's age member variable jint age = env->GetIntField(jobj, ageFieldId); // Get Person's name member variable jstring name = (jstring)env->GetObjectField(jobj, nameFieldId); const char *c_name = env->GetStringUTFChars(name, NULL); // Print age and name variables of Person objects passed from Java LOGD("age ===== %d, name ===== %s", age, c_name); } // The following is to construct Java objects from JNI and call member methods in Java classes for demonstration purposes only // Get the class of Person object jclass jstu = env->FindClass("com/alphagl/main/Person"); // Getting the method id of the construction method of Person object jmethodID personMethodId = env->GetMethodID(jperson, "<init>", "(ILjava/lang/String;)V"); // Construct a String string jstring name = env->NewStringUTF("bill"); // Construct a Person object jobject jPersonObj = env->NewObject(jstu, personMethodId, 30, name); // Get the method id of the printPerson member method of the Person object jmethodID jid = env->GetMethodID(jstu, "printPerson", "()V"); // Calling java's printPerson method env->CallVoidMethod(jPersonObj, jid); } static jobject getJniPerson_native(JNIEnv *env, jclass jcls) { LOGD(" ====================== getPersonFromJNI"); // Get the class of Person object jclass jstudent = env->FindClass("com/alphagl/main/Person"); // Getting the method id of the construction method of Person object jmethodID studentMethodId = env->GetMethodID(jstudent, "<init>", "(ILjava/lang/String;)V"); // Construct a String string jstring name = env->NewStringUTF("john"); // Construct a Person object jobject jstudentObj = env->NewObject(jstudent, studentMethodId, 20, name); return jstudentObj; } static JNINativeMethod gMethods[] = { {"getStringFromJNI", "()Ljava/lang/String;", (void*)getStringFromJNI_native}, {"getIntArrayFromJNI", "()[I", (void*)getIntArrayFromJNI_native}, {"setPersonToJNI", "(Lcom/alphagl/main/Person;)V", (void*)setJniPerson_native}, {"getPersonFromJNI", "()Lcom/alphagl/main/Person;", (void*)getJniPerson_native} }; static jint registerNativeMethods(JNIEnv *env, const char* className, JNINativeMethod *gMethods, int numMethods) { jclass jcls; jcls = env->FindClass(className); if (jcls == NULL) { return JNI_FALSE; } if (env->RegisterNatives(jcls, gMethods, numMethods) < 0) { return JNI_FALSE; } return JNI_TRUE; } static jint registerNative(JNIEnv *env) { return registerNativeMethods(env, CLASSNAME, 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 (!registerNative(env)) { return JNI_ERR; } return JNI_VERSION_1_6; }
The final results of implementation are as follows:
Comparisons of the two implementations:
(1) In dynamic registration, methods in the form of Java_packageName_className_methodName may not be declared.
(2) In dynamic registration, we need to rewrite the JNI_OnLoad method, call RegisterNatives manually to register the local method, and declare it in JNI NativeMethod.
(3) Dynamic registration, obviously this way is more flexible, but the code requirements are higher, recommended to use this way.
The above sample code has been uploaded to Github and can be viewed by yourself if necessary.
https://github.com/cnsuperx/android-jni-example
JNI debugging
If the LLVM environment is installed, simply turn on the Jni Debuggable option. Environmental construction can be referred to Android Game Development Practice (1) NDK and JNI Development 03.
Then you can set breakpoints directly in C or C++ code.
QQ Group of Technical Exchange: 528655025
Author: AlphaGL
Source: http://www.cnblogs.com/alphagl/
Copyright is reserved. The original link is welcome to be reproduced.