這是這個系列的第二篇,第一篇介紹瞭如何配置。這一篇介紹Java與C如何相互介紹。
沒有配置過的可以去看看Android JNI開發系列之配置
首先介紹的就是Java如何呼叫C,而C呼叫Java核心使用的就是反射,下面會依次介紹。
一、Java呼叫C
第一篇中有個簡單的例子,就是使用Java呼叫C,呼叫一個無參的native函式,並返回一個String,下面接著說點更多的情況:
- 基本型別對應情況
- 字串處理
- 陣列的處理
基本型別對應情況
因為Java和C的基本型別也有些許區別,而在這兩者之間還有一個jni的型別作為橋樑連線轉換型別,有一張圖特別好,一看就清楚了,借了一下這位作者文章中的圖,表示感謝。
下邊對於資料的處理就是基於這些型別去處理的。
字串的處理
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等等。
提示:對於基本型別又個技巧,括號內依次是引數的型別的縮寫,括號右邊是返回型別的縮寫,用得多了就可以不用每次都去使用命令查詢了,但是開始最好還是都查一下,免得出錯
但是對於靜態方法的呼叫就應該使用GetStaticMethodID
和CallStaticVoidMethod
了,而對於靜態方法就不需要例項化物件,相對來說還少一步。
到這裡,可能有使用過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即可。
這樣就不用在每個檔案開頭都去申明這些東西了。
示例程式碼
在這個專案中,java程式碼在包下的jni下,配置也可在相應位置檢視。
感謝
部分程式碼來源尚矽谷Android視訊《JNI》