Android NDK開發入門

JNI 簡介

JNI (Java Native Interface英文縮寫),譯爲Java本地接口。是Java衆多開發技術中的一門技術,意在利用本地代碼,爲Java程序提供更高效、更靈活的拓展。儘管Java一向以其良好的跨平臺性而著稱,但真正的跨平臺非C/C++莫屬,由於當前世上90%的系統都是基於C/C++編寫的。同時,Java的跨平臺是以犧牲效率換來對多種平臺的兼容性,於是JNI就是這種跨平臺的主流實現方式之一。javascript

總之,JNI是一門技術,是Java 與C/C++ 溝通的一門技術。首先,來回顧下Android的系統架構圖。
在這裏插入圖片描述
咱們來簡單介紹下每一層的做用。html

Linux層

Linux 內核

因爲Android 系統是基礎Linux 內核構建的,因此Linux是Android系統的基礎。事實上,Android 的硬件驅動、進程管理、內存管理、網絡管理都是在這一層。java

硬件抽象層

硬件抽象層(Hardware Abstraction Layer縮寫),硬件抽象層主要爲上層提供標準顯示界面,並向更高級別的 Java API 框架提供顯示設備硬件功能。HAL 包含多個庫模塊,其中每一個模塊都爲特定類型的硬件組件實現一個界面,例如相機或藍牙模塊。當框架 API 要求訪問設備硬件時,Android 系統將爲該硬件組件加載對應的庫模塊。linux

系統運行庫和運行環境層

Android Runtime

Android 5.0(API 21)以前,使用的是Dalvik虛擬機,以後被ART所取代。ART是Android操做系統的運行環境,經過運行虛擬機來執行dex文件。其中,dex文件是專爲安卓設計的的字節碼格式,Android打包和運行的就是dex文件,而Android toolchain(一種編譯工具)能夠將Java代碼編譯爲dex字節碼格式,轉化過程以下圖。
在這裏插入圖片描述
如上所示,Jack就是一種編譯工具鏈,能夠將Java 源代碼編譯爲 DEX 字節碼,使其可在 Android 平臺上運行。android

原生C/C++ 庫

不少核心 Android 系統組件和服務都是使用C 和 C++ 編寫的,爲了方便開發者調用這些原生庫功能,Android的Framework提供了調用相應的API。例如,您能夠經過 Android 框架的 Java OpenGL API 訪問 OpenGL ES,以支持在應用中繪製和操做 2D 和 3D 圖形。shell

應用程序框架層

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開發流程

  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_類全路徑_方法名,涉及的參數的含義以下:

  • 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 其餘字符串操做函數
  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函數。如strcat(buff, "xfhy"); 將xfhy添加到buff的末尾。
  5. GetStringCritical和ReleaseStringCritical: 爲了增長直接傳回指向Java字符串的指針的可能性(而不是拷貝).在這2個函數之間的區域,是絕對不能調用其餘JNI函數或者讓線程阻塞的native函數.不然JVM可能死鎖. 若是有一個字符串的內容特別大,好比1M,且只須要讀取裏面的內容打印出來,此時比較適合用該對函數,可直接返回源字符串的指針。
  6. 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;
}

接下來,咱們來分析下代碼。

  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引用表溢出。

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靜態方法仍是比較簡單的,主要會經歷如下幾個步驟。

  1. 首先,調用FindClass函數傳入Class描述符(Java類的全類名,這裏在AS中輸入MyJNIClass時會有提示補全,直接enter便可補全),找到該類並獲得jclass類型。
  2. 而後,經過GetStaticMethodID找到該方法的id,傳入方法簽名,獲得jmethodID類型的引用。
  3. 構建入參,而後調用CallStaticObjectMethod去調用Java類裏面的靜態方法,而後傳入參數,返回的直接就是Java層返回的數據。其實,這裏的CallStaticObjectMethod是調用的引用類型的靜態方法,與之類似的還有:CallStaticVoidMethod(無返參),CallStaticIntMethod(返參是Int),CallStaticFloatMethod等。
  4. 移除局部引用。

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實例方法的步驟以下:

  1. Native調用Java實例方法。
  2. 獲取構造方法的id,獲取須要調用方法的id。其中獲取構造方法時,方法名稱固定寫法就是<init>,而後後面是方法簽名。
  3. 使用NewObject()函數構建一個Java對象。
  4. 調用Java對象的setAge和getAge方法,獲取返回值,打印結果。
  5. 刪除引用。

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 Developers NDK 指南 C++ 庫支持
JNI/NDK開發指南
Android 內存泄露之jni local reference table overflow

相關文章
相關標籤/搜索