1. 程式人生 > >基於CMake的JNI開發探索

基於CMake的JNI開發探索

實際專案中用到了JNI,在此對碰到的坑做一些總結。android studio2.2支援android.mk和CMake編譯配置工具兩種方式,谷歌官方推薦使用CMake並且做了很好的支援。

首先,需要配置ndk環境:
File->Settings->Appearance&Behavior->System Setings->Android SDK,選中SDK Tools標籤頁,選擇CMake,LLDB,NDK進行安裝如下圖:
CMake:編譯配置工具。
LLDB:除錯C程式碼。
NDK:開發工具包。
這裡寫圖片描述
下載完成後檢視 File->Project Structure,是否配置ndk:
(如果沒有就配置一下)
這裡寫圖片描述

好了NDK環境配置完成,接下來重啟IDE,新建一個jni工程:

需要勾選include c++ support,專案就可以進行ndk開發
這裡寫圖片描述

C++ Standard
指定編譯庫的環境,其中Toolchain Default使用的是預設的CMake環境。建議選擇C++ 11,表示支援C++ 11庫。

Exceptions Support
如果選中複選框,則表示當前專案支援C++異常處理,建議勾選。

Runtime Type Information Support
同理,選中複選框,專案支援RTTI,建議勾選。

注:當然此處不勾選,也可以在專案中的gradle下新增支援
這裡寫圖片描述

好了,專案新建完成,檢視專案的結構:
cpp目錄:用於存放c/c++檔案。
CMakeLists.txt:CMake的配置檔案。
這裡寫圖片描述

直入主題,首先刪掉了android studio給我們的hellow示例。新建了一個JNIManager單例類,用於存放native介面;定義了一個頭檔案native-lib.h(cpp目錄下)。

public class JNIManager {

    static {
        System.loadLibrary("native-lib");
    }

