Android JNI開發系列之Java與C相互呼叫

arvinljw發表於2018-09-03

這是這個系列的第二篇,第一篇介紹瞭如何配置。這一篇介紹Java與C如何相互介紹。

沒有配置過的可以去看看Android JNI開發系列之配置

首先介紹的就是Java如何呼叫C,而C呼叫Java核心使用的就是反射,下面會依次介紹。

一、Java呼叫C

第一篇中有個簡單的例子,就是使用Java呼叫C,呼叫一個無參的native函式,並返回一個String,下面接著說點更多的情況:

  • 基本型別對應情況
  • 字串處理
  • 陣列的處理

基本型別對應情況

因為Java和C的基本型別也有些許區別,而在這兩者之間還有一個jni的型別作為橋樑連線轉換型別,有一張圖特別好,一看就清楚了,借了一下這位作者文章中的圖,表示感謝。

type_relationship.png

下邊對於資料的處理就是基於這些型別去處理的。

字串的處理

1、首先先來一個字串的拼接

這個也是坑了我這個萌新不少,體會到其實Java的垃圾回收機制還是很方便的。

其中在c中字串的拼接主要就是使用strcat方法,匯入#include<string.h>包。

還是老樣子,先定義一個native方法,對於配置都是在上一篇的基礎上的:

public class Hello {
    static {
        System.loadLibrary("Hello");
    }

    //傳入一個字串,拼接一段字串後返回
    public native String sayHello(String msg);
}
複製程式碼

接著在Hello.c檔案中寫這個方法,這裡有兩種方法去寫這個方法,第一種是手動自己寫,也有點技巧:

  • 首先看到返回的是String,對應的就是jstring
  • 然後函式名就是:Java_類完全限定名_方法名,其中完全限定名,可以在Hello這個類上右鍵->Copy Reference,然後再把名字中間的點改為下劃線
  • 然後函式的引數:前兩個引數必須的,JNIEnv *env, jobject instance,然後第三個引數開始就是在Java中定義的方法的引數,這裡傳入了一個String,在這裡的就改為jstring msg,方法如下:
jstring Java_net_arvin_androidstudy_jni_Hello_sayHello(JNIEnv *env, jobject instance,
                                                       jstring msg) {
    // implement code...
}
複製程式碼

還有一種方法就是使用javah命令,處理.java檔案就能得到定義的.h檔案;方法就是在該專案的java目錄下,使用命令javah 類的完全限定名,在我這個專案裡就是: javah net.arvin.androidstudy.jni.Hello

這樣在java目錄下就有一個net_arvin_androidstudy_jni_Hello.h檔案,開啟可以看到這個方法:

JNIEXPORT jstring JNICALL Java_net_arvin_androidstudy_jni_Hello_sayHello
  (JNIEnv *, jobject, jstring);
複製程式碼

其中JNIEXPORT和JNICALL關鍵字都可以去掉的,去掉後就和上邊的方法一樣了,然後自己去把引數的名字補充上即可。

最後對於字串的拼接,沒啥好說的,我這裡提供一種方式:

jstring Java_net_arvin_androidstudy_jni_Hello_sayHello(JNIEnv *env, jobject instance,
                                                       jstring msg) {
    char *fromJava = (char *) (*env)->GetStringUTFChars(env, msg, JNI_FALSE);
    char *fromC = " add I am from C~";
    char *result = (char *) malloc(strlen(fromJava) + strlen(fromC) + 1);
    strcpy(result, fromJava);
    strcat(result, fromC);
    return (*env)->NewStringUTF(env, result);
}
複製程式碼
  • 先將jstring轉為char*
  • 然後把要拼接的字串定義出來
  • 接著關鍵來了,動態申請一塊區域用於儲存拼接後的字串,申請的長度就是傳進來的字串和要新增的長度之和
  • 接著就是把這兩個字串拼在一起,先使用strcpy是因為result還沒有初始化,相當於把fromJava賦值給result,然後再把fromC拼接到result中
  • 最後就是使用NewStringUFT將char*轉換成jstring

