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

作者: AlphaGL | 来源:发表于2017-02-22 20:47 被阅读210次

    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++代码中设置断点即可。

    相关文章

      网友评论

        本文标题:Android游戏开发实践(1)之NDK与JNI开发04

        本文链接:https://www.haomeiwen.com/subject/ayamwttx.html