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閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容