Android JNI/NDK 從入門到進階

在這裏插入圖片描述
好久沒發文章了,這篇文章是是10月底開始計劃的,轉眼到現在12月都快過一半了,我太難了……,不過好在終於完成了,今晚必須去喫宵夜。深圳北,往北兩公里的**燒烤,有木有人過來?我請客,沒有到時候我再來問一遍。

先看目錄,各位覺得內容對你有用再繼續往下看,畢竟顯示有一萬多個字呢,怕沒用的話耽誤大家寶貴的時間。

閒聊一下爲什麼寫這篇文章?

之前寫過一篇關於C代碼生成和調試so庫的文章。前段時間在繼承一個音頻檢測庫的時候出現了點問題,又複習了下JNI部分,順便整理成文,分享給大家。

文章目標和期望

本文是一個 NDK/JNI 系列基礎到進階教程,目標是希望觀看這篇文章的朋友們能對Android中使用C/C++代碼,集成C/C++庫有一個比較基本的瞭解,並且能巧妙的應用到項目中。

好了,說完目的,咱們一如既往,學JNI之前,先來個給自己提幾個問題:

學前三問?

瞭解是什麼?用來做什麼?以及爲什麼?

什麼是JNI/NDK?二者的區別是什麼?

什麼是JNI?

JNI,全名 Java Native Interface,是Java本地接口,JNI是Java調用Native 語言的一種特性,通過JNI可以使得Java與C/C++機型交互。簡單點說就是JNI是Java中調用C/C++的統稱。

什麼是NDK?

NDK 全名Native Develop Kit,官方說法:Android NDK 是一套允許您使用 C 和 C++ 等語言,以原生代碼實現部分應用的工具集。在開發某些類型的應用時,這有助於您重複使用以這些語言編寫的代碼庫。

JNI和NDK都是調用C/C++代碼庫。所以總體來說,除了應用場景不一樣,其他沒有太大區別。細微的區別就是:JNI可以在Java和Android中同時使用,NDK只能在Android裏面使用。

好了,講了是什麼之後,咱們來了解下JNI/NDK到底有什麼用呢?

JNI/NDK用來做什麼?

一句話,快速調用C/C++的動態庫。除了調用C/C++之外別無它用。

就是這麼簡單好吧。知道做什麼之後,咱們學這玩意有啥用呢?

學JNI/NDK能給我帶來什麼好處?

暫時能想到的兩個點,一個是能讓我在開發中愉快的使用C/C++庫,第二個就是能在安全攻防這一塊有更深入的瞭解。其實無論這兩個點中的哪個點都能讓我有足夠動力學下去。所以,想啥呢,搞定他。

JNI/NDK如何使用?

如何配置JNI/NDK環境?

配置NDK的環境比較簡單。我們可以通過簡單三步來實現:

  • 第一步:下載NDK。可以在Google官方下載,也可以直接打開AS進行下載,建議選後者。這裏可以將LLDB和CMake也下載上。
  • 第二步:配置NDK路徑,可以直接在AS裏面進行配置,方便快捷。
  • 第三步: 打開控制檯,cd到NDK的指定目錄下,驗證NDK環境是否成功。

ok,驗證如上圖所示說明你NDK配置成功了。so easy。

HelloWorld一起進入C/C++的世界

現在開始,咱們一起進入HelloWorld的世界。我們一起來通過AS創建一個Native C++項目。主要步驟如下:

  • 第一步:File --> New --> New Project 滑動到選框底部,選中Native C++,點擊下一步。
  • 第二步:選個名字,然後一直點Next,直到Finish完成。

簡單通俗易懂有木有?好了,項目創建成功,運行,看界面,顯示Hello World,項目創建成功。

如何在Android中調用C/C++代碼?

從上面新建的項目中我們看到一個cpp目錄,我們所寫的C/C++代碼就這這個目錄下面。其中會發現有一個名爲native-lib.cpp的文件,這就是用C/C++賦值Hello World的地方。

Android 中調用C/C++庫的步驟:

  • 第一步:通過System.loadLibrary引入C代碼庫名。
  • 第二步:在cpp目錄下的natice-lib.cpp中編寫C/C++代碼。
  • 第二步:調用C/C++文件中對應的實現方法即可。

Hello World Demo的代碼:

Android代碼:

public class MainActivity extends AppCompatActivity {

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    public native String stringFromJNI();
}

natice-lib.cpp代碼:

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_testndk_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

ok,我們現在調用是調用通了,但是我們要在JNI中生成對象實例,調用對應方法,操作對應屬性,我們應該怎麼做呢?OK,接下來要講的內容將解答這些問題,咱們一起來學習下JNI/NDK中的API。