最後就是去呼叫,這就簡單了。

Hello jni = new Hello();
String result = jni.sayHello("I am from Java");
Log.d(TAG, result);
複製程式碼
2、字串比較

有了上文的介紹,這個比較就比較簡單,核心就是使用strcmp方法,Java程式碼如下:

public class Hello {
    static {
        System.loadLibrary("Hello");
    }

    //如果是c中要求的就返回200,否則就返回400
    public native int checkStr(String str);

}
複製程式碼

c程式碼如下:

jint Java_net_arvin_androidstudy_jni_Hello_checkStr
        (JNIEnv *env, jobject instance, jstring jstr) {
    char *input = (char *) (*env)->GetStringUTFChars(env, jstr, JNI_FALSE);
    char *real = "123456";
    return strcmp(input, real) == 0 ? 200 : 400;
}
複製程式碼

這裡就不接著介紹其他的處理方法了,需要時可以自己搜一下。

處理陣列

同樣有了上文的基礎,Java程式碼如下:

public class Hello {
    static {
        System.loadLibrary("Hello");
    }

    public native void increaseArray(int[] arr);

}
複製程式碼

C程式碼如下:

void Java_net_arvin_androidstudy_jni_Hello_increaseArray
        (JNIEnv *env, jobject instance, jintArray arr) {
    jsize length = (*env)->GetArrayLength(env, arr);
    jint *elements = (*env)->GetIntArrayElements(env, arr, JNI_FALSE);
    for (int i = 0; i < length; i++) {
        elements[i] += 10;
    }
    (*env)->ReleaseIntArrayElements(env, arr, elements, 0);
}
複製程式碼

可以看到:

  • GetArrayLength:獲取陣列長度
  • GetIntArrayElements:從java陣列獲取陣列指標,注意JNI_FALSE這個引數,程式碼是否複製一份,false表示不復制,直接使用java陣列的記憶體地址
  • for迴圈,每個陣列元素都加10
  • 最後釋放本地陣列記憶體,最後一個引數,0表示將值修改到java陣列中,然後釋放本地陣列,這個引數還有兩個可選值:JNI_COMMIT和JNI_ABORT,前一個修改值到java陣列,但是不釋放本地陣列記憶體,後一個,不修改值到java陣列,但是會釋放本地陣列記憶體。

到這裡Java呼叫C的介紹就到這裡,方法基本介紹了,但是如何更好的運用還需努力實踐。

C呼叫Java

上文中說到這個操作,主要是利用反射,這樣就能呼叫Java程式碼了。

對於配置都不說了,也直接上程式碼,主要的細節都是在反射那裡。

先來一個C呼叫Java無參無返回值的函式,Java程式碼如下:

public class CallJava {
    static {
        System.loadLibrary("Hello");
    }

    private static final String TAG = "CallJava";

    //呼叫無參,無返回函式
    public native void callVoid();

    public void hello() {
        Log.d(TAG, "Java的hello方法");
    }
}
複製程式碼

可以看到這裡換了一個類了,但是沒有影響,之後會介紹這一塊知識。

C程式碼:

//呼叫public void hello()方法
void Java_net_arvin_androidstudy_jni_CallJava_callVoid
        (JNIEnv *env, jobject instance) {
    jclass clazz = (*env)->FindClass(env, "net/arvin/androidstudy/jni/CallJava");
    jmethodID method = (*env)->GetMethodID(env, clazz, "hello", "()V");
    jobject object = (*env)->AllocObject(env, clazz);
    (*env)->CallVoidMethod(env, object, method);
}
複製程式碼

這個就是四部曲:

  • 獲取Java中的class
  • 獲取對應的函式
  • 例項化該class對應的例項
  • 呼叫方法
獲取Java中的class