    private static JNIManager mJNIManger = null
; private JNIManager() { } public static JNIManager getInstance() { if (mJNIManger == null) { synchronized (JNIManager.class) { if (mJNIManger == null) { mJNIManger = new JNIManager(); } } } return mJNIManger; } public native int add(int x, int y);//java傳入int值給C,C返回int值給java public static native String passString(String str);//傳入String值,返回String值 public static native int[] passIntArray(int[] intArray);//傳入int陣列,返回int陣列 public native void passOthers(char m_char, boolean m_boolean, byte m_byte, short m_short, long m_long, float m_float, double m_double);//傳入其他基本資料型別,無返回值 }

其中,

static {
        System.loadLibrary("native-lib");
    }

表示匯入native-lib的庫檔案,以便使用庫中的api。其介面都用native宣告(static修飾符可加可不加),在宣告介面後,用快捷鍵可在cpp生成相應的函式,如下:

#include "native-lib.h"

JNIEXPORT jint JNICALL
Java_com_hecc_jnitest_JNIManager_add(JNIEnv *env, jobject instance, jint x, jint y) {

    return x + y;//返回x,y的和
}

JNIEXPORT jstring JNICALL
Java_com_hecc_jnitest_JNIManager_passString(JNIEnv *env, jclass type, jstring m_str) {

    char *str = (char *) env->GetStringUTFChars(m_str, 0);//將java中的String型別的值轉化成C識別的char*型別的值
    // 對字串進行(移位)處理
    int length = strlen(str);
    for (int i = 0; i < length; ++i) {
        *(str + i) += 1;
    }
    env->ReleaseStringUTFChars(m_str, str);//釋放記憶體

    return env->NewStringUTF(str);//返回一個處理後的字串
}

JNIEXPORT jintArray JNICALL
Java_com_hecc_jnitest_JNIManager_passIntArray(JNIEnv *env, jclass type, jintArray m_intArray) {


    jint *intArray = env->GetIntArrayElements(m_intArray, NULL);//獲取陣列的指標
    jsize length = env->GetArrayLength(m_intArray);//獲取陣列長度
    // 對陣列進行(元素+10)處理
    for (int i = 0; i < length; i++) {
        *(intArray + i) += 10;
    }
    env->ReleaseIntArrayElements(m_intArray, intArray, 0);//釋放記憶體

    return m_intArray;//返回一個處理後的陣列
}

JNIEXPORT void JNICALL
Java_com_hecc_jnitest_JNIManager_passOthers(JNIEnv *env, jobject instance, jchar m_char,
                                            jboolean m_boolean, jbyte m_byte, jshort m_short,
                                            jlong m_long, jfloat m_float, jdouble m_double) {

    LOGI("m_char=%c,m_boolean=%d,m_byte=%d,m_short=%hd,m_long=%ld,m_float=%f,m_double=%lf",
         m_char, m_boolean, m_byte, m_short, m_long, m_float, m_double);

    LOGE("列印:LOGE");
}

簡單說明下,ndk環境下的書寫格式都是固定的。
C本地函式命名規則: Java_包名類名本地方法名
JNIEXPORT,JNICALL:是系統定義的巨集,JNIEXPORT後面寫函式的輸出型別,JNICALL後面寫函式名,實測這兩個巨集可寫可不寫。
JNIEnv *env: 是結構體JNINativeInterface的二級指標,重定義了大量的函式指標,這些函式指標在jni開發中很常用。可以理解為JNI的上下文環境,需要通過env呼叫各種介面。
jobject instance :呼叫本地函式的java物件,在此例中就是JNIManager的例項。

為了程式碼清晰,我把函式的宣告放在標頭檔案native-lib.h,如下:
注:extern “C”裡寫函式宣告,這一步也是必須的,否則將報錯:cant find method

#ifndef JNITEST_NATIVE_LIB_H
#define JNITEST_NATIVE_LIB_H

#include <jni.h>
#include <string>
#include <android/log.h>

#define  LOG_TAG    "System.out.fromC"
#define  LOGI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

extern "C" {

JNIEXPORT jint JNICALL
        Java_com_hecc_jnitest_JNIManager_add(JNIEnv *env, jobject instance, jint x, jint y);

JNIEXPORT jstring JNICALL
        Java_com_hecc_jnitest_JNIManager_passString(JNIEnv *env, jclass type, jstring str_);

JNIEXPORT jintArray JNICALL
        Java_com_hecc_jnitest_JNIManager_passIntArray(JNIEnv *env, jclass type, jintArray intArray_);

JNIEXPORT void JNICALL
              Java_com_hecc_jnitest_JNIManager_passOthers(JNIEnv *env, jobject instance, 
              jchar m_char, jboolean m_boolean, jbyte m_byte, jshort m_short, jlong m_long, 
              jfloat m_float, jdouble m_double);

}

#endif

然後在MainActivity寫測試JNI的程式碼,如下:

public class MainActivity extends AppCompatActivity {


    JNIManager manager = JNIManager.getInstance();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        testJNI();

    }

    private void testJNI() {
        int sum = manager.add(3, 4);
        Log.i("MainActivity", "3+4=" + sum);

        String str = JNIManager.passString("abc");
        Log.i("MainActivity", "abc from C:" + str);

        int[] arr = {1, 2, 3};
        int[] newArr = manager.passIntArray(arr);
        printArr(arr);//列印原陣列
        printArr(newArr);//列印JNI處理後的新陣列
    }

    void printArr(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            Log.i("MainActivity", "元素" + i + "=" + arr[i]);
        }
    }
}

列印的結果如圖:
print

檢視結果正確,呼叫jni函式成功!值得注意的是,此處我列印了兩次陣列,發現原陣列和新陣列的結果一致。是不是很意外,其實不難理解,原陣列傳給C後(對照程式碼),獲取原陣列的指標和長度,然後是對其記憶體地址進行操作。然後返回值是操作後的原陣列(所謂的新陣列)。因此,兩則結果一致就有理可據了,類似思想在JNI中會大量的用到。

可能大家會有疑問,如何在NDK環境中列印LOG?
答:需要呼叫系統庫,在CMakeList.txt中配置相關log庫(模板demo已配置好),然後在標頭檔案中定義相關巨集,如下:
log

上面C程式碼中是不是還有一個函式未測試,接下來在MainActivity中新增如下程式碼:

 char m_char = 'a';
 boolean m_boolean = true;
 byte m_byte = 1;
 short m_short = 2;
 long m_long = 30000000000L;
 float m_float = 1.2F;
 double m_double = 3.333D;
 manager.passOthers(m_char, m_boolean, m_byte, m_short, m_long, m_float, m_double);