JNI/NDK的API

在C/C++本地代碼中訪問Java端的代碼,一個常見的應用就是獲取類的屬性和調用類的方法,爲了在C/C++中表示屬性和方法,JNI在jni.h頭文件中定義了jfieldID,jmethodID類型來分別代表Java端的屬性和方法。在訪問或者設置Java屬性的時候,首先就要先在本地代碼取得代表該Java屬性的jfeldID,然後才能在本地代碼中進行Java屬性操作,同樣,需要調用Java端的方法時,也是需要取得代表該方法的jmethodID才能進行Java方法調用。

接下來,咱們來嘗試下如何在native中調用Java中的方法。先看下兩個常見的類型:

JNIEnv 類型和jobject類型

在上面的native-lib.cpp中,我們看到getCarName方法中有兩個參數,分別是JNIEnv *env,一個是jobjet instance。簡單介紹下這兩個類型的作用。

JNIEnv 類型

JNIEnv類型實際上代表了Java環境,通過JNIEnv*指針就可以對Java端的代碼進行操作。比如我們可以使用JNIEnv來創建Java類中的對象,調用Java對象的方法,獲取Java對象中的屬性等。

JNIEnv類中有很多函數可以用,如下所示:

  • NewObject: 創建Java類中的對象。
  • NewString: 創建Java類中的String對象。
  • NewArray: 創建類型爲Type的數組對象。
  • GetField: 獲取類型爲Type的字段。
  • SetField: 設置類型爲Type的字段的值。
  • GetStaticField: 獲取類型爲Type的static的字段。
  • SetStaticField: 設置類型爲Type的static的字段的值。
  • CallMethod: 調用返回類型爲Type的方法。
  • CallStaticMethod: 調用返回值類型爲Type的static 方法。
    當然,除了這些常用的函數方法外,還有更多可以使用的函數,可以在jni.h文件中進行查看,或者參考https://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/jniTOC.html鏈接去查詢相關方法,上面都說得特別清楚。

好了,說完JNIEnv,接下來我們講第二個 jobject。

jobject 類型

jobject可以看做是java中的類實例的引用。當然,情況不同,意義也不一樣。

如果native方法不是static, obj 就代表native方法的類實例。

如果native方法是static, obj就代表native方法的類的class 對象實例(static 方法不需要類實例的,所以就代表這個類的class對象)。

舉一個簡單的例子:我們在TestJNIBean中創建一個靜態方法testStaticCallMethod和非靜態方法testCallMethod,我們看在cpp文件中該如何編寫?

TestJNIBean的代碼:

public class TestJNIBean{
    public static final String LOGO = "learn android with aserbao";
    static {
        System.loadLibrary("native-lib");
    }
    public native String testCallMethod();  //非靜態

    public static native String testStaticCallMethod();//靜態
    
    public  String describe(){
        return LOGO + "非靜態方法";
    }
    
    public static String staticDescribe(){
        return LOGO + "靜態方法";
    }
}

