JNI 簡介
JNI (Java Native Interface英文縮寫),譯為Java本地接口。是Java眾多開發技術中的一門技術,意在利用本地代碼,為Java程序提供更高效、更靈活的拓展。盡管Java一貫以其良好的跨平臺性而著稱,但真正的跨平臺非C/C++莫屬,因為當前世上90%的系統都是基于C/C++編寫的。同時,Java的跨平臺是以犧牲效率換來對多種平臺的兼容性,因而JNI就是這種跨平臺的主流實現方式之一。
總之,JNI是一門技術,是Java 與C/C++ 溝通的一門技術。首先,來回顧下Android的系統架構圖。
![<meta charset="utf-8">
<article class="_2rhmJa" deep="5">
JNI 簡介
JNI (Java Native Interface英文縮寫),譯為Java本地接口。是Java眾多開發技術中的一門技術,意在利用本地代碼,為Java程序提供更高效、更靈活的拓展。盡管Java一貫以其良好的跨平臺性而著稱,但真正的跨平臺非C/C++莫屬,因為當前世上90%的系統都是基于C/C++編寫的。同時,Java的跨平臺是以犧牲效率換來對多種平臺的兼容性,因而JNI就是這種跨平臺的主流實現方式之一。
總之,JNI是一門技術,是Java 與C/C++ 溝通的一門技術。首先,來回顧下Android的系統架構圖。
我們來簡單介紹下每一層的作用。
Linux層
Linux 內核
由于Android 系統是基礎Linux 內核構建的,所以Linux是Android系統的基礎。事實上,Android 的硬件驅動、進程管理、內存管理、網絡管理都是在這一層。
硬件抽象層
硬件抽象層(Hardware Abstraction Layer縮寫),硬件抽象層主要為上層提供標準顯示界面,并向更高級別的 Java API 框架提供顯示設備硬件功能。HAL 包含多個庫模塊,其中每個模塊都為特定類型的硬件組件實現一個界面,例如相機或藍牙模塊。當框架 API 要求訪問設備硬件時,Android 系統將為該硬件組件加載對應的庫模塊。
系統運行庫和運行環境層
Android Runtime
Android 5.0(API 21)之前,使用的是Dalvik虛擬機,之后被ART所取代。ART是Android操作系統的運行環境,通過運行虛擬機來執行dex文件。其中,dex文件是專為安卓設計的的字節碼格式,Android打包和運行的就是dex文件,而Android toolchain(一種編譯工具)可以將Java代碼編譯為dex字節碼格式,轉化過程如下圖。
如上所示,Jack就是一種編譯工具鏈,可以將Java 源代碼編譯為 DEX 字節碼,使其可在 Android 平臺上運行。
原生C/C++ 庫
很多核心 Android 系統組件和服務都是使用C 和 C++ 編寫的,為了方便開發者調用這些原生庫功能,Android的Framework提供了調用相應的API。例如,您可以通過 Android 框架的 Java OpenGL API 訪問 OpenGL ES,以支持在應用中繪制和操作 2D 和 3D 圖形。
應用程序框架層
Android平臺最常用的組件和服務都在這一層,是每個Android開發者必須熟悉和掌握的一層,是應用開發的基礎。
Application層
Android系統App,如電子郵件、短信、日歷、互聯網瀏覽和聯系人等系統應用。我們可以像調用Java API Framework層一樣直接調用系統的App。
接下來我們看一下如何編寫Android JNI ,以及需要的流程。
NDK
NDK是什么
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工程之前,請先保證本地已經搭建好了NDK的相關環境。依次選擇【Preferences...】->【Android SDK】下載配置NDK,如下所示。
然后,新建一個Native C++工程,如下所示。
然后勾選【Include C++ support】選項,點擊【下一步】,到達【Customize C++ Support】設置頁,如下所示。
然后,點擊【Finish】按鈕即可。
NDK 項目目錄
打開新建的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();
}
初識Android JNI
1,JNI開發流程
- 編寫java類,聲明了native方法;
- 編寫native代碼;
- 將native代碼編譯成so文件;
- 在java類中引入so庫,調用native方法;
2,native方法命名
extern "C"
JNIEXPORT void JNICALL
Java_com_xfhy_jnifirst_MainActivity_callJavaMethod(JNIEnv *env, jobject thiz) {
}
函數命名規則: Java_類全路徑_方法名
,涉及的參數的含義如下:
- JNIEnv*是定義任意native函數的第一個參數,表示指向JNI環境的指針,可以通過它來訪問JNI提供的接口方法。
- jobject表示Java對象中的this,如果是靜態方法則表示jclass。
- JNIEXPORT和JNICALL: 它們是JNI中所定義的宏,可以在jni.h這個頭文件中查找到。
3,JNI數據類型與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 |
3.1基本數據類型
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 */
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);
然后在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);
}
4.2.1 native中獲取JVM字符串
在上面的代碼中,operateString函數接收一個jstring類型的參數str,jstring是指向JVM內部的一個字符串,不能直接使用。首先,需要將jstring轉為C風格的字符串類型char*后才能使用,這里必須使用合適的JNI函數來訪問JVM內部的字符串數據結構。
GetStringUTFChars(jstring string, jboolean* isCopy)對應的參數的含義如下:
- string : jstring,Java傳遞給native代碼的字符串指針。
- isCopy : 一般情況下傳NULL,取值可以是JNI_TRUE和JNI_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 其他字符串操作函數
- GetStringChars和ReleaseStringChars:這對函數和Get/ReleaseStringUTFChars函數功能類似,用于獲取和釋放的字符串是以Unicode格式編碼的。
- GetStringLength:獲取Unicode字符串(jstring)的長度。 UTF-8編碼的字符串是以0結尾,而Unicode的不是,所以這里需要單獨區分開。
- 「GetStringUTFLength」: 獲取UTF-8編碼字符串的長度,就是獲取C/C++默認編碼字符串的長度.還可以使用標準C函數「strlen」來獲取其長度。
- strcat: 拼接字符串,標準C函數。如
strcat(buff, "xfhy");
將xfhy添加到buff的末尾。 - GetStringCritical和ReleaseStringCritical: 為了增加直接傳回指向Java字符串的指針的可能性(而不是拷貝).在這2個函數之間的區域,是絕對不能調用其他JNI函數或者讓線程阻塞的native函數.否則JVM可能死鎖. 如果有一個字符串的內容特別大,比如1M,且只需要讀取里面的內容打印出來,此時比較適合用該對函數,可直接返回源字符串的指針。
- GetStringRegion和GetStringUTFRegion: 獲取Unicode和UTF-8字符串中指定范圍的內容(如: 只需要1-3索引處的字符串),這對函數會將源字符串復制到一個預先分配的緩沖區(自己定義的char數組)內。
通常,GetStringUTFRegion會進行越界檢查,越界會拋StringIndexOutOfBoundsException異常。GetStringUTFRegion其實和GetStringUTFChars有點相似,但是GetStringUTFRegion內部不會分配內存,不會拋出內存溢出異常。由于其內部沒有分配內存,所以也沒有類似Release這樣的函數來釋放資源。
4.2.6 小結
- Java字符串轉C/C++字符串: 使用GetStringUTFChars函數,必須調用ReleaseStringUTFChars釋放內存。
- 創建Java層需要的Unicode字符串,使用NewStringUTF函數。
- 獲取C/C++字符串長度,使用GetStringUTFLength或者strlen函數。
- 對于小字符串,GetStringRegion和GetStringUTFRegion這2個函數是最佳選擇,因為緩沖區數組可以被編譯器提取分配,不會產生內存溢出的異常。當只需要處理字符串的部分數據時,也還是不錯。它們提供了開始索引和子字符串長度值,復制的消耗也是非常小
- 獲取Unicode字符串和長度,使用GetStringChars和GetStringLength函數。
數組操作
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層數組中并進行求和。
接下來,我們來看另一種求和方式,代碼如下。
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則是返回原始數組指針。
5.2 對象數組
對象數組中的元素是一個類的實例或其他數組的引用,不能直接訪問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;
}
接下來,我們來分析下代碼。
- 首先,是利用FindClass函數找到java層int[]對象的class,這個class是需要傳入NewObjectArray創建對象數組的。調用NewObjectArray函數之后,即可創建一個對象數組,大小是size,元素類型是前面獲取到的class。
- 進入for循環構建size個int數組,構建int數組需要使用NewIntArray函數。可以看到我構建了一個臨時的buff數組,然后大小是隨便設置的,這里是為了示例,其實可以用malloc動態申請空間,免得申請100個空間,可能太大或者太小了。整buff數組主要是拿來給生成出來的jintArray賦值的,因為jintArray是Java的數據結構,咱native不能直接操作,得調用SetIntArrayRegion函數,將buff數組的值復制到jintArray數組中。
- 然后調用SetObjectArrayElement函數設置jobjectArray數組中某個索引處的數據,這里將生成的jintArray設置進去。
- 最后需要將for里面生成的jintArray及時移除引用。創建的jintArray是一個JNI局部引用,如果局部引用太多的話,會造成JNI引用表溢出。
6,Native調Java方法
熟悉JVM的都應該知道,在JVM中運行一個Java程序時,會先將運行時需要用到的所有相關class文件加載到JVM中,并按需加載,提高性能和節約內存。當我們調用一個類的靜態方法之前,JVM會先判斷該類是否已經加載,如果沒有被ClassLoader加載到JVM中,會去classpath路徑下查找該類。找到了則加載該類,沒有找到則報ClassNotFoundException異常。
6.1 Native調用Java靜態方法
首先,我們編寫一個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靜態方法還是比較簡單的,主要會經歷以下幾個步驟。
- 首先,調用FindClass函數傳入Class描述符(Java類的全類名,這里在AS中輸入MyJNIClass時會有提示補全,直接enter即可補全),找到該類并得到jclass類型。
- 然后,通過GetStaticMethodID找到該方法的id,傳入方法簽名,得到jmethodID類型的引用。
- 構建入參,然后調用CallStaticObjectMethod去調用Java類里面的靜態方法,然后傳入參數,返回的直接就是Java層返回的數據。其實,這里的CallStaticObjectMethod是調用的引用類型的靜態方法,與之相似的還有:CallStaticVoidMethod(無返參),CallStaticIntMethod(返參是Int),CallStaticFloatMethod等。
- 移除局部引用。
6.2 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實例方法的步驟如下:
- Native調用Java實例方法。
- 獲取構造方法的id,獲取需要調用方法的id。其中獲取構造方法時,方法名稱固定寫法就是
<init>
,然后后面是方法簽名。 - 使用NewObject()函數構建一個Java對象。
- 調用Java對象的setAge和getAge方法,獲取返回值,打印結果。
- 刪除引用。
NDK錯誤定位
由于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兩款工具來協助我們進行分析。
7.1 addr2line
現在,我們使用工具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行出的問題,我們只需要找到這個位置然后修復這個文件即可。
7.2 ndk-stack
除此之外,還有一種更簡單的方式,直接輸入命令。
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行。
8,JNI引用
眾所周知,Java在新創建對象的時候,不需要考慮JVM是怎么申請內存的,也不需要在使用完之后去釋放內存。而C++不同,需要我們手動申請和釋放內存(new->delete,malloc->free)。在使用JNI時,由于本地代碼不能直接通過引用操作JVM內部的數據結構,要進行這些操作必須調用相應的JNI接口間接操作JVM內部的數據內容。我們不需要關心JVM中對象的是如何存儲的,只需要學習JNI中的三種不同引用即可。
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干貨,交流Android技術。
對文章有何見解,或者有何技術問題,都可以在評論區一起留言討論,我會虔誠為你解答。
最后,如果你想知道更多Android的知識或需要其他資料我這里均免費分享,只需你多多支持我即可哦!
——可以直接點這里可以看到全部資料內容免費打包領取。