Android JNI開發系列之Java與C相互調用

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

沒有配置過的能夠去看看Android JNI開發系列之配置android

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

1、Java調用C

第一篇中有個簡單的例子,就是使用Java調用C,調用一個無參的native函數,並返回一個String,下面接着說點更多的狀況:github

  • 基本類型對應狀況
  • 字符串處理
  • 數組的處理

基本類型對應狀況

由於Java和C的基本類型也有些許區別,而在這二者之間還有一個jni的類型做爲橋樑鏈接轉換類型,有一張圖特別好,一看就清楚了,借了一下這位做者文章中的圖,表示感謝。數組

type_relationship.png

下邊對於數據的處理就是基於這些類型去處理的。bash

字符串的處理

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

這個也是坑了我這個萌新很多,體會到其實Java的垃圾回收機制仍是很方便的。函數

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

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

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

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

接着在Hello.c文件中寫這個方法,這裏有兩種方法去寫這個方法,第一種是手動本身寫,也有點技巧:ui

  • 首先看到返回的是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);
複製代碼
二、字符串比較

有了上文的介紹,這個比較就比較簡單,核心就是使用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》

相關文章
相關標籤/搜索