cpp文件中實現:

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_androidndk_TestJNIBean_testCallMethod(JNIEnv *env, jobject instance) {
    jclass  a_class = env->GetObjectClass(instance);                                   //因爲是非靜態的,所以要通過GetObjectClass獲取對象
    jmethodID  a_method = env->GetMethodID(a_class,"describe","()Ljava/lang/String;");// 通過GetMethod方法獲取方法的methodId.
    jobject jobj = env->AllocObject(a_class);                                         // 對jclass進行實例,相當於java中的new
    jstring pring= (jstring)(env)->CallObjectMethod(jobj,a_method);                 // 類調用類中的方法
    char *print=(char*)(env)->GetStringUTFChars(pring,0);                           // 轉換格式輸出。 
    return env->NewStringUTF(print);
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_androidndk_TestJNIBean_testStaticCallMethod(JNIEnv *env, jclass type) {
    jmethodID  a_method = env->GetMethodID(type,"describe","()Ljava/lang/String;"); // 通過GetMethod方法獲取方法的methodId.
    jobject jobj = env->AllocObject(type);                                          // 對jclass進行實例,相當於java中的new
    jstring pring= (jstring)(env)->CallObjectMethod(jobj,a_method);                 // 類調用類中的方法
    char *print=(char*)(env)->GetStringUTFChars(pring,0);                           // 轉換格式輸出。
    return env->NewStringUTF(print);
}

上面的兩個方法最大的區別就是靜態方法會直接傳入jclass,從而我們可以省去獲取jclass這一步,而非靜態方法傳入的是當前類

ok,接下來簡單講一下Java中類型和native中類型映射關係。

Java 類型和native中的類型映射關係

Java類型 本地類型 JNI定義的別名
int long jint/jsize
short short jshort
long _int64 jlong
float float jfloat
byte signed char jbyte
double double jdouble
boolean unsigned char jboolean
Object _jobject* jobject
char unsigned short jchar

這些後面我們在使用的時候也會講到。好了,講了這麼多基礎,也講了Android中對C/C++庫的基本調用。方便快捷的。直接調用native的方法就可以了。但是大部分情況下,我們需要在C/C++代碼中對Java代碼進行相應的操作以達到我們的加密或者方法調用的目的。這時候該怎麼辦呢?不急,咱們接下來就將如何在C/C++中調用Java代碼。

如何獲取Java中的類並生成對象

JNIEnv類中有如下幾個方法可以獲取java中的類:

  • jclass FindClass(const char* name) 根據類名來查找一個類,完整類名

需要我們注意的是,FindClass方法參數name是某個類的完整路徑。比如我們要調用Java中的Date類的getTime方法,那麼我們就可以這麼做:

extern "C"
JNIEXPORT jlong JNICALL
Java_com_example_androidndk_TestJNIBean_testNewJavaDate(JNIEnv *env, jobject instance) {
    jclass  class_date = env->FindClass("java/util/Date");//注意這裏路徑要換成/,不然會報illegal class name
    jmethodID  a_method = env->GetMethodID(class_date,"<init>","()V");
    jobject  a_date_obj = env->NewObject(class_date,a_method);
    jmethodID  date_get_time = env->GetMethodID(class_date,"getTime","()J");
    jlong get_time = env->CallLongMethod(a_date_obj,date_get_time);
    return get_time;
}
  • jclass GetObjectClass(jobject obj) 根據一個對象,獲取該對象的類

這個方法比較好理解,根據上面我們講的根據jobject的類型,我們在JNI中寫方法的時候如果是非靜態的都會傳一個jobject的對象。我們可以根據傳入的來獲取當前對象的類。代碼如下:

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_androidndk_TestJNIBean_testCallMethod(JNIEnv *env, jobject instance) {
    jclass  a_class = env->GetObjectClass(instance);//這裏的a_class就是通過instance獲取到的
    ……
}
  • jclass GetSuperClass(jclass obj) 獲取一個傳入的對象獲取他的父類的jclass。

好了,我們知道怎麼通過JNIEnv中獲取Java中的類,接下來我們來學習如何獲取並調用Java中的方法。

如何在C/C++中調用Java方法?

在JNIEnv環境下,我們有如下兩種方法可以獲取方法和屬性:

  • GetMethodID: 獲取非靜態方法的ID;
  • GetStaticMethodID: 獲取靜態方法的ID;
    來取得相應的jmethodID。

GetMethodID方法如下:

  jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)

方法的參數說明:

  • clazz: 這個方法依賴的類對象的class對象。
  • name: 這個字段的名稱。
  • sign: 這個字段的簽名(每個變量,每個方法都有對應的簽名)。

舉一個小例子,比如我們要在JNI中調用TestJNIBean中的describe方法,我們可以這樣做。

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_androidndk_TestJNIBean_testStaticCallMethod(JNIEnv *env, jclass type) {
    jmethodID  a_method = env->GetMethodID(type,"describe","()Ljava/lang/String;"); // 通過GetMethod方法獲取方法的methodId.
    jobject jobj = env->AllocObject(type);                                          // 對jclass進行實例,相當於java中的new
    jstring pring= (jstring)(env)->CallObjectMethod(jobj,a_method);                 // 類調用類中的方法
    char *print=(char*)(env)->GetStringUTFChars(pring,0);                           // 轉換格式輸出。
    return env->NewStringUTF(print);
}

GetStaticMethodID的方法和GetMoehodID相同,只是用來獲取靜態方法的ID而已。同樣,我們在cpp文件中調用TestJNiBean中的staticDescribe方法,代碼如下:


extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_androidndk_TestJNIBean_testStaticCallStaticMethod(JNIEnv *env, jclass type) {
    jmethodID  a_method = env->GetStaticMethodID(type,"staticDescribe","()Ljava/lang/String;"); // 通過GetStaticMethodID方法獲取方法的methodId.
    jstring pring= (jstring)(env)->CallStaticObjectMethod(type,a_method);                       // 類調用類中的方法
    char *print=(char*)(env)->GetStringUTFChars(pring,0);                                       // 轉換格式輸出。
    return env->NewStringUTF(print);
}

上面的調用其實很好區別,和我們平常在Java中使用一致,當時靜態的只需要傳個jclass對象即可調用靜態方法,非靜態方法則需要實例化之後再調用。