log
發現效果還可以,以後就方便在C中打log了。(注:C的基本資料沒有boolean型別,所以java中true對應C中是1,false對應C中是0

現在,我打算在java中寫JNIBean類,存放各種資料型別;在C++中寫一個CppBean.cpp和CppBean.h,用於和java資料進行互動。將java的實體類建立物件並賦值,傳遞給本地,再賦值給c++的對映的單例物件,大致的流程就是java–>ndk–>c/c++。
在cpp目錄新增C/C++檔案時,需要CMakeList.txt中進行配置,如下:
cmakelist.txt
你可能會覺得上圖和自己的檔案不一致,CMake語法中#是註釋,所以我將原註釋刪除,寫了中文註釋。然後新增一行程式碼:src/main/cpp/CppBean.cpp。(對CMake語法不做說明)接著,點選按鈕 Tools->Android->Sync Project with Gradle Files 進行同步。

JNIBean的程式碼,如下:

public class JNIBean {
    public static int jb_int;//靜態變數

    public boolean   jb_boolean;
    public byte      jb_byte;
    public short     jb_short;
    public long      jb_long;
    public float     jb_float;
    public double    jb_double;

    public String    jb_str;
    public int[]     jb_intArr;
}

CppBean.h的程式碼,如下:

#ifndef JNITEST_CPPBEAN_H
#define JNITEST_CPPBEAN_H

#include <string>

using namespace std;

#define MCppBean  (CppBean::GetInstance())

typedef signed char Bool;
#define MPTrue          1
#define MPFalse         0

typedef unsigned char Byte;

class CppBean {
public:
    int getcpp_int() const;

    void setcpp_int(int jb_int);

    Bool getcpp_Bool() const;

    void setcpp_Bool(Bool jb_boolean);

    Byte getcpp_byte() const;

    void setcpp_byte(Byte jb_byte);

    short getcpp_short() const;

    void setcpp_short(short jb_short);

    long getcpp_long() const;

    void setcpp_long(long jb_long);

    float getcpp_float() const;

    void setcpp_float(float jb_float);

    double getcpp_double() const;

    void setcpp_double(double jb_double);

    string getcpp_str() const;

    void setcpp_str(char *str);

    int *getcpp_intArr();

    void setcpp_intArr(int *intArr, int length);


public:
    static CppBean &GetInstance() {
        static CppBean instance;
        return instance;
    }

    virtual ~CppBean();

private:
    CppBean();

    CppBean(const CppBean &);

    CppBean &operator=(const CppBean &);

private:

    int cpp_int;
    Bool cpp_boolean;
    Byte cpp_byte;
    short cpp_short;
    long cpp_long;
    float cpp_float;
    double cpp_double;

    string cpp_str;
    int cpp_intArr[];

};


#endif

CppBean.cpp的程式碼,如下:

#include "cppbean.h"

CppBean::CppBean() {
}

CppBean::~CppBean() {
}

int CppBean::getcpp_int() const {
    return cpp_int;
}

void CppBean::setcpp_int(int jb_int) {
    cpp_int = jb_int;
}

Bool CppBean::getcpp_Bool() const {
    return cpp_boolean;
}

void CppBean::setcpp_Bool(Bool jb_boolean) {
    cpp_boolean = jb_boolean;
}

Byte CppBean::getcpp_byte() const {
    return cpp_byte;
}

void CppBean::setcpp_byte(Byte jb_byte) {
    cpp_byte = jb_byte;
}

short CppBean::getcpp_short() const {
    return cpp_short;
}

void CppBean::setcpp_short(short jb_short) {
    cpp_short = jb_short;
}

long CppBean::getcpp_long() const {
    return cpp_long;
}

void CppBean::setcpp_long(long jb_long) {
    cpp_long = jb_long;
}

float CppBean::getcpp_float() const {
    return cpp_float;
}

void CppBean::setcpp_float(float jb_float) {
    cpp_float = jb_float;
}

double CppBean::getcpp_double() const {
    return cpp_double;
}

void CppBean::setcpp_double(double jb_double) {
    cpp_double = jb_double;
}

string CppBean::getcpp_str() const {
    return cpp_str;
}

void CppBean::setcpp_str(char *str) {
    cpp_str.assign(str);
}

int *CppBean::getcpp_intArr() {
    return cpp_intArr;
}

void CppBean::setcpp_intArr(int *intArr, int length) {
    memcpy(cpp_intArr, intArr, length * 4);
}

然後我們需要在JNI中新增介面:

public native void passData(JNIBean bean);

然後在native-lib.h中宣告,並在native-lib.cpp中實現:

extern "C" {

        ...

JNIEXPORT void JNICALL
        Java_com_hecc_jnitest_JNIManager_passData(JNIEnv *env, jobject instance, jobject bean);
}
#include "native-lib.h"
#include "cppbean.h"//包含cppbean.h標頭檔案

        ...

JNIEXPORT void JNICALL
Java_com_hecc_jnitest_JNIManager_passData(JNIEnv *env, jobject instance, jobject bean) {

    jclass cls_bean = env->GetObjectClass(bean);//獲取位元組碼物件

    jfieldID ids_int = env->GetStaticFieldID(cls_bean, "jb_int", "I");//獲取靜態欄位ID物件
    jint jb_int = env->GetStaticIntField(cls_bean, ids_int);//獲取相應靜態欄位的int值
    MCppBean.setcpp_int(jb_int);//將int值賦給CppBean中的對映值

    jfieldID id_boolean = env->GetFieldID(cls_bean, "jb_boolean", "Z");//獲取非靜態欄位ID物件
    jboolean jb_boolean = env->GetBooleanField(bean, id_boolean);//獲取相應非靜態欄位的boolean值
    MCppBean.setcpp_Bool(jb_boolean);//將boolean值賦給CppBean中的對映值

    jbyte jb_byte = env->GetByteField(bean, env->GetFieldID(cls_bean, "jb_byte", "B"));
    MCppBean.setcpp_byte(jb_byte);

    MCppBean.setcpp_short(env->GetShortField(bean, env->GetFieldID(cls_bean, "jb_short", "S")));

    MCppBean.setcpp_long(env->GetLongField(bean, env->GetFieldID(cls_bean, "jb_long", "J")));

    MCppBean.setcpp_float(env->GetFloatField(bean, env->GetFieldID(cls_bean, "jb_float", "F")));

    MCppBean.setcpp_double(env->GetDoubleField(bean, env->GetFieldID(cls_bean, "jb_double", "D")));

    LOGI("MCppBean中:int欄位值=%d,Bool欄位值=%d,Byte欄位值=%d,short欄位值=%hd,long欄位值=%ld,float欄位值=%f,double欄位值=%lf",
         MCppBean.getcpp_int(), MCppBean.getcpp_Bool(), MCppBean.getcpp_byte(),
         MCppBean.getcpp_short(), MCppBean.getcpp_long(),
         MCppBean.getcpp_float(), MCppBean.getcpp_double());

    //字串
    jfieldID ids_str = env->GetFieldID(cls_bean, "jb_str", "Ljava/lang/String;");
    jstring jb_str = (jstring) env->GetObjectField(bean, ids_str);
    char *str = (char *) env->GetStringUTFChars(jb_str, 0);
    MCppBean.setcpp_str(str);
    env->ReleaseStringUTFChars(jb_str, str);
    LOGI("MCppBean中:string欄位值=%s", MCppBean.getcpp_str().c_str());

    //陣列
    jfieldID ids_intArr = env->GetFieldID(cls_bean, "jb_intArr", "[I");
    jintArray jb_intArr = (jintArray) env->GetObjectField(bean, ids_intArr);
    jint *intArray = env->GetIntArrayElements(jb_intArr, NULL);
    jsize length = env->GetArrayLength(jb_intArr);
    MCppBean.setcpp_intArr(intArray, length);
    env->ReleaseIntArrayElements(jb_intArr, intArray, 0);
    for (int i = 0; i < length; i++) {
        LOGI("陣列元素%d:%d", i, *(MCppBean.getcpp_intArr() + i));
    }
}

在NDK中基本資料型別轉換的寫法是固定的:
1.獲取javabean的位元組碼物件
除了通過env->GetObjectClass(object物件)方法,還可通過env->FindClass(“com/hecc/jnitest/JNIManager”)獲取,形參填寫javabean的全類名。

2.獲取欄位ID物件
java中欄位分為靜態和非靜態兩種,對應的api也有兩種:
jfieldID GetStaticFieldID(jclass clazz, const char* name, const char* sig);
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig);
形參1:正是步驟一的位元組碼物件。形參2:方法名。形參3:欄位描述符,簡單說每個基本資料型別都有一個固定的標記作識別(參考下表)。
這裡寫圖片描述
(注:另外陣列型別的簡寫,則用”[“加上如表所示的對應型別的簡寫形式進行表示就可以了,
比如:[I 表示 int []。[L全類名; 表示類型別陣列。另外,引用型別(除基本型別的陣列外)的標示最後都有個”;”)

3.獲取欄位值
欄位值
獲取欄位值的api也分靜態和非靜態(本地型別和java型別查上表):
本地基本資料型別 GetStatic(java型別)Field(jclass clazz, jfieldID fieldID);
本地基本資料型別 Get(java型別)Field(jobject obj, jfieldID fieldID);
形參1:若是靜態,為位元組碼物件;非靜態,為例項物件。形參2:正是步驟二的欄位ID物件。

對於字串和陣列的獲取稍微複雜點,所以單獨列出。
字串:jstring其實是_jobject的子類,所以用GetObjectField()獲取jstring,而c/c++並不識別jstring型別,別擔心,ndk提供了相關api—-GetStringUTFChars(jstring string, jboolean* isCopy)來轉換成char *型別。
陣列(int陣列為例):jintArray也是_jobject的子類,同時ndk提供了相關api來獲取陣列首地—- GetIntArrayElements(jintArray array, jboolean* isCopy)和陣列個數—-GetArrayLength(jarray array)。

在MainActivity新增如下程式碼,進行測試:

  ...
private void testJNI() {

     ...

        JNIBean bean = new JNIBean();
        JNIBean.jb_int = 11;
        bean.jb_boolean = false;
        bean.jb_byte = 22;
        bean.jb_short = 33;
        bean.jb_long = 30000000000L;
        bean.jb_float = 12.34f;
        bean.jb_double = 56.789d;
        bean.jb_str = "我是測試程式碼";
        bean.jb_intArr = new int[]{1, 23, 456, 78, 9};
        manager.passData(bean);
    }

執行,列印結果如圖:
result

有人可能會有這樣的需求:如何將內部類,類型別陣列的資料傳遞給本地?
ndk中資料的轉換方式是相當固定的,如果理解了上述的例項,對於類似的需要並不難實現。
首先,在JNIBean 中新增內部類和類型別陣列。

public class JNIBean {

    ...

    public JNIChildBean jb_ChildBean;
    public JNIChildBean[] jb_arr_ChildBean;

    public class JNIChildBean {
        public int jcb_int;
    }
}

在native-lib中新增如下程式碼:

JNIEXPORT void JNICALL
Java_com_hecc_jnitest_JNIManager_passData(JNIEnv *env, jobject instance, jobject bean) {

    ...

    //內部類
    jfieldID ids_child = env->GetFieldID(cls_bean, "jb_ChildBean", "Lcom/hecc/jnitest/JNIBean$JNIChildBean;");//內部類用$符號,而不是/符號。
    jobject jb_child = env->GetObjectField(bean, ids_child);
    jclass cls_child = env->GetObjectClass(jb_child);
    jint jcb_int = env->GetIntField(jb_child, env->GetFieldID(cls_child, "jcb_int", "I"));
    LOGI("jcb_int=%d", jcb_int);

    //類型別陣列
    jfieldID ids_arr_child = env->GetFieldID(cls_bean, "jb_arr_ChildBean",
                                     "[Lcom/hecc/jnitest/JNIBean$JNIChildBean;");
    jobjectArray jb_arr_child = (jobjectArray) env->GetObjectField(bean, ids_arr_child);
    jsize size = env->GetArrayLength(jb_arr_child);
    for (int i = 0; i < size; ++i) {
        jobject jab_child = env->GetObjectArrayElement(jb_arr_child, i);//獲取每個陣列的元素
        jint jacb_int = env->GetIntField(jab_child, 
                                env->GetFieldID(env->GetObjectClass(jab_child), "jcb_int", "I"));
        LOGI("jacb_int=%d",jacb_int);
    }
}

然後在MainActivity在新增如下程式碼,進行測試:

public class MainActivity extends AppCompatActivity {

  ...

    private void testJNI() {

     ...

        bean.jb_ChildBean = bean.new JNIChildBean();
        bean.jb_ChildBean.jcb_int = 1010;

        bean.jb_arr_ChildBean = new JNIBean.JNIChildBean[5];
        for (int i = 0; i < 5; i++) {
            JNIBean.JNIChildBean childBean = bean.new JNIChildBean();
            childBean.jcb_int = i + 2000;
            bean.jb_arr_ChildBean[i] = childBean;
        }

        manager.passData(bean);
    }
}

列印結果如圖:
這裡寫圖片描述

以上示例,都是以java–>c/c++的形式,實際需求中可能需要c/c++–>java或則java–>c/c++–>java的形式傳遞。那麼現在就以java–>c/c++–>java,我打算在介面上寫個按鈕,點選按鈕開啟一個執行緒呼叫本地方法,ndk再呼叫c++的函式處理邏輯(這裡讓執行緒睡了3秒,模擬耗時操作),然後c++通過ndk回撥java的函式,並列印日誌。事件邏輯可能有點複雜,下面按照先後流程貼程式碼:

在MainActivity在新增如下程式碼:

public class MainActivity extends AppCompatActivity {

    ...

    //按鈕點選事件
    public void test(View v) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                manager.test();
            }
        }).start();
    }
}

