Android游戲開發實踐(1)之NDK與JNI開發04
有了前面幾篇NDK與JNI開發相關基礎做鋪墊,再來通過代碼說明下這方面具體的操作以及一些重要的細節。那么,就繼續NDK與JNI的學習總結。
傳送門:
Android游戲開發實踐(1)之NDK與JNI開發01
Android游戲開發實踐(1)之NDK與JNI開發02
Android游戲開發實踐(1)之NDK與JNI開發03
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聲明為:
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*);
};
C++版的JavaVM聲明為:
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*/
};
JavaVM獲取方式
(1)jni動態注冊的方式。在加載動態鏈接庫的時候,JVM會調用JNI_OnLoad(JavaVM* vm, void* reserved),并傳入JavaVM指針:
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
}
(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聲明為:
typedef const struct JNINativeInterface* JNIEnv;
struct JNINativeInterface {
jint (*GetVersion)(JNIEnv *);
···
}
C++版的JavaVM聲明為:
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、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->GetMethodID(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
,那么,調用為:
static {
System.loadLibrary("cocos2dx");
}
(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<Object>,HashMap等)
而調用方式有可以分為:
(1)java調用native方法
(2)native調用java靜態方法,非靜態方法(成員方法),以及獲取java類的成員變量。
下面按照實現方式的不同結合以上要點,通過一個例子代碼來說明下具體是如何實現的。
(1)靜態注冊的方式
工程結構如下:(這里只列舉出主要說明的項)
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
代碼如下:(這里做了下簡化,去掉些注釋以及單元測試部分的代碼)
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:(封裝的自定義對象)
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");
// 構造一個String字符串
return env->NewStringUTF("Hello from jni");
}
JNIEXPORT jintArray JNICALL Java_com_alphagl_main_JNIUtils_getIntArrayFromJNI (JNIEnv *env, jclass jcls) {
LOGD(" ====================== getIntArrayFromJNI");
// 構造一個int[]數組
jintArray intArray = env->NewIntArray(2);
int size[]={640, 960};
// 給int[]數組賦值
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) {
// 獲取Person對象的age字段id
jfieldID ageFieldId = env->GetFieldID(jperson, "age", "I");
// 獲取Person對象的name字段id
jfieldID nameFieldId = env->GetFieldID(jperson, "name", "Ljava/lang/String;");
// 獲取Person的age成員變量
jint age = env->GetIntField(jobj, ageFieldId);
// 獲取Person的name成員變量
jstring name = (jstring)env->GetObjectField(jobj, nameFieldId);
const char *c_name = env->GetStringUTFChars(name, NULL);
// 打印從Java傳遞過來的Person對象的age和name變量
LOGD("age ===== %d, name ===== %s", age, c_name);
}
// 以下是從JNI構造Java對象,并調用Java類中的成員方法,僅用作演示
// 獲取Person對象的class
jclass jstu = env->FindClass("com/alphagl/main/Person");
// 獲取Person對象的構造方法的方法id
jmethodID personMethodId = env->GetMethodID(jperson, "<init>", "(ILjava/lang/String;)V");
// 構造一個String字符串
jstring name = env->NewStringUTF("bill");
// 構造一個Person對象
jobject jPersonObj = env->NewObject(jstu, personMethodId, 30, name);
// 獲取Person對象的printPerson成員方法的方法id
jmethodID jid = env->GetMethodID(jstu, "printPerson", "()V");
// 調用java的printPerson方法
env->CallVoidMethod(jPersonObj, jid);
}
JNIEXPORT jobject JNICALL Java_com_alphagl_main_JNIUtils_getPersonFromJNI(JNIEnv *env, jclass jcls) {
LOGD(" ====================== getPersonFromJNI");
// 獲取Person對象的class
jclass jstudent = env->FindClass("com/alphagl/main/Person");
// 獲取Person對象的構造方法的方法id
jmethodID studentMethodId = env->GetMethodID(jstudent, "<init>", "(ILjava/lang/String;)V");
// 構造一個String字符串
jstring name = env->NewStringUTF("john");
// 構造一個Person對象
jobject jstudentObj = env->NewObject(jstudent, studentMethodId, 20, name);
return jstudentObj;
}
這里再提一下,如上`JNIUtils.java`類中定義好了native方法,如何根據對象的方法簽名生成對應的C/C++方法的聲明。這部分內容在Android游戲開發實踐(1)之NDK與JNI開發01 已經提到過,我們可以借助javah
來根據編譯后的.class
生成對于的頭文件。
普通做法是:
在AndroidStudio中可以:
Tools-> External Tools -> 添加
(1)
javah
所在的路徑(2)命令行參數
(3)頭文件生成的路徑
在聲明了native方法的類,右鍵執行
javah
即可。
(2)動態注冊的方式
工程結構如下:(這里只列舉出主要說明的項)
JNISample2
│── build.gradle
│── CMakeLists.txt
└── app
├── build.gradle
├── CMakeLists.txt
└── src
├── cpp
│ └── JNIUtils.cpp
│
└── com.alphagl.main
├── JNIUtils.java
├── MainActivity.Java
└── Person.java
這里主要看下不同的代碼部分,即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");
// 構造一個String字符串
return env->NewStringUTF("Hello from jni");
}
static jarray getIntArrayFromJNI_native(JNIEnv *env, jclass jcls) {
LOGD(" ====================== getIntArrayFromJNI");
// 構造一個int[]數組
jintArray intArray = env->NewIntArray(2);
int size[]={640, 960};
// 給int[]數組賦值
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) {
// 獲取Person對象的age字段id
jfieldID ageFieldId = env->GetFieldID(jperson, "age", "I");
// 獲取Person對象的name字段id
jfieldID nameFieldId = env->GetFieldID(jperson, "name", "Ljava/lang/String;");
// 獲取Person的age成員變量
jint age = env->GetIntField(jobj, ageFieldId);
// 獲取Person的name成員變量
jstring name = (jstring)env->GetObjectField(jobj, nameFieldId);
const char *c_name = env->GetStringUTFChars(name, NULL);
// 打印從Java傳遞過來的Person對象的age和name變量
LOGD("age ===== %d, name ===== %s", age, c_name);
}
// 以下是從JNI構造Java對象,并調用Java類中的成員方法,僅用作演示
// 獲取Person對象的class
jclass jstu = env->FindClass("com/alphagl/main/Person");
// 獲取Person對象的構造方法的方法id
jmethodID personMethodId = env->GetMethodID(jperson, "<init>", "(ILjava/lang/String;)V");
// 構造一個String字符串
jstring name = env->NewStringUTF("bill");
// 構造一個Person對象
jobject jPersonObj = env->NewObject(jstu, personMethodId, 30, name);
// 獲取Person對象的printPerson成員方法的方法id
jmethodID jid = env->GetMethodID(jstu, "printPerson", "()V");
// 調用java的printPerson方法
env->CallVoidMethod(jPersonObj, jid);
}
static jobject getJniPerson_native(JNIEnv *env, jclass jcls) {
LOGD(" ====================== getPersonFromJNI");
// 獲取Person對象的class
jclass jstudent = env->FindClass("com/alphagl/main/Person");
// 獲取Person對象的構造方法的方法id
jmethodID studentMethodId = env->GetMethodID(jstudent, "<init>", "(ILjava/lang/String;)V");
// 構造一個String字符串
jstring name = env->NewStringUTF("john");
// 構造一個Person對象
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;
}
最后的執行結果為:
兩種實現方式比較:
(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。
接著直接在C或C++代碼中設置斷點即可。