JNI (Java Native Interface英文縮寫),譯爲Java本地接口。是Java衆多開發技術中的一門技術,意在利用本地代碼,爲Java程序提供更高效、更靈活的拓展。儘管Java一向以其良好的跨平臺性而著稱,但真正的跨平臺非C/C++莫屬,由於當前世上90%的系統都是基於C/C++編寫的。同時,Java的跨平臺是以犧牲效率換來對多種平臺的兼容性,於是JNI就是這種跨平臺的主流實現方式之一。javascript
總之,JNI是一門技術,是Java 與C/C++ 溝通的一門技術。首先,來回顧下Android的系統架構圖。
咱們來簡單介紹下每一層的做用。html
因爲Android 系統是基礎Linux 內核構建的,因此Linux是Android系統的基礎。事實上,Android 的硬件驅動、進程管理、內存管理、網絡管理都是在這一層。java
硬件抽象層(Hardware Abstraction Layer縮寫),硬件抽象層主要爲上層提供標準顯示界面,並向更高級別的 Java API 框架提供顯示設備硬件功能。HAL 包含多個庫模塊,其中每一個模塊都爲特定類型的硬件組件實現一個界面,例如相機或藍牙模塊。當框架 API 要求訪問設備硬件時,Android 系統將爲該硬件組件加載對應的庫模塊。linux
Android 5.0(API 21)以前,使用的是Dalvik虛擬機,以後被ART所取代。ART是Android操做系統的運行環境,經過運行虛擬機來執行dex文件。其中,dex文件是專爲安卓設計的的字節碼格式,Android打包和運行的就是dex文件,而Android toolchain(一種編譯工具)能夠將Java代碼編譯爲dex字節碼格式,轉化過程以下圖。
如上所示,Jack就是一種編譯工具鏈,能夠將Java 源代碼編譯爲 DEX 字節碼,使其可在 Android 平臺上運行。android
不少核心 Android 系統組件和服務都是使用C 和 C++ 編寫的,爲了方便開發者調用這些原生庫功能,Android的Framework提供了調用相應的API。例如,您能夠經過 Android 框架的 Java OpenGL API 訪問 OpenGL ES,以支持在應用中繪製和操做 2D 和 3D 圖形。shell
Android平臺最經常使用的組件和服務都在這一層,是每一個Android開發者必須熟悉和掌握的一層,是應用開發的基礎。數組
Android系統App,如電子郵件、短信、日曆、互聯網瀏覽和聯繫人等系統應用。咱們能夠像調用Java API Framework層同樣直接調用系統的App。網絡
接下來咱們看一下如何編寫Android JNI ,以及須要的流程。數據結構
NDK(Native Development Kit縮寫)一種基於原生程序接口的軟件開發工具包,可讓您在 Android 應用中利用 C 和 C++ 代碼的工具。經過此工具開發的程序直接在本地運行,而不是虛擬機。架構
在Android中,NDK是一系列工具的集合,主要用於擴展Android SDK。NDK提供了一系列的工具能夠幫助開發者快速的開發C或C++的動態庫,並能自動將so和Java應用一塊兒打包成apk。同時,NDK還集成了交叉編譯器,並提供了相應的mk文件隔離CPU、平臺、ABI等差別,開發人員只須要簡單修改mk文件(指出「哪些文件須要編譯」、「編譯特性要求」等),就能夠建立出so文件。
建立NDK工程以前,請先保證本地已經搭建好了NDK的相關環境。依次選擇【Preferences...】->【Android SDK】下載配置NDK,以下所示。
而後,新建一個Native C++工程,以下所示。
而後勾選【Include C++ support】選項,點擊【下一步】,到達【Customize C++ Support】設置頁,以下所示。
而後,點擊【Finish】按鈕便可。
打開新建的NDK工程,目錄以下圖所示。
咱們接下來看一下,Android的NDK工程和普通的Android應用工程有哪些不同的地方。首先,咱們來看下build.gradle配置。
apply plugin: 'com.android.application' android { compileSdkVersion 30 buildToolsVersion "30.0.2" defaultConfig { applicationId "com.xzh.ndk" minSdkVersion 16 targetSdkVersion 30 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { cmake { cppFlags "" } } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } externalNativeBuild { cmake { path "src/main/cpp/CMakeLists.txt" version "3.10.2" } } } dependencies { // 省略引用的第三方庫 }
能夠看到,相比普通的Android應用,build.gradle配置中多了兩個externalNativeBuild配置項。其中,defaultConfig裏面的的externalNativeBuild主要是用於配置Cmake的命令參數,而外部的
externalNativeBuild的主要是定義了CMake的構建腳本CMakeLists.txt的路徑。
而後,咱們來看一下CMakeLists.txt文件,CMakeLists.txt是CMake的構建腳本,做用至關於ndk-build中的Android.mk,代碼以下。
# 設置Cmake最小版本 cmake_minimum_required(VERSION 3.4.1) # 編譯library add_library( # 設置library名稱 native-lib # 設置library模式 # SHARED模式會編譯so文件,STATIC模式不會編譯 SHARED # 設置原生代碼路徑 src/main/cpp/native-lib.cpp ) # 定位library find_library( # library名稱 log-lib # 將library路徑存儲爲一個變量,能夠在其餘地方用這個變量引用NDK庫 # 在這裏設置變量名稱 log ) # 關聯library target_link_libraries( # 關聯的library native-lib # 關聯native-lib和log-lib ${log-lib} )
關於CMake的更多知識,能夠查看CMake官方手冊。
默認建立Android NDK工程時,Android提供了一個簡單的JNI交互示例,返回一個字符串給Java層,方法名的格式爲:Java_包名_類名_方法名
。首先,咱們看一下native-lib.cpp的代碼。
#include <jni.h> #include <string> extern "C" JNIEXPORT jstring JNICALL Java_com_xzh_ndk_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); }
而後,咱們在看一下Android的MainActivity.java 的代碼。
package com.xzh.ndk; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; 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(); }
extern "C" JNIEXPORT void JNICALL Java_com_xfhy_jnifirst_MainActivity_callJavaMethod(JNIEnv *env, jobject thiz) { }
函數命名規則: Java_類全路徑_方法名
,涉及的參數的含義以下:
首先,咱們在Java代碼裏編寫一個native方法聲明,而後使用【alt+enter】快捷鍵讓AS幫助咱們建立一個native方法,以下所示。
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); //對應的Native代碼 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) { }
下面,咱們整理下Java和JNI的類型對照表,以下所示。
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 |
|jjava.lang.Object[] | jobjectArray |
|Byte[]| jbyteArray |
|Char[] | jcharArray |
|Short[] | jshortArray |
|int[] | jintArray |
|long[] | jlongArray |
|float[] | jfloatArray |
|double[] | jdoubleArray |
Native的基本數據類型其實就是將C/C++中的基本類型用typedef從新定義了一個新的名字,在JNI中能夠直接訪問,以下所示。
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 */
若是使用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。
JNI會把Java中全部對象當作一個C指針傳遞到本地方法中,這個指針指向JVM內部數據結構,而內部的數據結構在內存中的存儲方式是不可見的.只能從JNIEnv指針指向的函數表中選擇合適的JNI函數來操做JVM中的數據結構。
好比native訪問java.lang.String 對應的JNI類型jstring時,不能像訪問基本數據類型那樣使用,由於它是一個Java的引用類型,因此在本地代碼中只能經過相似GetStringUTFChars這樣的JNI函數來訪問字符串的內容。
//調用 String result = operateString("待操做的字符串"); Log.d("xfhy", result); //定義 public native String operateString(String str);
而後在C中進行實現,代碼以下。
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); }
在上面的代碼中,operateString函數接收一個jstring類型的參數str,jstring是指向JVM內部的一個字符串,不能直接使用。首先,須要將jstring轉爲C風格的字符串類型char*後才能使用,這裏必須使用合適的JNI函數來訪問JVM內部的字符串數據結構。
GetStringUTFChars(jstring string, jboolean* isCopy)對應的參數的含義以下:
Java中默認是使用Unicode編碼,C/C++默認使用UTF編碼,因此在native層與java層進行字符串交流的時候須要進行編碼轉換。GetStringUTFChars就恰好能夠把jstring指針(指向JVM內部的Unicode字符序列)的字符串轉換成一個UTF-8格式的C字符串。
在使用GetStringUTFChars的時候,返回的值可能爲NULL,這時須要處理一下,不然繼續往下面走的話,使用這個字符串的時候會出現問題.由於調用這個方法時,是拷貝,JVM爲新生成的字符串分配內存空間,當內存空間不夠分配的時候就會致使調用失敗。調用失敗就會返回NULL,並拋出OutOfMemoryError。JNI遇到未決的異常不會改變程序的運行流程,仍是會繼續往下走。
native不像Java,咱們須要手動釋放申請的內存空間。GetStringUTFChars調用時會新申請一塊空間用來裝拷貝出來的字符串,這個字符串用來方便native代碼訪問和修改之類的。既然有內存分配,那麼就必須手動釋放,釋放方法是ReleaseStringUTFChars。能夠看到和GetStringUTFChars是一一對應配對的。
使用NewStringUTF函數能夠構建出一個jstring,須要傳入一個char *類型的C字符串。它會構建一個新的java.lang.String字符串對象,而且會自動轉換成Unicode編碼。若是JVM不能爲構造java.lang.String分配足夠的內存,則會拋出一個OutOfMemoryError異常並返回NULL。
strcat(buff, "xfhy");
將xfhy添加到buff的末尾。一般,GetStringUTFRegion會進行越界檢查,越界會拋StringIndexOutOfBoundsException異常。GetStringUTFRegion其實和GetStringUTFChars有點類似,可是GetStringUTFRegion內部不會分配內存,不會拋出內存溢出異常。因爲其內部沒有分配內存,因此也沒有相似Release這樣的函數來釋放資源。
基本類型數組就是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層數組中並進行求和。
接下來,咱們來看另外一種求和方式,代碼以下。
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則是返回原始數組指針。
對象數組中的元素是一個類的實例或其餘數組的引用,不能直接訪問Java傳遞給JNI層的數組。操做對象數組稍顯複雜,下面舉一個例子:在native層建立一個二維數組,且賦值並返回給Java層使用。
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_xzh_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; }
接下來,咱們來分析下代碼。
熟悉JVM的都應該知道,在JVM中運行一個Java程序時,會先將運行時須要用到的全部相關class文件加載到JVM中,並按需加載,提升性能和節約內存。當咱們調用一個類的靜態方法以前,JVM會先判斷該類是否已經加載,若是沒有被ClassLoader加載到JVM中,會去classpath路徑下查找該類。找到了則加載該類,沒有找到則報ClassNotFoundException異常。
首先,咱們編寫一個MyJNIClass.java類,代碼以下。
public class MyJNIClass { public int age = 30; 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_xzh_allinone_jni_CallMethodActivity_callJavaStaticMethod(JNIEnv *env, jobject thiz) { //調用某個類的static方法 //1. 從classpath路徑下搜索MyJNIClass這個類,並返回該類的Class對象 jclass clazz = env->FindClass("com/xzh/jni/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("我是xzh"); 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調用Java靜態方法仍是比較簡單的,主要會經歷如下幾個步驟。
接下來,咱們來看一下在Native層建立Java實例並調用該實例的方法,大體上是和上面調用靜態方法差很少的。首先,咱們修改下cpp文件的代碼,以下所示。
extern "C" JNIEXPORT void JNICALL Java_com_xzh_allinone_jni_CallMethodActivity_createAndCallJavaInstanceMethod(JNIEnv *env, jobject thiz) { jclass clazz = env->FindClass("com/xzh/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); }
如上所示,Native調用Java實例方法的步驟以下:
<init>
,而後後面是方法簽名。因爲NDK大部分的邏輯是在C/C++完成的,當NDK發生錯誤某種致命的錯誤的時候致使APP閃退。對於這類錯誤問題是很是很差排查的,好比內存地址訪問錯誤、使用野指針、內存泄露、堆棧溢出等native錯誤都會致使APP崩潰。
雖然這些NDK錯誤很差排查,可是咱們在NDK錯誤發生後也不是毫無辦法可言。具體來講,當拿到Logcat輸出的堆棧日誌,再結合addr2line和ndk-stack兩款調試工具,就能夠很夠精確地定位到相應發生錯誤的代碼行數,進而迅速找到問題。
首先,咱們打開ndk目錄下下的sdk/ndk/21.0.6113669/toolchains/目錄,能夠看到NDK交叉編譯器工具鏈的目錄結構以下所示。
而後,咱們再看一下ndk的文件目錄,以下所示。
其中,ndk-stack放在$NDK_HOME目錄下,與ndk-build同級目錄。addr2line在ndk的交叉編譯器工具鏈目錄下。同時,NDK針對不一樣的CPU架構實現了多套工具,在使用addr2line工具時,須要根據當前手機cpu架構來選擇。好比,個人手機是aarch64的,那麼須要使用aarch64-linux-android-4.9
目錄下的工具。Android NDK提供了查看手機的CPU信息的命令,以下所示。
adb shell cat /proc/cpuinfo
在正式介紹兩款調試工具以前,咱們能夠先寫好崩潰的native代碼方便咱們查看效果。首先,咱們修復native-lib.cpp裏面的代碼,以下所示。
void willCrash() { JNIEnv *env = NULL; int version = env->GetVersion(); } extern "C" JNIEXPORT void JNICALL Java_com_xzh_allinone_jni_CallMethodActivity_nativeCrashTest(JNIEnv *env, jobject thiz) { LOGI("崩潰前"); willCrash(); //後面的代碼是執行不到的,由於崩潰了 LOGI("崩潰後"); printf("oooo"); }
上面的這段代碼是很明顯的空指針異常,運行後錯誤日誌以下。
2020-10-07 17:05:25.230 12340-12340/? A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** 2020-10-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-10-07 17:05:25.230 12340-12340/? A/DEBUG: Revision: '0' 2020-10-07 17:05:25.230 12340-12340/? A/DEBUG: ABI: 'arm64' 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: Timestamp: 2020-06-07 17:05:25+0800 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: pid: 11527, tid: 11527, name: m.xfhy.allinone >>> com.xfhy.allinone <<< 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: uid: 10319 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: Cause: null pointer dereference 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: x0 0000000000000000 x1 0000007fd29ffd40 x2 0000000000000005 x3 0000000000000003 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: x4 0000000000000000 x5 8080800000000000 x6 fefeff6fb0ce1f1f x7 7f7f7f7fffff7f7f 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: x8 0000000000000000 x9 a95a4ec0adb574df x10 0000007fd29ffee0 x11 000000000000000a 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: x12 0000000000000018 x13 ffffffffffffffff x14 0000000000000004 x15 ffffffffffffffff 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: x16 0000006fc6476c50 x17 0000006fc64513cc x18 00000070b21f6000 x19 000000702d069c00 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: x20 0000000000000000 x21 000000702d069c00 x22 0000007fd2a00720 x23 0000006fc6ceb127 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: x24 0000000000000004 x25 00000070b1cf2020 x26 000000702d069cb0 x27 0000000000000001 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: x28 0000007fd2a004b0 x29 0000007fd2a00420 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: sp 0000007fd2a00410 lr 0000006fc64513bc pc 0000006fc64513e0 2020-10-07 17:05:25.788 12340-12340/? A/DEBUG: backtrace: 2020-10-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-10-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-10-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-10-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-10-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
,可是咱們不知道發生在具體哪裏,因此接下來咱們須要藉助addr2line和ndk-stack兩款工具來協助咱們進行分析。
如今,咱們使用工具addr2line來定位位置。首先,執行以下命令。
/Users/xzh/development/sdk/ndk/21.0.6113669/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line -e /Users/xzh/development/AllInOne/app/libnative-lib.so 00000000000113e0 00000000000113b8 做者:瀟風寒月 連接:https://juejin.im/post/6844904190586650632 來源:掘金 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
其中-e是指定so文件的位置,而後末尾的00000000000113e0和00000000000113b8是出錯位置的彙編指令地址。
/Users/xzh/development/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/jni.h:497 /Users/xzh/development/AllInOne/app/src/main/cpp/native-lib.cpp:260
能夠看到,是native-lib.cpp的260行出的問題,咱們只須要找到這個位置而後修復這個文件便可。
除此以外,還有一種更簡單的方式,直接輸入命令。
adb logcat | ndk-stack -sym /Users/xzh/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/xzh/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/xzh/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_xzh_allinone_jni_CallMethodActivity_nativeCrashTest+84) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d) Java_com_xzh_allinone_jni_CallMethodActivity_nativeCrashTest /Users/xzh/development/AllInOne/app/src/main/cpp/native-lib.cpp:267:5
能夠看到,上面的日誌明確指出了是willCrash()方法出的錯,它的代碼行數是260行。
衆所周知,Java在新建立對象的時候,不須要考慮JVM是怎麼申請內存的,也不須要在使用完以後去釋放內存。而C++不一樣,須要咱們手動申請和釋放內存(new->delete,malloc->free)。在使用JNI時,因爲本地代碼不能直接經過引用操做JVM內部的數據結構,要進行這些操做必須調用相應的JNI接口間接操做JVM內部的數據內容。咱們不須要關心JVM中對象的是如何存儲的,只須要學習JNI中的三種不一樣引用便可。
一般,本地函數中經過NewLocalRef或調用FindClass、NewObject、GetObjectClass、NewCharArray等建立的引用,就是局部引用。局部引用具備以下一些特徵:
一般是在函數中建立並使用的就是局部引用, 局部引用在函數返回以後會自動釋放。那麼咱們爲啥還須要去手動調用DeleteLocalRef進行釋放呢?
好比,開了一個for循環,裏面不斷地建立局部引用,那麼這時就必須得使用DeleteLocalRef手動釋放內存。否則局部引用會愈來愈多,最終致使崩潰(在Android低版本上局部引用表的最大數量有限制,是512個,超過則會崩潰)。
還有一種狀況,本地方法返回一個引用到Java層以後,若是Java層沒有對返回的局部引用使用的話,局部引用就會被JVM自動釋放。
全局引用是基於局部引用建立的,使用NewGlobalRef方法建立。全局引用具備以下一些特性:
弱全局引用是基於局部引用或者全局引用建立的,使用NewWeakGlobalRef方法建立。弱全局引用具備以下一些特性:
參考:
Android Developers NDK 指南 C++ 庫支持
JNI/NDK開發指南
Android 內存泄露之jni local reference table overflow