如何在C/C++中調用父類的方法?

針對多態情況,咱們如何準確調用我們想要的方法呢?舉一個例子,我有個Father類,裏面有個toString方法,然後Child 繼承Father並重寫toString方法,這時候我們如何在JNIEnv環境中分別調用Father和Child的toString呢?

代碼實現如下:

public class Father {
    public String toString(){
        return "調用的父類中的方法";
    }
}

public class Child extends Father {
    @Override
    public String toString(){
        return "調用的子類中的方法";
    }
}


public class TestJNIBean{
    static {
        System.loadLibrary("native-lib");
    }
    public Father father = new Child();
    public native String testCallFatherMethod(); //調用父類toString方法
    public native String testCallChildMethod(); // 調用子類toString方法
}

cpp中代碼實現:

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_androidndk_TestJNIBean_testCallFatherMethod(JNIEnv *env, jobject instance) {
    jclass clazz = env -> GetObjectClass(instance);
    jfieldID  father_field = env -> GetFieldID(clazz,"father","Lcom/example/androidndk/Father;");
    jobject  mFather = env -> GetObjectField(instance,father_field);
    jclass  clazz_father = env -> FindClass("com/example/androidndk/Father");
    jmethodID  use_call_non_virtual = env -> GetMethodID(clazz_father,"toString","()Ljava/lang/String;");
    // 如果調用父類方法用CallNonvirtual***Method
    jstring  result = (jstring) env->CallNonvirtualObjectMethod(mFather,clazz_father,use_call_non_virtual);
    return result;
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_androidndk_TestJNIBean_testCallChildMethod(JNIEnv *env, jobject instance) {
    jclass clazz = env -> GetObjectClass(instance);
    jfieldID  father_field = env -> GetFieldID(clazz,"father","Lcom/example/androidndk/Father;");
    jobject  mFather = env -> GetObjectField(instance,father_field);
    jclass  clazz_father = env -> FindClass("com/example/androidndk/Father");
    jmethodID  use_call_non_virtual = env -> GetMethodID(clazz_father,"toString","()Ljava/lang/String;");
    // 如果調用父類方法用Call***Method
    jstring  result = (jstring) env->CallObjectMethod(mFather,use_call_non_virtual);
    return result;
}

分別調用運行testCallFatherMethod和testCallChildMethod後的輸出結果爲:

調用的父類中的方法
調用的子類中的方法

從上面的例子我們也可以看出,JNIEnv中調用父類和子類方法的唯一區別在於調用方法時,當調用父類的方法時使用CallNonvirtual***Method,而調用子類方法時則是直接使用Call***Method。

好了,現在我們已經理清了JNIEnv中如何運用多態。現在咱們來了解下如何修改Java變量。

如何在C/C++中修改Java變量?

修改Java中對應的變量思路其實也很簡單。

  • 找到對應的類對象。
  • 找到類中的需要修改的屬性
  • 重新給類中屬性賦值

代碼如下:

public class TestJNIBean{
    static {
        System.loadLibrary("native-lib");
    }
     public int modelNumber = 1;
    /**
     * 修改modelNumber屬性
     */
    public native void testChangeField();
}

/*
 * 修改屬性
 */
extern "C"
JNIEXPORT void JNICALL
Java_com_example_androidndk_TestJNIBean_testChangeField(JNIEnv *env, jobject instance) {
    jclass  a_class = env->GetObjectClass(instance);                // 獲取當前對象的類
    jfieldID  a_field = env->GetFieldID(a_class,"modelNumber","I"); // 提取類中的屬性
    env->SetIntField(instance,a_field,100);                         // 重新給屬性賦值
}

調用testChangeField()方法後,TestJNIBean中的modelNumber將會修改爲100。

如何在C/C++中操作Java字符串?

  1. Java 中字符串和C/C++中字符創的區別在於:Java中String對象是Unicode的時候,無論是中文,字母,還是標點符號,都是一個字符佔兩個字節的。

JNIEnv中獲取字符串的一些方法:

  • jstring NewString(const jchar* unicodeChars, jsize len):生成jstring對象,將(Unicode)char數組換成jstring對象。比如下面這樣:
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_androidndk_TestJNIBean_testNewString(JNIEnv *env, jclass type) {
    jchar* data = new jchar[7];
    data[0] = 'a';
    data[1] = 's';
    data[2] = 'e';
    data[3] = 'r';
    data[4] = 'b';
    data[5] = 'a';
    data[6] = '0';
    return env->NewString(data, 5);
}
  • jstring NewStringUTF(const char* bytes):利用(UTF-8)char數組生成並返回 java String對象。操作如下:
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_androidndk_TestJNIBean_testNewStringUTF(JNIEnv *env, jclass type) {
    std::string learn="learn android from aserbao";
    return env->NewStringUTF(learn.c_str());//c_str()函數返回一個指向正規C字符串的指針, 內容與本string串相同.
}
  • jsize GetStringLength(jstring jmsg):獲取字符串(Unicode)的長度。
  • jsize GetStringUTFLength(jstring string): 獲取字符串((UTF-8))的長度。
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_androidndk_TestJNIBean_testStringLength(JNIEnv *env, jclass type,
                                                         jstring inputString_) {
    jint result = env -> GetStringLength(inputString_);
    jint resultUTF = env -> GetStringUTFLength(inputString_);
    return result;
}
  • void GetStringRegion(jstring str, jsize start, jsize len, jchar* buf):拷貝Java字符串並以UTF-8編碼傳入jstr。
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_androidndk_TestJNIBean_testGetStringRegion(JNIEnv *env, jclass type,
                                                            jstring inputString_) {
    jint length = env -> GetStringUTFLength(inputString_);
    jint half = length /2;
    jchar* chars = new jchar[half];
    env -> GetStringRegion(inputString_,0,length/2,chars);
    return env->NewString(chars,half);
}

  • void GetStringUTFRegion(jstring str, jsize start, jsize len, char* buf):拷貝Java字符串並以UTF-16編碼傳入jstr
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_androidndk_TestJNIBean_testGetStringUTFRegion(JNIEnv *env, jclass type,
                                                               jstring inputString_) {
    jint length = env -> GetStringUTFLength(inputString_);
    jint half = length /2;
    char* chars = new char[half];
    env -> GetStringUTFRegion(inputString_,0,length/2,chars);
    return env->NewStringUTF(chars);
}
  • jchar* GetStringChars(jstring string, jboolean* isCopy):將jstring對象轉成jchar字符串指針。此方法返回的jchar是一個UTF-16編碼的寬字符串。

    注意:返回的指針可能指向 java String 對象,也可能是指向 jni 中的拷貝,參數 isCopy 用於返回是否是拷貝,如果isCopy參數設置的是NUll,則不會關心是否對Java的String對象進行拷貝。返回值是用 const修飾的,所以獲取的(Unicode)char數組是不能被更改的;還有注意在使用完了之後要對內存進行釋放,釋放方法是:ReleaseStringChars(jstring string, const jchar* chars)。

  • char* GetStringUTFChars(jstring string, jboolean* isCopy):將jstring對象轉成jchar字符串指針。方法返回的jchar是一個UTF-8編碼的字符串。

    返回指針同樣可能指向 java String對象。取決與isCopy的值。返回值是const修飾,不支持修改。使用完了也需釋放,釋放的方法爲:ReleaseStringUTFChars(jstring string, const char* utf)。

  • const jchar* GetStringCritical(jstring string, jboolean* isCopy):將jstring轉換成const jchar*。他和GetStringChars/GetStringUTF的區別在於GetStringCritical更傾向於獲取 java String 的指針,而不是進行拷貝;

    對應的釋放方法:ReleaseStringCritical(jstring string, const jchar* carray)。

    特別注意的是,在GetStringCritical調用和ReleaseStringCritical釋放這兩個方法調用的之間是一個關鍵區,不能調用其他JNI函數。否則將造成關鍵區代碼執行期間垃圾回收器停止運作,任何觸發垃圾回收器的線程也會暫停,其他的觸發垃圾回收器的線程不能前進直到當前線程結束而激活垃圾回收器。就是說在關鍵區域中千萬不要出現中斷操作,或在JVM中分配任何新對象;否則會
    造成JVM死鎖。

如何在C/C++中操作Java數組?

  • jType* GetArrayElements((Array array, jboolean* isCopy)):這類方法可以把Java的基本類型數組轉換成C/C++中的數組。isCopy爲true的時候表示數據會拷貝一份,返回的數據的指針是副本的指針。如果false則不會拷貝,直接使用Java數據的指針。不適用isCopy可以傳NULL或者0。
  • void ReleaseArrayElements(jTypeArray array, j* elems,jint mode):釋放操作,只要有調用GetArrayElements方法,就必須要調用一次對應的ReleaseArrayElements方法,因爲這樣會刪除掉可能會阻止垃圾回收的JNI本地引用。這裏我們注意以下這個方法的最後一個參數mode,他的作用主要用於避免在處理副本數據的時產生對Java堆不必要的影響。如果GetArrayElements中的isCopy爲true,我們才需要設置mode,爲false我們mode可以不用處理,賦值0。mode有三個值:
    • 0:更新Java堆上的數據並釋放副本使用所佔有的空間。
    • JNI_COMMIT:提交,更新Java堆上的數據,不釋放副本使用的空間。
    • JNI_ABORT:撤銷,不更新Java堆上的數據,釋放副本使用所佔有的空間。
  • void* GetPrimitiveArrayCritical(jarray array, jboolean* isCopy):作用類似與GetArrayElements。這個方法可能會通過VM返回指向原始數組的指針。注意在使用此方法的時候避免死鎖問題。
  • void ReleasePrimitiveArrayCritical(jarray array, void* carray, jint mode):上面方法對應的釋放方法。注意這兩個方法之間不要調用任何JNI的函數方法。因爲可能會導致當前線程阻塞。
  • void GetArrayRegion(JNIEnv *env, ArrayType array,jsize start, jsize len, Type *buf):和GetStringRegion的作用是相似的,事先在C/C++中創建一個緩存區,然後將Java中的原始數組拷貝到緩衝區中去。
  • void SetArrayRegion(JNIEnv *env, ArrayType array,jsize start, jsize len, const Type *buf):上面方法的對應方法,將緩衝區的部分數據設置回Java原始數組中。
  • jsize GetArrayLength(JNIEnv *env, jarray array):獲取數組長度。
  • jobjectArray NewObjectArray(JNIEnv *env, jsize length,jclass elementClass, jobject initialElement):創建指定長度的數組。

通過一個方法來使用下上面方法,代碼如下:

extern "C"
JNIEXPORT void JNICALL
Java_com_example_androidndk_TestJNIBean_testGetTArrayElement(JNIEnv *env, jobject instance) {
    jclass  jclazz = env -> GetObjectClass(instance);
    //獲取Java中數組屬性arrays的id
    jfieldID fid_arrays = env-> GetFieldID(jclazz , "testArrays","[I") ;
    //獲取Java中數組屬性arrays的對象
    jintArray jint_arr = (jintArray) env->GetObjectField(instance, fid_arrays) ;

    //獲取arrays對象的指針
    jint* int_arr = env->GetIntArrayElements(jint_arr, NULL) ;
    //獲取數組的長度
    jsize len = env->GetArrayLength(jint_arr) ;
    LOGD("---------------獲取到的原始數據爲---------------");
    for(int i = 0; i < len; i++){
        LOGD("len %d",int_arr[i]);
    }

    //新建一個jintArray對象
    jintArray jint_arr_temp = env->NewIntArray (len) ;
    //獲取jint_arr_temp對象的指針
    jint* int_arr_temp = env->GetIntArrayElements (jint_arr_temp , NULL) ;
    //計數
    jint count = 0;

    LOGD("---------------打印其中是奇數---------------");
    //奇數數位存入到int_ _arr_ temp內存中
    for (jsize j=0;j<len;j++) {
        jint result = int_arr[j];
        if (result % 2 != 0) {
            int_arr_temp[count++] = result;
        }
    }
    //打印int_ _arr_ temp內存中的數組
    for(int k = 0; k < count; k++){
        LOGD("len %d",int_arr_temp[k]);
    }

    LOGD("---------------打印前兩位---------------");
    //將數組中一段(1-2)數據拷貝到內存中,並且打印出來
    jint* buffer = new jint[len] ;
    //獲取數組中從0開始長度爲2的一段數據值
    env->GetIntArrayRegion(jint_arr,0,2,buffer) ;

    for(int z=0;z<2;z++){
        LOGD("len %d",buffer[ z]);
    }

    LOGD("---------------重新賦值打印---------------");
    //創建一個新的int數組
    jint* buffers = new jint[3];
    jint start = 100;
    for (int n = start; n < 3+start ; ++n) {
        buffers[n-start] = n+1;
    }
    //重新給jint_arr數組中的從第1位開始往後3個數賦值
    env -> SetIntArrayRegion(jint_arr,1,3,buffers);
    //從新獲取數據指針
    int_arr = env -> GetIntArrayElements(jint_arr,NULL);
    for (int i = 0; i < len; ++i) {
        LOGD("重新賦值之後的結果爲 %d",int_arr[i]);
    }

    LOGD("---------------排序---------------");

    std::sort(int_arr,int_arr+len);
    for (int i = 0; i < len; ++i) {
        LOGD("排序結果爲 %d",int_arr[i]);
    }

    LOGD("---------------數據處理完成---------------");

}

運行結果:

D/learn JNI: ---------------獲取到的原始數據爲---------------
D/learn JNI: len 1
D/learn JNI: len 2
D/learn JNI: len 3
D/learn JNI: len 4
D/learn JNI: len 5
D/learn JNI: len 8
D/learn JNI: len 6
D/learn JNI: ---------------打印其中是奇數---------------
D/learn JNI: len 1
D/learn JNI: len 3
D/learn JNI: len 5
D/learn JNI: ---------------打印前兩位---------------
D/learn JNI: len 1
D/learn JNI: len 2
D/learn JNI: ---------------重新賦值打印---------------
D/learn JNI: 重新賦值之後的結果爲 1
D/learn JNI: 重新賦值之後的結果爲 101
D/learn JNI: 重新賦值之後的結果爲 102
D/learn JNI: 重新賦值之後的結果爲 103
D/learn JNI: 重新賦值之後的結果爲 5
D/learn JNI: 重新賦值之後的結果爲 8
D/learn JNI: 重新賦值之後的結果爲 6
D/learn JNI: ---------------排序---------------
D/learn JNI: 排序結果爲 1
D/learn JNI: 排序結果爲 5
D/learn JNI: 排序結果爲 6
D/learn JNI: 排序結果爲 8
D/learn JNI: 排序結果爲 101
D/learn JNI: 排序結果爲 102
D/learn JNI: 排序結果爲 103
D/learn JNI: ---------------數據處理完成---------------

JNI中幾種引用的區別?

從JVM創建的對象傳遞到C/C++代碼時會產生引用,由於Java的垃圾回收機制限制,只要對象有引用存在就不會被回收。所以無論在C/C++中還是Java中我們在使用引用的時候需要特別注意。下面講下C/C++中的引用:

全局引用

全局引用可以跨多個線程,在多個函數中都有效。全局引用需要通過NewGlobalRef方法手動創建,對應的釋放全局引用的方法爲DeleteGlobalRef

局部引用

局部引用很常見,基本上通過JNI函數獲取到的返回引用都算局部引用,局部引用只在單個函數中有效。局部引用會在函數返回時自動釋放,當然我們也可以通過DeleteLocalRef方法手動釋放。

弱引用

弱引用也需要自己手動創建,作用和全局引用的作用相似,不同點在於弱引用不會阻止垃圾回收器對引用所指對象的回收。我們可以通過NewWeakGlobalRef方法來創建弱引用,也可以通過DeleteWeakGlobalRef來釋放對應的弱引用。

小技巧

如何在C/C++中打印日誌?

在Jni中C/C++層打印日誌是幫助我們調試代碼較爲重要的一步。簡單分爲三步:

  • 第一步:在需要打印日誌的文件頭部導入android下的log日誌功能。
#include <android/log.h>
  • 第二步:自定義LOGD標記。(可省略)
#define TAG "learn JNI" // 這個是自定義的LOG的標識
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) // 定義LOGD類型
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) // 定義LOGI類型
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) // 定義LOGW類型
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) // 定義LOGE類型
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__) // 定義LOGF類型
  • 第三步:打印日誌。
