這是這個系列的第二篇肴盏,第一篇介紹了如何配置烛愧。這一篇介紹Java與C如何相互介紹摘能。
沒有配置過的可以去看看Android JNI開發(fā)系列之配置
首先介紹的就是Java如何調(diào)用C昂勉,而C調(diào)用Java核心使用的就是反射脂凶,下面會以此介紹宪睹。
一、Java調(diào)用C
第一篇中有個簡單的例子蚕钦,就是使用Java調(diào)用C亭病,調(diào)用一個無參的native函數(shù),并返回一個String嘶居,下面接著說點更多的情況:
- 基本類型對應(yīng)情況
- 字符串處理
- 數(shù)組的處理
基本類型對應(yīng)情況
因為Java和C的基本類型也有些許區(qū)別罪帖,而在這兩者之間還有一個jni的類型作為橋梁連接轉(zhuǎn)換類型,有一張圖特別好邮屁,一看就清楚了整袁,借了一下這位作者文章中的圖,表示感謝佑吝。
下邊對于數(shù)據(jù)的處理就是基于這些類型去處理的坐昙。
字符串的處理
1、首先先來一個字符串的拼接
這個也是坑了我這個萌新不少芋忿,體會到其實Java的垃圾回收機(jī)制還是很方便的炸客。
其中在c中字符串的拼接主要就是使用strcat
方法,導(dǎo)入#include<string.h>
包盗飒。
還是老樣子嚷量,先定義一個native方法,對于配置都是在上一篇的基礎(chǔ)上的:
public class Hello {
static {
System.loadLibrary("Hello");
}
//傳入一個字符串逆趣,拼接一段字符串后返回
public native String sayHello(String msg);
}
接著在Hello.c文件中寫這個方法蝶溶,這里有兩種方法去寫這個方法,第一種是手動自己寫宣渗,也有點技巧:
- 首先看到返回的是String抖所,對應(yīng)的就是jstring
- 然后函數(shù)名就是:Java_類完全限定名_方法名,其中完全限定名痕囱,可以在Hello這個類上右鍵->Copy Reference田轧,然后再把名字中間的點改為下劃線。
- 然后函數(shù)的參數(shù):前兩個參數(shù)必須的鞍恢,
JNIEnv *env, jobject instance
属百,然后第三個參數(shù)開始就是在Java中定義的方法的參數(shù),這里傳入了一個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關(guān)鍵字都可以去掉的污秆,去掉后就和上邊的方法一樣了劈猪,然后自己去把參數(shù)的名字補(bǔ)充上即可。
最后對于字符串的拼接良拼,沒啥好說的战得,我這里提供一種方式:
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轉(zhuǎn)為char*
- 然后把要拼接的字符串定義出來
- 接著關(guān)鍵來了,動態(tài)申請一塊區(qū)域用于存儲拼接后的字符串庸推,申請的長度就是傳進(jìn)來的字符串和要添加的長度之和
- 接著就是把這兩個字符串拼在一起贡避,先使用strcpy是因為result還沒有初始化,相當(dāng)于把fromJava賦值給result予弧,然后再把fromC拼接到result中
- 最后就是使用NewStringUFT將char*轉(zhuǎn)換成jstring
最后就是去調(diào)用,這就簡單了湖饱。
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;
}
這里就不接著介紹其他的處理方法了仅仆,需要時可以自己搜一下器赞。
處理數(shù)組
同樣有了上文的基礎(chǔ),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:獲取數(shù)組長度
- GetIntArrayElements:從java數(shù)組獲取數(shù)組指針墓拜,注意JNI_FALSE這個參數(shù)港柜,代碼是否復(fù)制一份,false表示不復(fù)制咳榜,直接使用java數(shù)組的內(nèi)存地址
- for循環(huán)夏醉,每個數(shù)組元素都加10
- 最后釋放本地數(shù)組內(nèi)存,最后一個參數(shù)涌韩,0表示將值修改到j(luò)ava數(shù)組中畔柔,然后釋放本地數(shù)組,這個參數(shù)還有兩個可選值:JNI_COMMIT和JNI_ABORT臣樱,前一個修改值到j(luò)ava數(shù)組靶擦,但是不釋放本地數(shù)組內(nèi)存腮考,后一個,不修改值到j(luò)ava數(shù)組玄捕,但是會釋放本地數(shù)組內(nèi)存踩蔚。
到這里Java調(diào)用C的介紹就到這里,方法基本介紹了桩盲,但是如何更好的運(yùn)用還需努力實踐寂纪。
C調(diào)用Java
上文中說到這個操作,主要是利用反射赌结,這樣就能調(diào)用Java代碼了捞蛋。
對于配置都不說了,也直接上代碼柬姚,主要的細(xì)節(jié)都是在反射那里拟杉。
先來一個C調(diào)用Java無參無返回值的函數(shù),Java代碼如下:
public class CallJava {
static {
System.loadLibrary("Hello");
}
private static final String TAG = "CallJava";
//調(diào)用無參量承,無返回函數(shù)
public native void callVoid();
public void hello() {
Log.d(TAG, "Java的hello方法");
}
}
可以看到這里換了一個類了搬设,但是沒有影響,之后會介紹這一塊知識撕捍。
C代碼:
//調(diào)用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
- 獲取對應(yīng)的函數(shù)
- 實例化該class對應(yīng)的實例
- 調(diào)用方法
獲取Java中的class
第一步:使用FindClass
方法拿穴,第二個參數(shù),就是要調(diào)用的函數(shù)的類的完全限定名忧风,但是需要把點換成/
獲取對應(yīng)的函數(shù)
第二步:使用GetMethodID
方法默色,第二個參數(shù)就是剛得到的類的class,第三個就是方法名狮腿,第四個就是該函數(shù)的簽名腿宰,這里有個技巧,使用javap -s 類的完全限定名
就能得到該函數(shù)的簽名缘厢,但是需要在build->intermediates->classes->debug目錄下吃度,使用該命令,得到如下結(jié)果:
//else method...
public void hello();
descriptor: ()V
descriptor:后邊的就是該方法的簽名
實例化該class對應(yīng)的實例
第三步:使用AllocObject
方法贴硫,使用clazz創(chuàng)建該class的實例椿每。
調(diào)用方法
第四步:使用CallVoidMethod
方法,可以看到這個就是調(diào)用返回為void的方法英遭,第二個參數(shù)就是第三步中創(chuàng)建的實例拖刃,第三個參數(shù)就是上邊創(chuàng)建的要調(diào)用的方法。
有了這個四部就能在C中吊起Java中的代碼了贪绘。
而對于有參兑牡,有返回的方法,在這四部曲的基礎(chǔ)上税灌,只需要修改第二步獲取方法的名字和簽名均函,其中簽名以及第四步的Call<Type>Method方法亿虽,Type可以是int,string苞也,boolean洛勉,float等等。
提示:對于基本類型又個技巧如迟,括號內(nèi)依次是參數(shù)的類型的縮寫收毫,括號右邊是返回類型的縮寫,用得多了就可以不用每次都去使用命令查詢了殷勘,但是開始最好還是都查一下此再,免得出錯
但是對于靜態(tài)方法的調(diào)用就應(yīng)該使用GetStaticMethodID
和CallStaticVoidMethod
了,而對于靜態(tài)方法就不需要實例化對象玲销,相對來說還少一步输拇。
到這里,可能有使用過java的反射的同學(xué)有疑問了贤斜,如果是去調(diào)用private的方法策吠,會不會報錯呢,這個可以告訴你瘩绒,我試過了猴抹,也是可以調(diào)用起來的,沒有問題锁荔,不用擔(dān)心啦洽糟。
到這里,Java調(diào)用C堕战,C調(diào)用Java基本就算是完成了,這個代碼我也會上傳到github上拍霜,需要的同學(xué)可以自行下載比對嘱丢,有不足之處也請多多指教。地址在文末祠饺。
添加多個C文件的配置
前文中說了越驻,對于多文件的配置會在之后的文章中說到,果然道偷,在第二篇中缀旁,想著方法太多了,我想放到別的文件中去處理勺鸦,避免混亂了并巍,所以就去了解了一下,在此告訴大家换途,其實很簡答懊渡。
首先刽射,在之前的配置基礎(chǔ)上,再在cpp目錄下創(chuàng)建一個文件剃执,例如這里叫做Test.c誓禁,然后再到CMakeLists.txt文件中關(guān)聯(lián)上就行了,關(guān)聯(lián)方式如下:
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
相當(dāng)于把Test.c文件也關(guān)聯(lián)到叫做Hello的這個lib中摹恰。
雖然現(xiàn)在c代碼也可以調(diào)試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文件,導(dǎo)入上文中的配置配阵,然后在需要用日志的地方引入Log.c即可馏颂。
這樣就不用在每個文件開頭都去申明這些東西了。
示例代碼
在這個項目中棋傍,java代碼在包下的jni下救拉,配置也可在相應(yīng)位置查看。
感謝
部分代碼來源尚硅谷Android視頻《JNI》