Android遊戲開發實踐(1)之NDK與JNI開發04

Android遊戲開發實踐(1)之NDK與JNI開發04

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

做者:AlphaGL。版權全部,歡迎保留原文連接進行轉載 :)html

傳送門:
Android遊戲開發實踐(1)之NDK與JNI開發01
Android遊戲開發實踐(1)之NDK與JNI開發02
Android遊戲開發實踐(1)之NDK與JNI開發03java

JavaVM和JNIEnv

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

JavaVM

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

JavaVM聲明

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

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聲明爲:markdown

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*)來建立。app

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->(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,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++代碼中設置斷點便可。

    技術交流QQ羣:528655025
    做者:AlphaGL
    出處:http://www.cnblogs.com/alphagl/
    版權全部,歡迎保留原文連接進行轉載 :)

    posted on 2017-02-23 09:46 alphagl 閱讀( ...) 評論( ...) 編輯 收藏
相關文章
相關標籤/搜索