LOGE("my name is %s\n", "aserbao");//簡約型
__android_log_print(ANDROID_LOG_INFO, "android", "my name is %s\n", "aserbao"); //如果第二步省略也可以通過這個直接打印日誌。

上面是我們新建項目自動創建的cpp目錄和.cpp文件。如果想自己寫一個該怎麼辦呢?且聽我娓娓道來:

如何通過*.java生成*.cpp?

比如我現在創建一個工具類Car,裏面想寫個native方法叫getCarName(),我們如何快速得到對應的.cpp文件呢?方法也很簡單,我們只需要按步驟運行幾個命令就行了。步驟如下:

  • 第一步:新建工具類Car,寫一個本地靜態方法getCarName()。
public class Car {
    static {
        System.loadLibrary("native-lib");
    }
    public native String getCarName();
}
  • 第二步:到Terimal中cd到Car目錄,運行命令javac -h . Car.java就能在當前目錄得到對應的.h結尾的文件。
aserbao:androidndk aserbao$ cd /Users/aserbao/aserbao/code/code/framework/AndroidNDK/app/src/main/java/com/example/androidndk
aserbao:androidndk aserbao$ javac -h . Car.java
  • 第三步:將.h修改爲natice-lib.cpp並放到cpp目錄下,並在對應方法下修改返回。
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_androidndk_Car_getCarName(JNIEnv *env, jobject instance) {
    std::string hello = "This is a beautiful car";
    return env->NewStringUTF(hello.c_str());
}

