Android JNI 基礎學習講解

好久沒發(fā)文章了,這篇文章是是10月底開始計劃的忆家,轉眼到現(xiàn)在12月都快過一半了犹菇,我太難了……,不過好在終于完成了芽卿,今晚必須去吃宵夜揭芍。深圳北,往北兩公里的**燒烤卸例,有木有人過來称杨?我請客,沒有到時候我再來問一遍筷转。

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

閑聊一下為什么寫這篇文章?

之前寫過一篇關于C代碼生成和調試so庫的文章袭蝗。前段時間在繼承一個音頻檢測庫的時候出現(xiàn)了點問題唤殴,又復習了下JNI部分,順便整理成文呻袭,分享給大家眨八。

文章目標和期望

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

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

學前三問?

了解是什么没陡?用來做什么涩哟?以及為什么索赏?

什么是JNI/NDK?二者的區(qū)別是什么贴彼?

什么是JNI潜腻?

JNI,全名 Java Native Interface器仗,是Java本地接口融涣,JNI是Java調用Native 語言的一種特性,通過JNI可以使得Java與C/C++機型交互精钮。簡單點說就是JNI是Java中調用C/C++的統(tǒng)稱威鹿。

什么是NDK?

NDK 全名Native Develop Kit轨香,官方說法:Android NDK 是一套允許您使用 C 和 C++ 等語言忽你,以原生代碼實現(xiàn)部分應用的工具集。在開發(fā)某些類型的應用時臂容,這有助于您重復使用以這些語言編寫的代碼庫檀夹。

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

好了沛婴,講了是什么之后吼畏,咱們來了解下JNI/NDK到底有什么用呢?

JNI/NDK用來做什么嘁灯?

一句話泻蚊,快速調用C/C++的動態(tài)庫。除了調用C/C++之外別無它用丑婿。

就是這么簡單好吧性雄。知道做什么之后,咱們學這玩意有啥用呢羹奉?

學JNI/NDK能給我?guī)硎裁春锰帲?/h3>

暫時能想到的兩個點秒旋,一個是能讓我在開發(fā)中愉快的使用C/C++庫,第二個就是能在安全攻防這一塊有更深入的了解诀拭。其實無論這兩個點中的哪個點都能讓我有足夠動力學下去迁筛。所以,想啥呢耕挨,搞定他细卧。

JNI/NDK如何使用尉桩?

如何配置JNI/NDK環(huán)境?

配置NDK的環(huán)境比較簡單贪庙。我們可以通過簡單三步來實現(xiàn):

  • 第一步:下載NDK魄健。可以在Google官方下載插勤,也可以直接打開AS進行下載沽瘦,建議選后者。這里可以將LLDB和CMake也下載上农尖。
  • 第二步:配置NDK路徑析恋,可以直接在AS里面進行配置,方便快捷盛卡。
  • 第三步: 打開控制臺助隧,cd到NDK的指定目錄下,驗證NDK環(huán)境是否成功滑沧。

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

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

現(xiàn)在開始滓技,咱們一起進入HelloWorld的世界哩牍。我們一起來通過AS創(chuàng)建一個Native C++項目。主要步驟如下:

  • 第一步:File --> New --> New Project 滑動到選框底部令漂,選中Native C++,點擊下一步膝昆。
  • 第二步:選個名字,然后一直點Next叠必,直到Finish完成荚孵。

簡單通俗易懂有木有?好了纬朝,項目創(chuàng)建成功收叶,運行,看界面共苛,顯示Hello World判没,項目創(chuàng)建成功。

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

從上面新建的項目中我們看到一個cpp目錄哆致,我們所寫的C/C++代碼就這這個目錄下面。其中會發(fā)現(xiàn)有一個名為native-lib.cpp的文件患膛,這就是用C/C++賦值Hello World的地方。

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

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

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,我們現(xiàn)在調用是調用通了跃捣,但是我們要在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方法中有兩個參數(shù)蒲障,分別是JNIEnv *env,一個是jobjet instance。簡單介紹下這兩個類型的作用瘫证。

JNIEnv 類型

JNIEnv類型實際上代表了Java環(huán)境揉阎,通過JNIEnv*指針就可以對Java端的代碼進行操作。比如我們可以使用JNIEnv來創(chuàng)建Java類中的對象背捌,調用Java對象的方法毙籽,獲取Java對象中的屬性等。

