很久沒發文章了,這篇文章是是10月底開始計劃的,轉眼到如今12月都快過一半了,我太難了……,不過好在終於完成了,今晚必須去吃宵夜。深圳北,往北兩千米的**燒烤,有木有人過來?我請客,沒有到時候我再來問一遍。html
先看目錄,各位以爲內容對你有用再繼續往下看,畢竟顯示有一萬多個字呢,怕沒用的話耽誤你們寶貴的時間。java
以前寫過一篇關於C代碼生成和調試so庫的文章。前段時間在繼承一個音頻檢測庫的時候出現了點問題,又複習了下JNI部分,順便整理成文,分享給你們。android
本文是一個 NDK/JNI 系列基礎到進階教程,目標是但願觀看這篇文章的朋友們能對Android中使用C/C++代碼,集成C/C++庫有一個比較基本的瞭解,而且能巧妙的應用到項目中。git
好了,說完目的,我們一如既往,學JNI以前,先來個給本身提幾個問題:github
瞭解是什麼?用來作什麼?以及爲何?數組
什麼是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到底有什麼用呢?
一句話,快速調用C/C++的動態庫。除了調用C/C++以外別無它用。
就是這麼簡單好吧。知道作什麼以後,我們學這玩意有啥用呢?
暫時能想到的兩個點,一個是能讓我在開發中愉快的使用C/C++庫,第二個就是能在安全攻防這一塊有更深刻的瞭解。其實不管這兩個點中的哪一個點都能讓我有足夠動力學下去。因此,想啥呢,搞定他。
配置NDK的環境比較簡單。咱們能夠經過簡單三步來實現:
ok,驗證如上圖所示說明你NDK配置成功了。so easy。
如今開始,我們一塊兒進入HelloWorld的世界。咱們一塊兒來經過AS建立一個Native C++項目。主要步驟以下:
簡單通俗易懂有木有?好了,項目建立成功,運行,看界面,顯示Hello World,項目建立成功。
從上面新建的項目中咱們看到一個cpp目錄,咱們所寫的C/C++代碼就這這個目錄下面。其中會發現有一個名爲native-lib.cpp的文件,這就是用C/C++賦值Hello World的地方。
Android 中調用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。
在C/C++本地代碼中訪問Java端的代碼,一個常見的應用就是獲取類的屬性和調用類的方法,爲了在C/C++中表示屬性和方法,JNI在jni.h頭文件中定義了jfieldID,jmethodID類型來分別表明Java端的屬性和方法。在訪問或者設置Java屬性的時候,首先就要先在本地代碼取得表明該Java屬性的jfeldID,而後才能在本地代碼中進行Java屬性操做,一樣,須要調用Java端的方法時,也是須要取得表明該方法的jmethodID才能進行Java方法調用。
接下來,我們來嘗試下如何在native中調用Java中的方法。先看下兩個常見的類型:
在上面的native-lib.cpp中,咱們看到getCarName方法中有兩個參數,分別是JNIEnv *env,一個是jobjet instance。簡單介紹下這兩個類型的做用。
JNIEnv 類型
JNIEnv類型實際上表明瞭Java環境,經過JNIEnv*指針就能夠對Java端的代碼進行操做。好比咱們可使用JNIEnv來建立Java類中的對象,調用Java對象的方法,獲取Java對象中的屬性等。
JNIEnv類中有不少函數能夠用,以下所示:
好了,說完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代碼。
JNIEnv類中有以下幾個方法能夠獲取java中的類:
須要咱們注意的是,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;
}
複製代碼
這個方法比較好理解,根據上面咱們講的根據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獲取到的
……
}
複製代碼
好了,咱們知道怎麼經過JNIEnv中獲取Java中的類,接下來咱們來學習如何獲取並調用Java中的方法。
在JNIEnv環境下,咱們有以下兩種方法能夠獲取方法和屬性:
GetMethodID方法以下:
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
複製代碼
方法的參數說明:
舉一個小例子,好比咱們要在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對象便可調用靜態方法,非靜態方法則須要實例化以後再調用。
針對多態狀況,我們如何準確調用咱們想要的方法呢?舉一個例子,我有個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變量。
修改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。
JNIEnv中獲取字符串的一些方法:
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);
}
複製代碼
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串相同.
}
複製代碼
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;
}
複製代碼
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);
}
複製代碼
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死鎖。
經過一個方法來使用下上面方法,代碼以下:
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: ---------------數據處理完成---------------
複製代碼
從JVM建立的對象傳遞到C/C++代碼時會產生引用,因爲Java的垃圾回收機制限制,只要對象有引用存在就不會被回收。因此不管在C/C++中仍是Java中咱們在使用引用的時候須要特別注意。下面講下C/C++中的引用:
全局引用
全局引用能夠跨多個線程,在多個函數中都有效。全局引用須要經過NewGlobalRef方法手動建立,對應的釋放全局引用的方法爲DeleteGlobalRef
局部引用
局部引用很常見,基本上經過JNI函數獲取到的返回引用都算局部引用,局部引用只在單個函數中有效。局部引用會在函數返回時自動釋放,固然咱們也能夠經過DeleteLocalRef方法手動釋放。
弱引用
弱引用也須要本身手動建立,做用和全局引用的做用類似,不一樣點在於弱引用不會阻止垃圾回收器對引用所指對象的回收。咱們能夠經過NewWeakGlobalRef方法來建立弱引用,也能夠經過DeleteWeakGlobalRef來釋放對應的弱引用。
在Jni中C/C++層打印日誌是幫助咱們調試代碼較爲重要的一步。簡單分爲三步:
#include <android/log.h>
複製代碼
#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文件。若是想本身寫一個該怎麼辦呢?且聽我娓娓道來:
好比我如今建立一個工具類Car,裏面想寫個native方法叫getCarName(),咱們如何快速獲得對應的.cpp文件呢?方法也很簡單,咱們只須要按步驟運行幾個命令就好了。步驟以下:
public class Car {
static {
System.loadLibrary("native-lib");
}
public native String getCarName();
}
複製代碼
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
複製代碼
#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「。大功告成。
在學習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代碼。
javac *.java
複製代碼
javac -h . *.java
複製代碼
javap -s -p *.class
複製代碼
異常處理一般咱們分爲兩步,捕獲異常和拋出異常。在C/C++中實現這兩步也至關簡單。咱們先看幾個函數:
代碼實例:
//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交流羣。