我將返回修改爲”This is a beautiful car“,所以運行後我們可以看到hello world C++ 變成了”This is a beautiful car“。大功告成。

如何獲取Java中方法的簽名?

在學習C/C++調用Java代碼之前,我們先講一個小知識點。Java中方法的簽名。不知道大家有沒有了解過,其實Java中每個方法,都有其對應的簽名的。在接下來的調用過程中,我們會多次運用到方法簽名。

首先講一下方法簽名如何獲取?
很簡單,比如上面的對象Car,我們在裏面寫一個toString方法。我們可以首先通過javac命令生成.class文件,然後再通過javap命令來獲取對應的方法簽名,使用方法及結果如下:

javap -s **.class

對應的簽名類型如下:

類型 相應的簽名
boolean Z
float F
byte B
double D
char C
void V
short S
object L用/分割包的完整類名; Ljava/lang/String;
int I
Array [簽名[I [Ljava/lang/Object;
long L
Method (參數類型簽名…)返回值類型簽名

好了,拿到方法簽名了,我們就可以開始在C/C++中來調用Java代碼了。來來來,現在我們一起來學習如何在C/C++中調用Java代碼。

  • .java 生成.class
javac *.java 
  • *.java 生成 *.h
javac -h . *.java
  • 查看*.class中的方法和簽名
javap -s -p *.class

如何在C/C++中處理異常?

異常處理通常我們分爲兩步,捕獲異常和拋出異常。在C/C++中實現這兩步也相當簡單。我們先看幾個函數:

  • ExceptionCheck:檢測是否有異常,有返回JNI_TRUE,否則返回FALSE。
  • ExceptionOccurred:判斷是否有異常,有返回異常,沒有返回NULL。
  • ExceptionClear:清除異常堆棧信息。
  • Throw:拋出當前異常。
  • ThrowNew:創建一個新異常,並自定義異常信息。
  • FatalError:致命錯誤,並且終止當前VM。

代碼實例:

//Java代碼
public class TestJNIBean{
    static {
        System.loadLibrary("native-lib");
    }
    public native void testThrowException();
    private void throwException() throws NullPointerException{
        throw new NullPointerException("this is an NullPointerException");
    }
}

//JNI代碼
extern "C"
JNIEXPORT void JNICALL
Java_com_example_androidndk_TestJNIBean_testThrowException(JNIEnv *env, jobject instance) {

    jclass jclazz = env -> GetObjectClass(instance);
    jmethodID  throwExc = env -> GetMethodID(jclazz,"throwException","()V");
    if (throwExc == NULL) return;
    env -> CallVoidMethod(instance,throwExc);
    jthrowable excOcc = env -> ExceptionOccurred();
    if (excOcc){
        jclass  newExcCls ;
        env -> ExceptionDescribe();//打印異常堆棧信息
        env -> ExceptionClear();
        jclass newExcClazz = env -> FindClass("java/lang/IllegalArgumentException");
        if (newExcClazz == NULL) return;
        env -> ThrowNew(newExcClazz,"this is a IllegalArgumentException");
    }
}

運行結果:

12-05 15:20:27.547 8077-8077/com.example.androidndk E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.androidndk, PID: 8077
    java.lang.IllegalArgumentException: this is a IllegalArgumentException
        at com.example.androidndk.TestJNIBean.testThrowException(Native Method)
        at com.example.androidndk.MainActivity.itemClickBack(MainActivity.java:90)
        at com.example.androidndk.base.viewHolder.BaseClickViewHolder$1.onClick(BaseClickViewHolder.java:32)
        at android.view.View.performClick(View.java:5198)
        at android.view.View$PerformClick.run(View.java:21147)
        at android.os.Handler.handleCallback(Handler.java:739)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:148)
        at android.app.ActivityThread.main(ActivityThread.java:5417)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
    --------- beginning of system

項目地址

本來想這將這個項目也放到AserbaoAndroid裏面的,後來又偷懶,新建了個項目,整篇文章的源碼存放地址在:https://github.com/aserbao/AndroidNDK。

參考文章及鏈接

文章總結

這篇文章從開始動筆到最後完工差不多斷斷續續一個多月時間了,轉眼都快過年了,目測這是年前最後一篇,原本計劃想着將so的相關知識點也寫到這篇文章裏面,後面由於多方面考慮就改變主意了,關於so的相關知識會重新出一篇較詳細的文章。

這篇文章講的還是學習JNI中必備的一些東西,希望對大家有用吧,後期有時間再出第二篇關於C/C++庫的接入和使用吧。

最後,還是那句老話,如果大家在開發Android中有遇到我寫過文章中的問題,可以在我公衆號「aserbaocool」給我留言,知無不言,同時也歡迎大家來加入Android交流羣。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章