JNIEnv類中有很多函數(shù)可以用毡庆,如下所示:

  • NewObject: 創(chuàng)建Java類中的對象坑赡。
  • NewString: 創(chuàng)建Java類中的String對象。
  • New<Type>Array: 創(chuàng)建類型為Type的數(shù)組對象么抗。
  • Get<Type>Field: 獲取類型為Type的字段毅否。
  • Set<Type>Field: 設置類型為Type的字段的值。
  • GetStatic<Type>Field: 獲取類型為Type的static的字段蝇刀。
  • SetStatic<Type>Field: 設置類型為Type的static的字段的值螟加。
  • Call<Type>Method: 調用返回類型為Type的方法。
  • CallStatic<Type>Method: 調用返回值類型為Type的static 方法。
    當然捆探,除了這些常用的函數(shù)方法外然爆,還有更多可以使用的函數(shù),可以在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中創(chuàng)建一個靜態(tài)方法testStaticCallMethod和非靜態(tài)方法testCallMethod蹦渣,我們看在cpp文件中該如何編寫?

TestJNIBean的代碼:

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

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

cpp文件中實現(xiàn):

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_androidndk_TestJNIBean_testCallMethod(JNIEnv *env, jobject instance) {
    jclass  a_class = env->GetObjectClass(instance);                                   //因為是非靜態(tài)的貌亭,所以要通過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);
}

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

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) 根據(jù)類名來查找一個類茶鹃,完整類名

需要我們注意的是涣雕,F(xiàn)indClass方法參數(shù)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) 根據(jù)一個對象胞谭,獲取該對象的類

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

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環(huán)境下骇钦,我們有如下兩種方法可以獲取方法和屬性:

  • GetMethodID: 獲取非靜態(tài)方法的ID;
  • GetStaticMethodID: 獲取靜態(tài)方法的ID;
    來取得相應的jmethodID宛渐。

GetMethodID方法如下:

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

方法的參數(shù)說明:

  • 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相同仗岸,只是用來獲取靜態(tài)方法的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);
}

上面的調用其實很好區(qū)別业稼,和我們平常在Java中使用一致盗痒,當時靜態(tài)的只需要傳個jclass對象即可調用靜態(tài)方法,非靜態(tài)方法則需要實例化之后再調用盼忌。

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

針對多態(tài)情況,咱們如何準確調用我們想要的方法呢谦纱?舉一個例子看成,我有個Father類,里面有個toString方法跨嘉,然后Child 繼承Father并重寫toString方法川慌,這時候我們如何在JNIEnv環(huán)境中分別調用Father和Child的toString呢?

代碼實現(xiàn)如下:

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中代碼實現(xiàn):

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中調用父類和子類方法的唯一區(qū)別在于調用方法時梦重,當調用父類的方法時使用CallNonvirtual*Method兑燥,而調用子類方法時則是直接使用Call*Method。

好了琴拧,現(xiàn)在我們已經(jīng)理清了JNIEnv中如何運用多態(tài)〗低現(xiàn)在咱們來了解下如何修改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++中字符創(chuàng)的區(qū)別在于:Java中String對象是Unicode的時候锹安,無論是中文短荐,字母,還是標點符號叹哭,都是一個字符占兩個字節(jié)的忍宋。

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

  • jstring NewString(const jchar* unicodeChars, jsize len):生成jstring對象,將(Unicode)char數(shù)組換成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數(shù)組生成并返回 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()函數(shù)返回一個指向正規(guī)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 中的拷貝,參數(shù) isCopy 用于返回是否是拷貝,如果isCopy參數(shù)設置的是NUll,則不會關心是否對Java的String對象進行拷貝研乒。返回值是用 const修飾的汹忠,所以獲取的(Unicode)char數(shù)組是不能被更改的;還有注意在使用完了之后要對內存進行釋放雹熬,釋放方法是: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的區(qū)別在于GetStringCritical更傾向于獲取 java String 的指針诡壁,而不是進行拷貝;

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

    特別注意的是妹卿,在GetStringCritical調用和ReleaseStringCritical釋放這兩個方法調用的之間是一個關鍵區(qū),不能調用其他JNI函數(shù)涨共。否則將造成關鍵區(qū)代碼執(zhí)行期間垃圾回收器停止運作纽帖,任何觸發(fā)垃圾回收器的線程也會暫停宠漩,其他的觸發(fā)垃圾回收器的線程不能前進直到當前線程結束而激活垃圾回收器举反。就是說在關鍵區(qū)域中千萬不要出現(xiàn)中斷操作,或在JVM中分配任何新對象;否則會
    造成JVM死鎖扒吁。