在JNIManager在新增如下程式碼:

public class JNIManager {

   ...

    public native void test();

    //c++呼叫java的函式
    public void printFromC(int sec) {
        Log.i("JNIManager", "hello from c after " + sec + "s");
    }
}

我打算先建立c++的類,並在CMakeLists.txt中配置
text.h檔案:

#ifndef JNITEST_TEST_H
#define JNITEST_TEST_H

extern void runTest();

#endif

text.cpp檔案:

#include "test.h"
#include <unistd.h>
#include "native-lib.h"//包含native-lib.h標頭檔案

void runTest() {
    int sec = 3;
    ::sleep(sec); // 睡3秒
    call_printFromC(sec);//需要在native-lib.h中宣告
}

這裡寫圖片描述
配置完成後點選同步按鈕。

此時在native-lib.h中宣告函式:

...

extern "C" {

    ...

JNIEXPORT void JNICALL
        Java_com_hecc_jnitest_JNIManager_test(JNIEnv *env, jobject instance);
}

extern void call_printFromC(int sec);//回撥函式

最後是native-lib.cpp的程式碼:

#include "native-lib.h"
#include "cppbean.h"
#include "test.h"//包含test.h標頭檔案

static JavaVM *m_JavaVM;//java虛擬機器
static jclass m_jcls_JNI;

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *java_vm, void *reserved) {
    m_JavaVM = java_vm;

    JNIEnv *jni_env = 0;

    //獲取JavaVM的JNIEnv 
    if (m_JavaVM->GetEnv((void **) (&jni_env), JNI_VERSION_1_4) != JNI_OK) {
        return -1;
    }
    jclass jcls_JNI = jni_env->FindClass("com/hecc/jnitest/JNIManager");
    m_jcls_JNI = (jclass) jni_env->NewGlobalRef((jobject) jcls_JNI);//全域性變數,方便呼叫
    return JNI_VERSION_1_4;
}

