Android游戏开发实践(1)之NDK与JNI开发04

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头文件中定义了两种重要的数据结构JavaVMJNIEnv,并且在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,需要调用AttachCurrentThreadAttachCurrentThreadAsDaemon将当前线程与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++代码中设置断点即可。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容