如何在C/C++中操作Java數(shù)組火鼻?

  • jType* Get<Type>ArrayElements((<Type>Array array, jboolean* isCopy)):這類方法可以把Java的基本類型數(shù)組轉換成C/C++中的數(shù)組。isCopy為true的時候表示數(shù)據(jù)會拷貝一份雕崩,返回的數(shù)據(jù)的指針是副本的指針魁索。如果false則不會拷貝,直接使用Java數(shù)據(jù)的指針盼铁。不適用isCopy可以傳NULL或者0粗蔚。
  • void Release<Type>ArrayElements(jTypeArray array, j<Type>* elems,jint mode):釋放操作,只要有調用Get<Type>ArrayElements方法饶火,就必須要調用一次對應的Release<Type>ArrayElements方法鹏控,因為這樣會刪除掉可能會阻止垃圾回收的JNI本地引用。這里我們注意以下這個方法的最后一個參數(shù)mode,他的作用主要用于避免在處理副本數(shù)據(jù)的時產(chǎn)生對Java堆不必要的影響肤寝。如果Get<Type>ArrayElements中的isCopy為true当辐,我們才需要設置mode,為false我們mode可以不用處理,賦值0。mode有三個值:
    • 0:更新Java堆上的數(shù)據(jù)并釋放副本使用所占有的空間鲤看。
    • JNI_COMMIT:提交缘揪,更新Java堆上的數(shù)據(jù),不釋放副本使用的空間义桂。
    • JNI_ABORT:撤銷找筝,不更新Java堆上的數(shù)據(jù),釋放副本使用所占有的空間慷吊。
  • void* GetPrimitiveArrayCritical(jarray array, jboolean* isCopy):作用類似與Get<Type>ArrayElements袖裕。這個方法可能會通過VM返回指向原始數(shù)組的指針。注意在使用此方法的時候避免死鎖問題罢浇。
  • void ReleasePrimitiveArrayCritical(jarray array, void* carray, jint mode):上面方法對應的釋放方法陆赋。注意這兩個方法之間不要調用任何JNI的函數(shù)方法沐祷。因為可能會導致當前線程阻塞。
  • void Get<Type>ArrayRegion(JNIEnv *env, ArrayType array,jsize start, jsize len, Type *buf):和GetStringRegion的作用是相似的攒岛,事先在C/C++中創(chuàng)建一個緩存區(qū)赖临,然后將Java中的原始數(shù)組拷貝到緩沖區(qū)中去。
  • void Set<Type>ArrayRegion(JNIEnv *env, ArrayType array,jsize start, jsize len, const Type *buf):上面方法的對應方法灾锯,將緩沖區(qū)的部分數(shù)據(jù)設置回Java原始數(shù)組中兢榨。
  • jsize GetArrayLength(JNIEnv *env, jarray array):獲取數(shù)組長度。
  • jobjectArray NewObjectArray(JNIEnv *env, jsize length,jclass elementClass, jobject initialElement):創(chuàng)建指定長度的數(shù)組顺饮。

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

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

    //獲取arrays對象的指針
    jint* int_arr = env->GetIntArrayElements(jint_arr, NULL) ;
    //獲取數(shù)組的長度
    jsize len = env->GetArrayLength(jint_arr) ;
    LOGD("---------------獲取到的原始數(shù)據(jù)為---------------");
    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) ;
    //計數(shù)
    jint count = 0;

    LOGD("---------------打印其中是奇數(shù)---------------");
    //奇數(shù)數(shù)位存入到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內存中的數(shù)組
    for(int k = 0; k < count; k++){
        LOGD("len %d",int_arr_temp[k]);
    }

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

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

    LOGD("---------------重新賦值打印---------------");
    //創(chuàng)建一個新的int數(shù)組
    jint* buffers = new jint[3];
    jint start = 100;
    for (int n = start; n < 3+start ; ++n) {
        buffers[n-start] = n+1;
    }
    //重新給jint_arr數(shù)組中的從第1位開始往后3個數(shù)賦值
    env -> SetIntArrayRegion(jint_arr,1,3,buffers);
    //從新獲取數(shù)據(jù)指針
    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("---------------數(shù)據(jù)處理完成---------------");

}

運行結果:

D/learn JNI: ---------------獲取到的原始數(shù)據(jù)為---------------
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: ---------------打印其中是奇數(shù)---------------
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: ---------------數(shù)據(jù)處理完成---------------

JNI中幾種引用的區(qū)別兼雄?