第一步:使用FindClass方法,第二個引數,就是要呼叫的函式的類的完全限定名,但是需要把點換成/

獲取對應的函式

第二步:使用GetMethodID方法,第二個引數就是剛得到的類的class,第三個就是方法名,第四個就是該函式的簽名,這裡有個技巧,使用javap -s 類的完全限定名就能得到該函式的簽名,但是需要在build->intermediates->classes->debug目錄下,使用該命令,得到如下結果:

//else method...

public void hello();
    descriptor: ()V
複製程式碼

descriptor:後邊的就是該方法的簽名

例項化該class對應的例項

第三步:使用AllocObject方法,使用clazz建立該class的例項。

呼叫方法

第四步:使用CallVoidMethod方法,可以看到這個就是呼叫返回為void的方法,第二個引數就是第三步中建立的例項,第三個引數就是上邊建立的要呼叫的方法。

有了這個四部就能在C中吊起Java中的程式碼了。

而對於有參,有返回的方法,在這四部曲的基礎上,只需要修改第二步獲取方法的名字和簽名,其中籤名以及第四步的CallMethod方法,Type可以是int,string,boolean,float等等。

提示:對於基本型別又個技巧,括號內依次是引數的型別的縮寫,括號右邊是返回型別的縮寫,用得多了就可以不用每次都去使用命令查詢了,但是開始最好還是都查一下,免得出錯

但是對於靜態方法的呼叫就應該使用GetStaticMethodIDCallStaticVoidMethod了,而對於靜態方法就不需要例項化物件,相對來說還少一步。

到這裡,可能有使用過java的反射的同學有疑問了,如果是去呼叫private的方法,會不會報錯呢,這個可以告訴你,我試過了,也是可以呼叫起來的,沒有問題,不用擔心啦。

到這裡,Java呼叫C,C呼叫Java基本就算是完成了,這個程式碼我也會上傳到github上,需要的同學可以自行下載比對,有不足之處也請多多指教。地址在文末。

新增多個C檔案的配置

前文中說了,對於多檔案的配置會在之後的文章中說到,果然,在第二篇中,想著方法太多了,我想放到別的檔案中去處理,避免混亂了,所以就去了解了一下,在此告訴大家,其實很簡答。

首先,在之前的配置基礎上,再在cpp目錄下建立一個檔案,例如這裡叫做Test.c,然後再到CMakeLists.txt檔案中關聯上就行了,關聯方式如下:

cmake_minimum_required(VERSION 3.4.1)

add_library(Hello
            SHARED
            src/main/cpp/Hello.c
            src/main/cpp/Test.c)
複製程式碼

對比之前的配置,對了一行src/main/cpp/Test.c相當於把Test.c檔案也關聯到叫做Hello的這個lib中。

雖然現在c程式碼也可以除錯debug了,但是還是有列印日誌才方便,printf是沒有用的,所以需要我們手動去新增一個日誌庫,首先在CMakeLists.txt中新增成如下:

cmake_minimum_required(VERSION 3.4.1)

add_library(Hello
            SHARED
            src/main/cpp/Hello.c
            src/main/cpp/Test.c)

find_library(log-lib log)

target_link_libraries(Hello ${log-lib})
複製程式碼

多了後兩句程式碼。然後再需要用到的地方申明:

#include "android/log.h"

#define LOG_TAG "JNI_TEST"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
複製程式碼

這樣就能在這個類中使用了:

  • LOGD:debug級別日誌
  • LOGI:info級別日誌
  • LOGE:error級別日誌

這裡就有個技巧了,定義一個Log.c檔案,匯入上文中的配置,然後在需要用日誌的地方引入Log.c即可。

這樣就不用在每個檔案開頭都去申明這些東西了。

示例程式碼

Android JNI學習

在這個專案中,java程式碼在包下的jni下,配置也可在相應位置檢視。

感謝

部分程式碼來源尚矽谷Android視訊《JNI》

相關文章