Android JNI/NDK 使用全解

很久沒發文章了,這篇文章是是10月底開始計劃的,轉眼到如今12月都快過一半了,我太難了……,不過好在終於完成了,今晚必須去吃宵夜。深圳北,往北兩千米的**燒烤,有木有人過來?我請客,沒有到時候我再來問一遍。html

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

閒聊一下爲何寫這篇文章?

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

文章目標和指望

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

好了,說完目的,我們一如既往,學JNI以前,先來個給本身提幾個問題:github

學前三問?

瞭解是什麼?用來作什麼?以及爲何?數組

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

什麼是JNI?緩存

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

什麼是NDK?bash

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

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裏面的,後來又偷懶,新建了個項目,整篇文章的源碼存放地址在:github.com/aserbao/And…

參考文章及連接

文章總結

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

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

最後,仍是那句老話,若是你們在開發Android中有遇到我寫過文章中的問題,能夠在我公衆號「aserbaocool」給我留言,知無不言,同時也歡迎你們來加入Android交流羣。

相關文章
相關標籤/搜索