...


JNIEXPORT void JNICALL
Java_com_hecc_jnitest_JNIManager_test(JNIEnv *env, jobject instance) {

    runTest();//呼叫test.h的方法
}

void call_printFromC(int sec) {
    JNIEnv *env;
    m_JavaVM->AttachCurrentThread(&env, NULL);//獲取當前執行緒的JNIEnv 
    jmethodID methodID = env->GetMethodID(m_jcls_JNI, "printFromC", "(I)V");
    jobject obj_manager = env->AllocObject(m_jcls_JNI);//獲取例項物件
    env->CallVoidMethod(obj_manager,methodID,sec);
}

眾所周知,ndk環境下呼叫api離不開JNIEnv指標,但是如何在c/c++函式中獲取JNIEnv呢?
答:要獲取JNIEnv之前,先了解JavaVM ,它代表java的虛擬機器,所有的工作都是從獲取虛擬機器的介面開始的。在載入動態連結庫的時候,JVM會呼叫JNI_OnLoad(JavaVM* jvm, void* reserved)(如果定義了該函式),第一個引數會傳入JavaVM指標。然後,通過JVM的AttachCurrentThread(JNIEnv** p_env, void* thr_args)的函式獲取JNIEnv。
獲取到JNIEnv後,想要呼叫其他api就方便了。對於ndk回撥java函式的格式也是相當固定的:

1.獲取函式ID物件
java中函式分為靜態和非靜態,對應的api:
jmethodID GetStaticMethodID(jclass clazz, const char* name, const char* sig);
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig);
形參1:位元組碼物件。形參2:方法名。形參3:函式描述符,具體格式是(形參型別)返回值型別,另外形參可以寫任意個數,沒有返回值用V(表示void型)表示。

2.執行回撥函式
也分靜態和非靜態:
本地基本資料型別 CallStatic(返回值型別)Method(jclass clazz, jmethodID methodID, …)
本地基本資料型別 Call(返回值型別)Method(jobject obj, jmethodID methodID, …)
形參1:若是靜態,為位元組碼物件;非靜態,為例項物件。形參2:正是步驟一的函式ID物件。其他形參:這裡填寫的就是java函式對應的引數值,可以寫任意個,沒有就不用寫。

回撥java函式完成,點選test按鈕,執行檢視結果(等待3秒):
這裡寫圖片描述

這個時候,來了一個變態需求,JeanBean裡有一個靜態的int型二維陣列,通過JNI給其賦值。

public class JNIBean {
...
    public static int[][] jb_intArr2;
}

來看看具體的實現過程:

...

 void call_printFromC(int sec) {
    int pInt[2][3] = {{1, 2, 3}, {4, 5, 6}};//模擬資料

    JNIEnv *env;
    m_JavaVM->AttachCurrentThread(&env, NULL);

    processJNIBean(env, pInt);//處理資料

    jmethodID methodID = env->GetMethodID(m_jcls_JNI, "printFromC", "(I)V");
    jobject newManager = env->AllocObject(m_jcls_JNI);
    env->CallVoidMethod(newManager, methodID, sec);
}