從JVM創(chuàng)建的對象傳遞到C/C++代碼時會產(chǎn)生引用吟逝,由于Java的垃圾回收機制限制,只要對象有引用存在就不會被回收赦肋。所以無論在C/C++中還是Java中我們在使用引用的時候需要特別注意块攒。下面講下C/C++中的引用:

全局引用

全局引用可以跨多個線程,在多個函數(shù)中都有效佃乘。全局引用需要通過NewGlobalRef方法手動創(chuàng)建囱井,對應的釋放全局引用的方法為DeleteGlobalRef

局部引用

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

弱引用

弱引用也需要自己手動創(chuàng)建住练,作用和全局引用的作用相似,不同點在于弱引用不會阻止垃圾回收器對引用所指對象的回收骆捧。我們可以通過NewWeakGlobalRef方法來創(chuàng)建弱引用澎羞,也可以通過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"); //如果第二步省略也可以通過這個直接打印日志。

上面是我們新建項目自動創(chuàng)建的cpp目錄和.cpp文件来涨。如果想自己寫一個該怎么辦呢图焰?且聽我娓娓道來:

如何通過.java生成.cpp?

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

  • 第一步:新建工具類Car,寫一個本地靜態(tài)方法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 (參數(shù)類型簽名..)返回值類型簽名

好了尿这,拿到方法簽名了,我們就可以開始在C/C++中來調用Java代碼了庆杜。來來來射众,現(xiàn)在我們一起來學習如何在C/C++中調用Java代碼。

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

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

異常處理通常我們分為兩步叨橱,捕獲異常和拋出異常。在C/C++中實現(xiàn)這兩步也相當簡單断盛。我們先看幾個函數(shù):

  • ExceptionCheck:檢測是否有異常罗洗,有返回JNI_TRUE,否則返回FALSE。
  • ExceptionOccurred:判斷是否有異常钢猛,有返回異常伙菜,沒有返回NULL。
  • ExceptionClear:清除異常堆棧信息命迈。
  • Throw:拋出當前異常贩绕。
  • ThrowNew:創(chuàng)建一個新異常火的,并自定義異常信息。
  • 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

參考文章及鏈接

文章總結

這篇文章從開始動筆到最后完工差不多斷斷續(xù)續(xù)一個多月時間了迂尝,轉眼都快過年了脱茉,目測這是年前最后一篇,原本計劃想著將so的相關知識點也寫到這篇文章里面垄开,后面由于多方面考慮就改變主意了琴许,關于so的相關知識會重新出一篇較詳細的文章。

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

最后锻梳,還是那句老話箭券,如果大家在開發(fā)Android中有遇到我寫過文章中的問題,可以在我公眾號「aserbaocool」給我留言疑枯,知無不言辩块,同時也歡迎大家來加入Android交流群。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末荆永,一起剝皮案震驚了整個濱河市废亭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌具钥,老刑警劉巖豆村,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異骂删,居然都是意外死亡掌动,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門桃漾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來坏匪,“玉大人,你說我怎么就攤上這事撬统∈首遥” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵恋追,是天一觀的道長凭迹。 經(jīng)常有香客問我罚屋,道長,這世上最難降的妖魔是什么嗅绸? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任脾猛,我火速辦了婚禮,結果婚禮上鱼鸠,老公的妹妹穿的比我還像新娘猛拴。我一直安慰自己,他們只是感情好蚀狰,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布愉昆。 她就那樣靜靜地躺著,像睡著了一般麻蹋。 火紅的嫁衣襯著肌膚如雪跛溉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天扮授,我揣著相機與錄音芳室,去河邊找鬼。 笑死刹勃,一個胖子當著我的面吹牛堪侯,可吹牛的內容都是我干的。 我是一名探鬼主播深夯,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼抖格,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了咕晋?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤收奔,失蹤者是張志新(化名)和其女友劉穎掌呜,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體坪哄,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡质蕉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了翩肌。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片模暗。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖念祭,靈堂內的尸體忽然破棺而出兑宇,到底是詐尸還是另有隱情,我是刑警寧澤粱坤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布隶糕,位于F島的核電站瓷产,受9級特大地震影響,放射性物質發(fā)生泄漏枚驻。R本人自食惡果不足惜濒旦,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望再登。 院中可真熱鬧尔邓,春花似錦、人聲如沸锉矢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽沈撞。三九已至慷荔,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間缠俺,已是汗流浹背显晶。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留壹士,地道東北人磷雇。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像躏救,于是被迫代替她去往敵國和親唯笙。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354