JNI NDK入門詳解

Android開發中,因爲各類緣由(跨平臺,高性能,敏感數據處理等),這時候須要用到你們耳熟能詳的JNI(Java Native Interface).本篇文章將帶你們複習一下JNI中那些經常使用的知識點.因此本文中沒有一些基本環境配置的講解,若是須要的話,能夠先閱讀一下我以前寫的:html

  1. JNI初識 HelloWorld
  2. JNI Java與C的相互調用與基本操做

本文相關demo: https://github.com/xfhy/AllInOne/blob/master/app/src/main/cpp/native-lib.cppjava

先來看下目錄:linux

JNI NDK入門詳解
JNI NDK入門詳解

1. JNI開發流程

  1. 編寫java類,聲明瞭native方法
  2. 編寫native代碼
  3. 將native代碼編譯成so文件
  4. 在java類中引入so庫,調用native方法

2. native方法命名

extern "C"
JNIEXPORT void JNICALL Java_com_xfhy_jnifirst_MainActivity_callJavaMethod(JNIEnv *env, jobject thiz) {  } 複製代碼

函數命名規則: Java_類全路徑_方法名android

函數參數:git

  1. JNIEnv*是定義任意native函數的第一個參數,是指向JNI環境的指針,能夠經過它來訪問JNI提供的接口方法.
  2. jobject: 表示Java對象中的this. 若是是靜態方法則是用jclass
  3. JNIEXPORT和JNICALL: 它們是JNI中所定義的宏,能夠在jni.h這個頭文件中查找到.

3. JNI數據類型及與Java數據類型的映射關係

首先在Java代碼裏寫個native方法聲明,而後alt+enter讓AS幫忙建立一個native方法.看看類型對應關係github

public static native void ginsengTest(short s, int i, long l, float f, double d, char c,  boolean z, byte b, String str, Object obj, MyClass p, int[] arr); 複製代碼
Java_com_xfhy_jnifirst_MainActivity_ginsengTest(JNIEnv *env, jclass clazz, jshort s, jint i, jlong l, jfloat f, jdouble d, jchar c,
 jboolean z, jbyte b, jstring str, jobject obj, jobject p, jintArray arr) {  } 複製代碼

上面的方法定義上看,除了JNIEnv和jclass以外,其餘都是一一對應的.故能夠總結出以下表格:web

Java和JNI的類型對照表shell

java 類型 Native 類型 符號屬性 字長
boolean jboolean 無符號 8 位
byte jbyte 有符號 8 位
char jchar 無符號 16 位
short jshort 有符號 16 位
int jint 有符號 32 位
long jlong 有符號 64 位
float jfloat 有符號 32 位
double jdouble 有符號 64 位

引用類型對照表數組

java 類型 Native 類型
java.lang.Class jclass
java.lang.Throwable jthrowable
java.lang.String jstring
java.lang.Object[] jobjectArray
Boolean[] jbooleanArray
Byte[] jbyteArray
Char[] jcharArray
Short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdoubleArray

3.1 基本數據類型

基本數據類型其實就是將C/C++中的基本類型用typedef從新定義了一個新的名字,在JNI中能夠直接訪問.安全

/* Primitive types that match up with Java equivalents. */
typedef uint8_t jboolean; /* unsigned 8 bits */ typedef int8_t jbyte; /* signed 8 bits */ typedef uint16_t jchar; /* unsigned 16 bits */ typedef int16_t jshort; /* signed 16 bits */ typedef int32_t jint; /* signed 32 bits */ typedef int64_t jlong; /* signed 64 bits */ typedef float jfloat; /* 32-bit IEEE 754 */ typedef double jdouble; /* 64-bit IEEE 754 */ 複製代碼

3.2 引用數據類型

若是使用C++語言編寫,則全部引用派生自jobject.

class _jobject {};
class _jclass : public _jobject {}; class _jstring : public _jobject {}; class _jarray : public _jobject {}; class _jobjectArray : public _jarray {}; class _jbooleanArray : public _jarray {}; class _jbyteArray : public _jarray {}; class _jcharArray : public _jarray {}; class _jshortArray : public _jarray {}; class _jintArray : public _jarray {}; class _jlongArray : public _jarray {}; class _jfloatArray : public _jarray {}; class _jdoubleArray : public _jarray {}; class _jthrowable : public _jobject {}; 複製代碼

JNI使用C語言時,全部引用類型使用jobject.

4. JNI 字符串處理

4.1 native操做JVM的數據結構

JNI會把Java中全部對象當作一個C指針傳遞到本地方法中,這個指針指向JVM內部數據結構,而內部的數據結構在內存中的存儲方式是不可見的.只能從JNIEnv指針指向的函數表中選擇合適的JNI函數來操做JVM中的數據結構.

好比native訪問java.lang.String 對應的JNI類型jstring時,不能像訪問基本數據類型那樣使用,由於它是一個Java的引用類型,因此在本地代碼中只能經過相似GetStringUTFChars這樣的JNI函數來訪問字符串的內容.