void processJNIBean(JNIEnv *env, int pInt[2][3]) {
    jclass cls_JB = env->FindClass("com/hecc/jnitest/JNIBean");
    jobjectArray _infoArr2 = env->NewObjectArray(2, env->FindClass("[I"), NULL);//建立一個長度為2,元素為int[]的jobjectArray 
    for (int i = 0; i < 2; ++i) {
        jintArray _infoArr = env->NewIntArray(3);//建立長度為3的jintArray 陣列
        env->SetIntArrayRegion(_infoArr, 0, 3, pInt[i]);//給jintArray賦值 
        env->SetObjectArrayElement(_infoArr2, i, _infoArr);//給jobjectArray賦值
    }
    jfieldID id_infoArr2 = env->GetStaticFieldID(cls_JB, "jb_intArr2", "[[I"); //靜態二維陣列ID物件
    env->SetStaticObjectField(cls_JB, id_infoArr2, _infoArr2);//設定java端陣列資料
}

千萬不要忘了在native-lib.h宣告函式:

static void processJNIBean(JNIEnv *env,int pInt[2][3]);

然後列印陣列元素:

public class JNIManager {

   ...

    //c++呼叫java的函式
    public void printFromC(int sec) {

        for (int i = 0; i < 2; i++) {
            for (int j = 0; j < 3; j++) {
                Log.i("JNIManager","元素["+i+"]["+j+"]="+JNIBean.jb_intArr2[i][j]);
            }
        }
    }
}

這裡寫圖片描述
賦值成功,和模擬的資料一樣。

如果對ndk 的api不熟悉,可以檢視官方文件:

吾日三省吾身:
程式碼寫完了,功能實現了,穩定性夠不夠?網路是不是有優化的空間?執行緒是否安全?記憶體耗費是否可以更小一些,釋放乾淨沒有?各種平臺的適配做得夠不夠好?