4.2 字符串操做

先來看一下例子:

//調用 String result = operateString("待操做的字符串"); Log.d("xfhy", result);  //定義 public native String operateString(String str); 複製代碼
extern "C"
JNIEXPORT jstring JNICALL Java_com_xfhy_jnifirst_MainActivity_operateString(JNIEnv *env, jobject thiz, jstring str) {  //從java的內存中把字符串拷貝出來 在native使用  const char *strFromJava = (char *) env->GetStringUTFChars(str, NULL);  if (strFromJava == NULL) {  //必須空檢查  return NULL;  }   //將strFromJava拷貝到buff中,待會兒好拿去生成字符串  char buff[128] = {0};  strcpy(buff, strFromJava);  strcat(buff, " 在字符串後面加點東西");   //釋放資源  env->ReleaseStringUTFChars(str, strFromJava);   //自動轉爲Unicode  return env->NewStringUTF(buff); } 複製代碼

輸出爲:待操做的字符串 在字符串後面加點東西

4.2.1 native中獲取JVM字符串

operateString函數接收一個jstring類型的參數str,jstring是指向JVM內部的一個字符串,不能直接使用.首先須要將jstring轉爲C風格的字符串類型char*,而後才能使用,這裏必須使用合適的JNI函數來訪問JVM內部的字符串數據結構.(上例中使用的是GetStringUTFChars)

GetStringUTFChars(jstring string, jboolean* isCopy)參數說明:

  • string : jstring,Java傳遞給native代碼的字符串指針
  • isCopy : 通常狀況下傳NULL. 取值是 JNI_TRUEJNI_FALSE.若是是 JNI_TRUE則會返回JVM內部源字符串的一份拷貝,併爲新產生的字符串分配內存空間.若是是 JNI_FALSE則返回JVM內部源字符串的指針,意味着能夠在native層修改源字符串,可是不推薦修改,Java字符串的原則是不能修改的.

Java中默認是使用Unicode編碼,C/C++默認使用UTF編碼,因此在native層與java層進行字符串交流的時候須要進行編碼轉換.GetStringUTFChars就恰好能夠把jstring指針(指向JVM內部的Unicode字符序列)的字符串轉換成一個UTF-8格式的C字符串.

4.2.2 異常處理

在使用GetStringUTFChars的時候,返回的值可能爲NULL,這時須要處理一下,不然繼續往下面走的話,使用這個字符串的時候會出現問題.由於調用這個方法時,是拷貝,JVM爲新生成的字符串分配內存空間,當內存空間不夠分配的時候,會致使調用失敗.調用失敗就會返回NULL,並拋出OutOfMemoryError.JNI遇到未決的異常不會改變程序的運行流程,仍是會繼續往下走.

4.2.3 釋放字符串資源

native不像Java,咱們須要手動釋放申請的內存空間.GetStringUTFChars調用時會新申請一塊空間用來裝拷貝出來的字符串,這個字符串用來方便native代碼訪問和修改之類的. 既然有內存分配,那麼就必須手動釋放,釋放方法是ReleaseStringUTFChars.能夠看到和GetStringUTFChars是一一對應的,配對的.

4.2.4 構建字符串

使用NewStringUTF函數能夠構建出一個jstring,須要傳入一個char *類型的C字符串.它會構建一個新的java.lang.String字符串對象,而且會自動轉換成Unicode編碼. 若是JVM不能爲構造java.lang.String分配足夠的內存,則會拋出一個OutOfMemoryError異常,並返回NULL.

4.2.5 其餘字符串操做函數

  1. GetStringChars和ReleaseStringChars: 這對函數和Get/ReleaseStringUTFChars函數功能相似,用於獲取和釋放的字符串是以Unicode格式編碼的.
  2. GetStringLength: 獲取Unicode字符串(jstring)的長度. UTF-8編碼的字符串是以 \0結尾,而Unicode的不是,因此這裏須要單獨區分開.
  3. GetStringUTFLength: 獲取UTF-8編碼字符串的長度,就是獲取C/C++默認編碼字符串的長度.還可使用標準C函數 strlen來獲取其長度.
  4. strcat: 拼接字符串,標準C函數. eg: strcat(buff, "xfhy"); 將xfhy添加到buff的末尾.
  5. GetStringCritical和ReleaseStringCritical: 爲了增長直接傳回指向Java字符串的指針的可能性(而不是拷貝).在這2個函數之間的區域,是絕對不能調用其餘JNI函數或者讓線程阻塞的native函數.不然JVM可能死鎖. 若是有一個字符串的內容特別大,好比1M,且只須要讀取裏面的內容打印出來,此時比較適合用該對函數,可直接返回源字符串的指針.
  6. GetStringRegion和GetStringUTFRegion: 獲取Unicode和UTF-8字符串中指定範圍的內容(eg: 只須要1-3索引處的字符串),這對函數會將源字符串複製到一個預先分配的緩衝區(本身定義的char數組)內.
extern "C"
JNIEXPORT jstring JNICALL Java_com_xfhy_jnifirst_MainActivity_operateString(JNIEnv *env, jobject thiz, jstring str) {  //方式2 用GetStringUTFRegion方法將JVM中的字符串拷貝到C/C++的緩衝區(數組)中  //獲取Unicode字符串長度  int len = env->GetStringLength(str);  char buff[128];  env->GetStringUTFRegion(str, 0, len, buff);  LOGI("-------------- %s", buff);   //自動轉爲Unicode  return env->NewStringUTF(buff); } 複製代碼

GetStringUTFRegion會進行越界檢查,越界會拋StringIndexOutOfBoundsException異常.GetStringUTFRegion其實和GetStringUTFChars有點類似,可是GetStringUTFRegion內部不會分配內存,不會拋出內存溢出異常. 因爲其內部沒有分配內存,因此也沒有相似Release這樣的函數來釋放資源.

4.2.6 字符串 小結

  1. Java字符串轉C/C++字符串: 使用GetStringUTFChars函數,必須調用ReleaseStringUTFChars釋放內存
  2. 建立Java層須要的Unicode字符串,使用NewStringUTF函數
  3. 獲取C/C++字符串長度,使用GetStringUTFLength或者strlen函數
  4. 對於小字符串,GetStringRegion和GetStringUTFRegion這2個函數是最佳選擇,由於緩衝區數組能夠被編譯器提取分配,不會產生內存溢出的異常.當只須要處理字符串的部分數據時,也仍是不錯.它們提供了開始索引和子字符串長度值,複製的消耗也是很是小
  5. 獲取Unicode字符串和長度,使用GetStringChars和GetStringLength函數

5. 數組操做

5.1 基本類型數組

基本類型數組就是JNI中的基本數據類型組成的數組,能夠直接訪問.下面舉個簡單例子,int數組求和:

//MainActivity.java
public native int sumArray(int[] array); 複製代碼
extern "C"
JNIEXPORT jint JNICALL Java_com_xfhy_jnifirst_MainActivity_sumArray(JNIEnv *env, jobject thiz, jintArray array) {  //數組求和  int result = 0;   //方式1 推薦使用  jint arr_len = env->GetArrayLength(array);  //動態申請數組  jint *c_array = (jint *) malloc(arr_len * sizeof(jint));  //初始化數組元素內容爲0  memset(c_array, 0, sizeof(jint) * arr_len);  //將java數組的[0-arr_len)位置的元素拷貝到c_array數組中  env->GetIntArrayRegion(array, 0, arr_len, c_array);  for (int i = 0; i < arr_len; ++i) {  result += c_array[i];  }  //動態申請的內存 必須釋放  free(c_array);   return result; } 複製代碼

C層拿到jintArray以後首先須要獲取它的長度,而後動態申請一個數組(由於Java層傳遞過來的數組長度是不定的,因此這裏須要動態申請C層數組),這個數組的元素是jint類型的.malloc是一個常用的拿來申請一塊連續內存的函數,申請以後的內存是須要手動調用free釋放的.而後就是調用GetIntArrayRegion函數將Java層數組拷貝到C層數組中,而後求和,看起來仍是so easy的.

下面還看另外一種求和方式:

extern "C"
JNIEXPORT jint JNICALL Java_com_xfhy_jnifirst_MainActivity_sumArray(JNIEnv *env, jobject thiz, jintArray array) {  //數組求和  int result = 0;   //方式2   //此種方式比較危險,GetIntArrayElements會直接獲取數組元素指針,是能夠直接對該數組元素進行修改的.  jint *c_arr = env->GetIntArrayElements(array, NULL);  if (c_arr == NULL) {  return 0;  }  c_arr[0] = 15;  jint len = env->GetArrayLength(array);  for (int i = 0; i < len; ++i) {  //result += *(c_arr + i); 寫成這種形式,或者下面一行那種都行  result += c_arr[i];  }  //有Get,通常就有Release  env->ReleaseIntArrayElements(array, c_arr, 0);   return result; } 複製代碼

直接經過GetIntArrayElements函數拿到原數組元素指針,直接操做,就能夠拿到元素求和.看起來要簡單不少,可是這種方式我我的以爲是有點危險的,畢竟這種能夠在C層直接進行源數組修改.GetIntArrayElements的第二個參數通常傳NULL,傳遞JNI_TRUE是返回臨時緩衝區數組指針(即拷貝一個副本),傳遞JNI_FALSE則是返回原始數組指針.

這裏簡單小結一下: 推薦使用Get/SetArrayRegion函數來操做數組元素是效率最高的.本身動態申請數組,本身操做,省得影響Java層.

5.2 對象數組

對象數組中的元素是一個類的實例或其餘數組的引用,不能直接訪問Java傳遞給JNI層的數組.

操做對象數組稍顯複雜,下面舉一個例子,在native層建立一個二維數組,且賦值並返回給Java層使用.(ps: 第二維是int[],它屬於對象)

public native int[][] init2DArray(int size);
 //交給native層建立->Java打印輸出 int[][] init2DArray = init2DArray(3); for (int i = 0; i < 3; i++) {  for (int i1 = 0; i1 < 3; i1++) {  Log.d("xfhy", "init2DArray[" + i + "][" + i1 + "]" + " = " + init2DArray[i][i1]);  } }  複製代碼
extern "C"
JNIEXPORT jobjectArray JNICALL Java_com_xfhy_jnifirst_MainActivity_init2DArray(JNIEnv *env, jobject thiz, jint size) {  //建立一個size*size大小的二維數組   //jobjectArray是用來裝對象數組的 Java數組就是一個對象 int[]  jclass classIntArray = env->FindClass("[I");  if (classIntArray == NULL) {  return NULL;  }  //建立一個數組對象,元素爲classIntArray  jobjectArray result = env->NewObjectArray(size, classIntArray, NULL);  if (result == NULL) {  return NULL;  }  for (int i = 0; i < size; ++i) {  jint buff[100];  //建立第二維的數組 是第一維數組的一個元素  jintArray intArr = env->NewIntArray(size);  if (intArr == NULL) {  return NULL;  }  for (int j = 0; j < size; ++j) {  //這裏隨便設置一個值  buff[j] = 666;  }  //給一個jintArray設置數據  env->SetIntArrayRegion(intArr, 0, size, buff);  //給一個jobjectArray設置數據 第i索引,數據位intArr  env->SetObjectArrayElement(result, i, intArr);  //及時移除引用  env->DeleteLocalRef(intArr);  }   return result; } 複製代碼

比較複雜,分析一下

  1. 首先是利用FindClass函數找到java層int[]對象的class,這個class是須要傳入NewObjectArray建立對象數組的.調用NewObjectArray函數以後,便可建立一個對象數組,大小是size,元素類型是前面獲取到的class.
  2. 進入for循環構建size個int數組,構建int數組須要使用NewIntArray函數.能夠看到我構建了一個臨時的buff數組,而後大小是隨便設置的,這裏是爲了示例,其實能夠用malloc動態申請空間,省得申請100個空間,可能太大或者過小了.整buff數組主要是拿來給生成出來的jintArray賦值的,由於jintArray是Java的數據結構,咱native不能直接操做,得調用SetIntArrayRegion函數,將buff數組的值複製到jintArray數組中.
  3. 而後調用SetObjectArrayElement函數設置jobjectArray數組中某個索引處的數據,這裏將生成的jintArray設置進去.
  4. 最後須要將for裏面生成的jintArray及時移除引用.建立的jintArray是一個JNI局部引用,若是局部引用太多的話,會形成JNI引用表溢出.

ps: 在JNI中,只要是jobject的子類就屬於引用變量,會佔用引用表的空間. 而基礎數據類型jint,jfloat,jboolean等是不會佔用引用表空間的,不須要釋放.

6. native調Java方法

前面已經敘述瞭如何在Java中調用native方法,這裏將帶你們看看native如何調用Java方法.

ps: 在JVM中,運行一個Java程序時,會先將運行時須要用到的全部相關class文件加載到JVM中,並按需加載,提升性能和節約內存.當咱們調用一個類的靜態方法以前,JVM會先判斷該類是否已經加載,若是沒有被ClassLoader加載到JVM中,會去classpath路徑下查找該類.找到了則加載該類,沒有找到則報ClassNotFoundException異常.

6.1 native調用Java靜態方法

爲了演示代碼簡潔,方便理清核心內容,已刪除各類NULL判斷.

我先寫一個MyJNIClass.java類

public class MyJNIClass {
  public int age = 18;   public int getAge() {  return age;  }   public void setAge(int age) {  this.age = age;  }   public static String getDes(String text) {  if (text == null) {  text = "";  }  return "傳入的字符串長度是 :" + text.length() + " 內容是 : " + text;  }  } 複製代碼

而後去native調用getDes方法,爲了複雜一點,這個getDes方法不只有入參,還有返參.

extern "C"
JNIEXPORT void JNICALL Java_com_xfhy_allinone_jni_CallMethodActivity_callJavaStaticMethod(JNIEnv *env, jobject thiz) {  //調用某個類的static方法   //JVM使用一個類時,是須要先判斷這個類是否被加載了,若是沒被加載則還須要加載一下才能使用  //1. 從classpath路徑下搜索MyJNIClass這個類,並返回該類的Class對象  jclass clazz = env->FindClass("com/xfhy/allinone/jni/MyJNIClass");  //2. 從clazz類中查找getDes方法 獲得這個靜態方法的方法id  jmethodID mid_get_des = env->GetStaticMethodID(clazz, "getDes", "(Ljava/lang/String;)Ljava/lang/String;");  //3. 構建入參,調用static方法,獲取返回值  jstring str_arg = env->NewStringUTF("我是xfhy");  jstring result = (jstring) env->CallStaticObjectMethod(clazz, mid_get_des, str_arg);  const char *result_str = env->GetStringUTFChars(result, NULL);  LOGI("獲取到Java層返回的數據 : %s", result_str);   //4. 移除局部引用  env->DeleteLocalRef(clazz);  env->DeleteLocalRef(str_arg);  env->DeleteLocalRef(result); } 複製代碼

看起來好像比較簡單,native的代碼實際上是短小精悍,這裏面涉及的知識點挺多.

  1. 首先是調用FindClass函數,傳入Class描述符(Java類的全類名,這裏在AS中輸入MyJNIClass時會有提示補全,直接enter便可補全),找到該類,獲得jclass類型.
  2. 而後經過GetStaticMethodID找到該方法的id,傳入方法簽名,獲得jmethodID類型的引用(存儲方法的引用).(這裏輸入getDes時,AS也會有補全功能,按enter直接把簽名帶出來了,真tm方便). 這裏先使用AS的自動補全功能把方法簽名帶出來,後面會詳細說這個方法簽名是什麼.
  3. 構建入參,而後調用CallStaticObjectMethod去調用Java類裏面的靜態方法,而後傳入參數,返回的直接就是Java層返回的數據. 其實這裏的CallStaticObjectMethod是調用的引用類型的靜態方法,與之類似的還有: CallStaticVoidMethod(無返參),CallStaticIntMethod(返參是Int),CallStaticFloatMethod,CallStaticShortMethod.他們的用法是一致的.
  4. 移除局部引用

ps: 函數結束後,JVM會自動釋放全部局部引用變量所佔的內存空間. 這裏仍是手動釋放一下比較安全,由於在JVM中維護着一個引用表,用於存儲局部和全局引用變量. 經測試發如今Android低版本(我測試的是Android 4.1)上,這個表的最大存儲空間是512個引用,,當超出這個數量時直接崩潰.當我在高版本,好比小米 8(安卓10)上,這個引用個數能夠達到100000也不會崩潰,只是會卡頓一下.多是硬件比當年更牛逼了,默認值也跟着改了.

下面是引用表溢出時所報的錯誤(要獲得這個錯還挺難的,得找一個比較老舊的設備才行):

E/dalvikvm: JNI ERROR (app bug): local reference table overflow (max=512)
E/dalvikvm: Failed adding to JNI local ref table (has 512 entries)
E/dalvikvm: VM aborting
A/libc: Fatal signal 11 (SIGSEGV) at 0xdeadd00d (code=1), thread 2561 (m.xfhy.allinone)
複製代碼

下面是我拿來還原錯誤的native代碼,這裏恰好是513個就會異常,低於513就不會.說明早年的一些設備確實是以512做爲最大限度.當咱們APP須要兼容老設備(通常都須要吧,哈哈)的話,這確定是須要注意的點.

extern "C"
JNIEXPORT jobject JNICALL Java_com_xfhy_allinone_jni_CallMethodActivity_testMaxQuote(JNIEnv *env, jobject thiz) {  //測試Android虛擬機引用表最大限度數量   jclass clazz = env->FindClass("java/util/ArrayList");  jmethodID constrId = env->GetMethodID(clazz, "<init>", "(I)V");  jmethodID addId = env->GetMethodID(clazz, "add", "(ILjava/lang/Object;)V");  jobject arrayList = env->NewObject(clazz, constrId, 513);  for (int i = 0; i < 513; ++i) {  jstring test_str = env->NewStringUTF("test");  env->CallVoidMethod(arrayList, addId, 0, test_str);  //這裏應該刪除的 局部引用  //env->DeleteLocalRef(test_str);  }   return arrayList; } 複製代碼

補充:

後來我在一篇文章中發現,

  • 局部引用溢出在Android不一樣版本上表現有所區別.Android 8.0以前局部引用表的上限是512個引用,Android 8.0以後局部引用表上限提高到了8388608個引用.(這裏我沒有實際測試,可是基本符合我上面的測試).
  • 而Oracle Java沒有局部引用表上限限制,隨着局部引用表不斷增大,最終會OOM.

6.2 native調用Java實例方法

下面演示在native層建立Java實例,並調用該實例的方法,大體上是和上面調用靜態方法差很少的.

extern "C"
JNIEXPORT void JNICALL Java_com_xfhy_allinone_jni_CallMethodActivity_createAndCallJavaInstanceMethod(JNIEnv *env, jobject thiz) {  //構建一個類的實例,並調用該實例的方法   //特別注意 : 正常狀況下,這裏的每個獲取出來都須要判空處理,我這裏爲了展現核心代碼,就不判空了  //這裏輸入MyJNIClass以後AS會提示,而後按Enter鍵便可自動補全  jclass clazz = env->FindClass("com/xfhy/allinone/jni/MyJNIClass");  //獲取構造方法的方法id  jmethodID mid_construct = env->GetMethodID(clazz, "<init>", "()V");  //獲取getAge方法的方法id  jmethodID mid_get_age = env->GetMethodID(clazz, "getAge", "()I");  jmethodID mid_set_age = env->GetMethodID(clazz, "setAge", "(I)V");  jobject jobj = env->NewObject(clazz, mid_construct);   //調用方法setAge  env->CallVoidMethod(jobj, mid_set_age, 20);  //再調用方法getAge 獲取返回值 打印輸出  jint age = env->CallIntMethod(jobj, mid_get_age);  LOGI("獲取到 age = %d", age);   //凡是使用是jobject的子類,都須要移除引用  env->DeleteLocalRef(clazz);  env->DeleteLocalRef(jobj); } 複製代碼

這裏仍是使用上面的MyJNIClass類

  1. 找到該類的class
  2. 獲取構造方法的id,獲取須要調用方法的id. 其中獲取構造方法時,方法名稱固定寫法就是 <init>,而後後面是方法簽名.
  3. 利用NewObject()函數構建一個Java對象
  4. 調用Java對象的setAge和getAge方法,獲取返回值,打印結果.這裏調用了CallIntMethod和CallVoidMethod,相信你們一看名字就能猜出來,這是幹啥的.就是調用返回值是int和void的方法.和上面的CallStaticIntMethod相似.
  5. 刪除局部引用 !!!

6.3 方法簽名

調用Java方法須要使用jmethodID,每次獲取jmethodID都須要傳入方法簽名.明明已經傳入了方法名稱,感受就已經能夠調這個方法了啊,爲啥還須要傳入一個方法簽名? 由於Java方法存在重載,可能方法名是相同的,可是參數不一樣.因此這裏必須傳入方法簽名以區別.

方法簽名格式爲: (形參參數類型列表)返回值 , 引用類型以L開頭,後面是類的全路徑名.

Java基本類型與方法簽名中的定義關係:

Java Field Descriptor
boolean Z
byte B
char C
short S
int I
long J
float F
double D
int[] [I

下面給出一個示例:

public String[] testString(boolean a, byte b, float f, double d, int i, int[] array) 複製代碼env->GetMethodID(clazz,"testString", "(ZBFDI[I)[Ljava/lang/String;") 複製代碼

6.4 native調用Java方法 小結

  1. 獲取一個類的Class實例,得使用FindClass函數,傳入類描述符.JVM會從classpath目錄下去查找該類.
  2. 建立一個類的實例是使用NewObject方法,需傳入class引用和構造方法的id.
  3. 局部引用(只要是jobject的子類就屬於引用變量,會佔用引用表的空間)必定記得及時移除掉,不然會引用表溢出.低版本上很容易就溢出了(低版本上限512).
  4. 方法簽名格式: (形參參數類型列表)返回值
  5. 在使用FindClass和GetMethodID的時候,必定得判空.基操.
  6. 獲取實例方法ID,使用GetMethodID函數;獲取靜態方法ID,使用GetStaticMethodID函數;獲取構造方法ID,方法名稱使用 <init>
  7. 調用實例方法使用CallXXMethod函數,XX表示返回數據類型. eg:CallIntMethod() ; 調用靜態方法使用CallStaticXXMethod函數.

7. NDK Crash錯誤定位

當NDK發生錯誤某種致命的錯誤的時候,會致使APP閃退.這類錯誤很是很差查問題,好比內存地址訪問錯誤,使用野指針,內存泄露,堆棧溢出,數字除0等native錯誤都會致使APP崩潰.

雖然這些NDK錯誤很差排查,可是咱們在NDK錯誤發生後,拿到logcat輸出的堆棧日誌,再結合下面的2款調試工具--addr2line和ndk-stack,可以精確地定位到相應發生錯誤的代碼行數,而後迅速找到問題.

首先到ndk目錄下,來到sdk/ndk/21.0.6113669/toolchains/目錄,我本地的目錄以下,能夠看到NDK交叉編譯器工具鏈的目錄結構.

aarch64-linux-android-4.9
arm-linux-androideabi-4.9
llvm
renderscript
x86-4.9
x86_64-4.9
複製代碼

其中ndk-stack放在$NDK_HOME目錄下,與ndk-build同級目錄。addr2line在ndk的交叉編譯器工具鏈目錄下.NDK針對不一樣的CPU架構實現了多套工具.在使用addr2line工具時,須要根據當前手機cpu架構來選擇.個人手機是aarch64的,則使用aarch64-linux-android-4.9目錄下的工具. 查看手機的cpu信息的命令:adb shell cat /proc/cpuinfo

在介紹兩款調試工具以前,咱們得先寫好能崩潰的native代碼,方便看效果. 我在demo的native-lib.cpp裏面寫了以下代碼:

void willCrash() {
 JNIEnv *env = NULL;  int version = env->GetVersion(); }  extern "C" JNIEXPORT void JNICALL Java_com_xfhy_allinone_jni_CallMethodActivity_nativeCrashTest(JNIEnv *env, jobject thiz) {  LOGI("崩潰前");  willCrash();  //後面的代碼是執行不到的,由於崩潰了  LOGI("崩潰後");  printf("oooo"); } 複製代碼

這段代碼,是很明顯的空指針錯誤.我原本想搞一個除0錯誤的,可是除0死活不崩潰,,,不知道爲啥(進化了?). 而後運行起來,錯誤日誌以下:

2020-06-07 17:05:25.230 12340-12340/? A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** 2020-06-07 17:05:25.230 12340-12340/? A/DEBUG: Build fingerprint: 'Xiaomi/dipper/dipper:10/QKQ1.190828.002/V11.0.8.0.QEACNXM:user/release-keys' 2020-06-07 17:05:25.230 12340-12340/? A/DEBUG: Revision: '0' 2020-06-07 17:05:25.230 12340-12340/? A/DEBUG: ABI: 'arm64' 2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: Timestamp: 2020-06-07 17:05:25+0800 2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: pid: 11527, tid: 11527, name: m.xfhy.allinone >>> com.xfhy.allinone <<< 2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: uid: 10319 2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: Cause: null pointer dereference 2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: x0 0000000000000000 x1 0000007fd29ffd40 x2 0000000000000005 x3 0000000000000003 2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: x4 0000000000000000 x5 8080800000000000 x6 fefeff6fb0ce1f1f x7 7f7f7f7fffff7f7f 2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: x8 0000000000000000 x9 a95a4ec0adb574df x10 0000007fd29ffee0 x11 000000000000000a 2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: x12 0000000000000018 x13 ffffffffffffffff x14 0000000000000004 x15 ffffffffffffffff 2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: x16 0000006fc6476c50 x17 0000006fc64513cc x18 00000070b21f6000 x19 000000702d069c00 2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: x20 0000000000000000 x21 000000702d069c00 x22 0000007fd2a00720 x23 0000006fc6ceb127 2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: x24 0000000000000004 x25 00000070b1cf2020 x26 000000702d069cb0 x27 0000000000000001 2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: x28 0000007fd2a004b0 x29 0000007fd2a00420 2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: sp 0000007fd2a00410 lr 0000006fc64513bc pc 0000006fc64513e0 2020-06-07 17:05:25.788 12340-12340/? A/DEBUG: backtrace: 2020-06-07 17:05:25.788 12340-12340/? A/DEBUG: #00 pc 00000000000113e0 /data/app/com.xfhy.allinone-4VScOmUWz8wLqqwBWZCP2w==/lib/arm64/libnative-lib.so (_JNIEnv::GetVersion()+20) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d) 2020-06-07 17:05:25.788 12340-12340/? A/DEBUG: #01 pc 00000000000113b8 /data/app/com.xfhy.allinone-4VScOmUWz8wLqqwBWZCP2w==/lib/arm64/libnative-lib.so (willCrash()+24) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d) 2020-06-07 17:05:25.788 12340-12340/? A/DEBUG: #02 pc 0000000000011450 /data/app/com.xfhy.allinone-4VScOmUWz8wLqqwBWZCP2w==/lib/arm64/libnative-lib.so (Java_com_xfhy_allinone_jni_CallMethodActivity_nativeCrashTest+84) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d) 2020-06-07 17:05:25.788 12340-12340/? A/DEBUG: #03 pc 000000000013f350 /apex/com.android.runtime/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: 2bc2e11d57f839316bf2a42bbfdf943a) 2020-06-07 17:05:25.788 12340-12340/? A/DEBUG: #04 pc 0000000000136334 /apex/com.android.runtime/lib64/libart.so (art_quick_invoke_stub+548) (BuildId: 2bc2e11d57f839316bf2a42bbfdf943a) 複製代碼複製代碼

看到這種錯誤,首先找到關鍵信息Cause: null pointer dereference,可是咱們不知道發生在具體哪裏,只知道是這個錯誤.

7.1 addr2line

有了錯誤日誌,如今咱們使用工具addr2line來定位位置.

須要執行命令 /Users/xfhy/development/sdk/ndk/21.0.6113669/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line -e /Users/xfhy/development/AllInOne/app/libnative-lib.so 00000000000113e0 00000000000113b8,這其中-e是指定so文件的位置,而後末尾的00000000000113e0和00000000000113b8是出錯位置的彙編指令地址.

執行以後,結果以下:

/Users/xfhy/development/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/jni.h:497
/Users/xfhy/development/AllInOne/app/src/main/cpp/native-lib.cpp:260
複製代碼

能夠看到是native-lib.cpp的260行出的問題.咱們只須要去找到這個位置,修復這個bug便可.完美,瞬間就找到了.

7.2 ndk-stack

還有一種更簡單的方式,直接輸入命令adb logcat | ndk-stack -sym /Users/xfhy/development/AllInOne/app/build/intermediates/cmake/debug/obj/arm64-v8a,末尾是so文件的位置.執行完命令,而後在手機上產生native錯誤就能在這個so文件中定位到這個錯誤點,以下:

********** Crash dump: ********** Build fingerprint: 'Xiaomi/dipper/dipper:10/QKQ1.190828.002/V11.0.8.0.QEACNXM:user/release-keys' #00 0x00000000000113e0 /data/app/com.xfhy.allinone-oVu0tjta-aW9LYa08eoK1Q==/lib/arm64/libnative-lib.so (_JNIEnv::GetVersion()+20) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d) _JNIEnv::GetVersion() /Users/xfhy/development/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/jni.h:497:14 #01 0x00000000000113b8 /data/app/com.xfhy.allinone-oVu0tjta-aW9LYa08eoK1Q==/lib/arm64/libnative-lib.so (willCrash()+24) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d) willCrash() /Users/xfhy/development/AllInOne/app/src/main/cpp/native-lib.cpp:260:24 #02 0x0000000000011450 /data/app/com.xfhy.allinone-oVu0tjta-aW9LYa08eoK1Q==/lib/arm64/libnative-lib.so (Java_com_xfhy_allinone_jni_CallMethodActivity_nativeCrashTest+84) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d) Java_com_xfhy_allinone_jni_CallMethodActivity_nativeCrashTest /Users/xfhy/development/AllInOne/app/src/main/cpp/native-lib.cpp:267:5 複製代碼複製代碼

能夠看出是willCrash()方法出的錯,它的代碼行數是260行(甚至連列都打印出來了,,第24列).

8. JNI 引用

Java平時在新建立對象的時候,不須要管JVM是怎麼申請內存的,使用完以後也不須要管JVM是如何釋放內存的.而C++不一樣,須要咱們手動申請和釋放內存(new->delete,malloc->free). 在使用JNI時,因爲本地代碼不能直接經過引用操做JVM內部的數據結構,要進行這些操做必須調用相應的JNI接口間接操做JVM內部的數據內容.咱們不須要關心JVM中對象的是如何存儲的,只須要學習JVI中的三種不一樣引用便可.

8.1 JNI 局部引用

本地函數中經過NewLocalRef或調用FindClass、NewObject、GetObjectClass、NewCharArray等建立的引用,就是局部引用.

  • 會阻止GC回收所引用的對象
  • 不能跨線程使用
  • 不在本地函數中跨函數使用
  • 釋放: 函數返回後局部引用所引用的對象會被JVM自動釋放,也能夠調用DeleteLocalRef釋放

一般是在函數中建立並使用的.上面提到了: 局部引用在函數返回以後會自動釋放,那麼咱們爲啥還須要去管它(手動調用DeleteLocalRef釋放)呢?

好比,開了一個for循環,裏面不斷地建立局部引用,那麼這時就必須得使用DeleteLocalRef手動釋放內存.否則局部引用會愈來愈多,致使崩潰(在Android低版本上局部引用表的最大數量有限制,是512個,超過則會崩潰).

還有一種狀況,本地方法返回一個引用到Java層以後,若是Java層沒有對返回的局部引用使用的話,局部引用就會被JVM自動釋放.

8.2 JNI 全局引用

全局引用是基於局部引用建立的,調用NewGlobalRef方法.

  • 會阻止GC回收所引用的對象
  • 能夠跨方法、跨線程使用
  • JVM不會自動釋放,需調用DeleteGlobalRef手動釋放

8.3 JNI 弱全局引用

弱全局引用是基於局部引用或者全局引用建立的,調用NewWeakGlobalRef方法.

  • 不會阻止GC回收所引用的對象
  • 能夠跨方法、跨線程使用
  • 引用不會自動釋放,只有在JVM內存不足時纔會進行回收而被釋放. 還有就是能夠調用DeleteWeakGlobalRef手動釋放.

參考

  • Android Developers NDK 指南 C++ 庫支持 https://developer.android.com/ndk/guides/cpp-support.html
  • JNI/NDK開發指南 https://blog.csdn.net/xyang81/column/info/blogjnindk
  • android 內存泄露之jni local reference table overflow (max=512) https://www.cnblogs.com/lzl-sml/p/3520052.html
  • Android JNI 局部引用溢出(local reference table overflow (max=512)) https://blog.csdn.net/lylwo317/article/details/103105413
  • Android JNI介紹(七)- 引用的管理 https://juejin.im/post/5e04b2236fb9a0162c487ef8#heading-2
  • JNI開發之局部引用、全局引用和弱全局引用(三)https://zhuanlan.zhihu.com/p/93115458
相關文章
相關標籤/搜索