JNI的一些基礎

Cmake配置

如下介紹所有基於C++ 11html

android {
          ......
            externalNativeBuild {
            cmake {
                //設置 C++ flag,啓用 C++11 可選配置,-frtti 表示項目支持RTTI;(-fno-rtti 表示禁用)
                // -fexceptions 表示當前項目支持C++異常處理
                cppFlags "-std=c++11 -frtti -fexceptions"
                //arguments 語法:-D + 變量,更多變量:https://developer.android.com/ndk/guides/cmake.html
                arguments "-DANDROID_ARM_NEON=TRUE"

            }

        }
        // 指定 ABI
        ndk {

            abiFilters 'arm64-v8a', 'armeabi-v7a','x86'
        }
  .....
}

CMake 編譯NDK 所支持的參數配置

變數名 引數 描述
ANDROID_TOOLCHAIN clang (default) gcc (deprecated) 指定 Cmake 編譯所使用的工具鏈。示例:arguments 「-DANDROID_TOOLCHAIN=clang」
ANDROID_PLATFORM API版本 指定 NDK 所用的安卓平臺的版本是多少。示例:arguments 「-DANDROID_PLATFORM=android-21」
ANDROID_STL gnustl_static(default) 指定 Cmake 編譯所使用的標準模版庫。使用示例:arguments 「-DANDROID_STL=gnustl_static」
ANDROID_PIE ON (android-16以上預設爲ON) OFF (android-15如下預設爲OFF) 使得編譯的elf檔案能夠載入到記憶體中的任意位置就叫pie(position independent executables)。 出於安全保護,在Android 4.4以後可執行檔案必須是採用PIE編譯的。使用示例:arguments 「-DANDROID_PIE=ON」
ANDROID_CPP_FEATURES 空(default) rtti(支持RTTI) exceptions(支持C異常) 指定是否須要支持 RTTI(RunTime Type Information)和 C 的異常,預設爲空。使用示例:arguments 「-DANDROID_CPP_FEATURES=rtti exceptions」
ANDROID_ALLOW_UNDEFINED_SYMBOLS TRUE FALSE(default) 指定在編譯時,若是遇到未定義的引用時是否拋出錯誤。若是要容許這些型別的錯誤,請將該變數設定爲 TRUE。使用示例:arguments 「-DANDROID_ALLOW_UNDEFINED_SYMBOLS=TRUE」
ANDROID_ARM_MODE arm thumb (default) 若是是 thumb 模式,每條指令的寬度是 16 位,若是是 arm 模式,每條指令的寬度是 32 位。示例:arguments 「-DANDROID_ARM_MODE=arm」
ANDROID_ARM_NEON TRUE FALSE(default) 指定在編譯時,是否使用NEON對程式碼進行優化。NEON只適用於armeabi-v7a和x86 ABI,且並不是全部基於ARMv7的Android裝置都支持NEON,但支持的裝置可能會因其支援標量/向量指令而明顯受益。 更多參考:https://developer.android.com...:arguments 「-DANDROID_ARM_NEON=TRUE」
ANDROID_DISABLE_NO_EXECUTE TRUE FALSE(default) 指定在編譯時是否啓動 NX(No eXecute)。NX 是一種應用於 CPU 的技術,幫助防止大多數惡意程式的攻擊。若是要禁用 NX,請將該變數設定爲 TRUE。示例:arguments 「-DANDROID_DISABLE_NO_EXECUTE=TRUE」
ANDROID_DISABLE_RELRO TRUE FALSE(default) RELocation Read-Only (RELRO) 重定位只讀,它可以保護庫函式的呼叫不受攻擊者重定向的影響。若是要禁用 RELRO,請將該變數設定爲 TRUE。使用示例:arguments 「-DANDROID_DISABLE_RELRO=FALSE」
ANDROID_DISABLE_FORMAT_STRING_CHECKS TRUE FALSE(default) 在相似 printf 的方法中使用很是量格式字串時是否拋出錯誤。若是爲 TRUE,即不檢查字串格式。示例:arguments 「-DANDROID_DISABLE_FORMAT_STRING_CHECKS=FALSE」

C庫支持

名稱 說明 功能
libstdc 預設最小系統 C 執行時庫 不適用
gabi _static GAbi 執行時(靜態)。 C 異常和 RTTI
gabi _shared GAbi 執行時(共享)。 C 異常和 RTTI
stlport_static STLport 執行時(靜態)。 C 異常和 RTTI;標準庫
stlport_shared STLport 執行時(共享)。 C 異常和 RTTI;標準庫
gnustl_static GNU STL(靜態)。 C 異常和 RTTI;標準庫
gnustl_shared GNU STL(共享)。 C 異常和 RTTI;標準庫
c _static LLVM libc 執行時(靜態)。 C 異常和 RTTI;標準庫
c _shared LLVM libc 執行時(共享)。 C 異常和 RTTI;標準庫
參考:https://developer.android.com...

數據類型

從一個簡單例子開始,聲明 native 方法以下:java

object NDKLibrary {
    init {
        //加載動態庫,這裏對應 CMakeLists.txt 裏的 add_library NDKSample
        System.loadLibrary("NDKSample")
    }

    //使用 external 關鍵字指示以原生代碼形式實現的方法
    external fun plus(a: Int, b: Int): Int
}

c++:android

cppextern "C"
JNIEXPORT jint JNICALL
Java_tt_reducto_ndksample_NDKLibrary_plus(JNIEnv *env, jobject thiz, jint a, jint b) {
    jint sum = a + b;
    return sum;
}

這是一個簡單的計算 a+b 的 native 方法,在 C++ 層接收來自 kotlin 方法的參數,並轉換成 C++ 層的數據類型,計算以後再返回成 應用層的數據類型。ios

(*env)->方法名(env,參數列表)  //C的語法
env->方法名(參數列表)         //C++的語法

C語言沒有對象的概念,所以要將env指針做爲形參傳入到JNIEnv方法中。c++

C++中const描述的都是一些「運行時常量性」的概念,即具備運行時數據的不可更改性。這與編譯時期的常量性要區別開。git

C++11中對編譯時期常量的回答是constexpr,即常量表達式(constant expression)程序員

基本數據類型轉換
Java 類型 Kotlin類型 Native 類型 符號屬性 字長
boolean kotlin.Boolean jboolean 無符號 8位
byte kotlin.Byte jbyte 無符號 8位
char kotlin.Char jchar 無符號 16位
short kotlin.Short jshort 有符號 16位
int kotlin.Int jnit 有符號 32位
long kotlin.Long jlong 有符號 64位
float kotlin.Float jfloat 有符號 32位
double kotlin.Double jdouble 有符號 64位
引用數據類型轉換
Java 引用類型 Native 類型
All objects jobject
java.lang.Class jclass
java.lang.String jstring
Object[] jobjectArray
boolean[] jbooleanArray
byte[] jbyteArray
java.lang.Throwable jthrowable
char[] jcharArray
short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jdoubleArray

除了 Java 中基本數據類型的數組、Class、String 和 Throwable 外,其他全部 Java 對象的數據類型在 JNI 中都用 jobject 表示。github

在 kotlin 方法中只有兩個參數,在 C++ 代碼就有四個參數了,至少都會包含前面兩個參數express

JNI 定義了兩個關鍵數據結構,即「JavaVM」和「JNIEnv」。二者本質上都是指向函數表的二級指針。(在 C++ 版本中,它們是一些類,這些類具備指向函數表的指針,並具備每一個經過該函數表間接調用的 JNI 函數的成員函數。)JavaVM 提供「調用接口」函數,能夠利用此類來函數建立和銷燬 JavaVM。理論上,每一個進程能夠有多個 JavaVM,但 Android 只容許有一個。編程

JNIEnv 提供了大部分 JNI 函數。原生函數都會收到 JNIEnv 做爲第一個參數。

該 JNIEnv 將用於線程本地存儲。所以,沒法在線程之間共享 JNIEnv。若是一段代碼沒法經過其餘方法獲取本身的 JNIEnv,應該共享相應 JavaVM,而後使用 GetEnv 發現線程的 JNIEnv。

JNIEnv*

定義任意 native 函數的第一個參數,是一個指針,經過它能夠訪問虛擬機內部的各類數據結構,同時它還指向 JVM 函數表的指針,函數表中的每個入口指向一個 JNI 函數,每一個函數用於訪問 JVM 中特定的數據結構。

JNIEnv類型是一個指向所有JNI方法的指針。該指針只在建立它的線程有效,不能跨線程傳遞。其聲明以下:

struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

JNIEnv在C語言環境和C++語言環境中的實現是不同的在C環境下其中方法的聲明方式爲:

struct JNINativeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;
    void*       reserved3;
    
    jint        (*GetVersion)(JNIEnv *);
    ...
};

C++中對其進行了封裝:

struct _JNIEnv {
    /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;

#if defined(__cplusplus)

    jint GetVersion()
    { return functions->GetVersion(this); }

    .........  
#endif /*__cplusplus*/
};

返回值是宏定義的常量,可使用獲取到的值與下列宏進行匹配來知道當前的版本:

#define JNI_VERSION_1_1 0x00010001
#define JNI_VERSION_1_2 0x00010002
#define JNI_VERSION_1_4 0x00010004
#define JNI_VERSION_1_6 0x00010006

JavaVM

JavaVM是虛擬機在JNI中的表示,一個JVM中只有一個JavaVM對象,並且對象是線程共享的。

經過JNIEnv咱們能夠獲取一個Java虛擬機對象,其函數以下:

jint **GetJavaVM**(JNIEnv *env, JavaVM **vm);
  • vm:用來存放得到的虛擬機的指針的指針。
  • return:成功返回0,失敗返回其餘。

JNI中JVM的聲明:

/*
 * JNI invocation interface.
 */
struct JNIInvokeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;

    jint        (*DestroyJavaVM)(JavaVM*);
    jint        (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
    jint        (*DetachCurrentThread)(JavaVM*);
    jint        (*GetEnv)(JavaVM*, void**, jint);
    jint        (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};

JVM的建立:

/*
 * VM initialization functions.
 *
 * Note these are the only symbols exported for JNI by the VM.
 */
jint JNI_GetDefaultJavaVMInitArgs(void*);
jint JNI_CreateJavaVM(JavaVM**, JNIEnv**, void*);
jint JNI_GetCreatedJavaVMs(JavaVM**, jsize, jsize*);

其中JavaVMInitArgs是存放虛擬機參數的結構體,定義以下:

/*
 * JNI 1.2+ initialization.  (As of 1.6, the pre-1.2 structures are no
 * longer supported.)
 */
typedef struct JavaVMOption {
    const char* optionString;
    void*       extraInfo;
} JavaVMOption;

typedef struct JavaVMInitArgs {
    jint        version;    /* use JNI_VERSION_1_2 or later */

    jint        nOptions;
    JavaVMOption* options;
    jboolean    ignoreUnrecognized;
} JavaVMInitArgs;

JNI_CreateJavaVM() 函數給 JavaVM *指針 和 JNIEnv *指針進行賦值。獲得這兩個指針就能夠操縱java了。

示例

#include <dlfcn.h>
#include <jni.h>
typedef int (*JNI_CreateJavaVM_t)(void *, void *, void *);
typedef jint (*registerNatives_t)(JNIEnv* env, jclass clazz);
static int init_jvm(JavaVM **p_vm, JNIEnv **p_env) {
  // https://android.googlesource.com/platform/frameworks/native/+/ce3a0a5/services/surfaceflinger/DdmConnection.cpp
  JavaVMOption opt[4];
  opt[0].optionString = "-Djava.class.path=/data/local/tmp/shim_app.apk";
  opt[1].optionString = "-agentlib:jdwp=transport=dt_android_adb,suspend=n,server=y";
  opt[2].optionString = "-Djava.library.path=/data/local/tmp";
  opt[3].optionString = "-verbose:jni"; // may want to remove this, it's noisy
  JavaVMInitArgs args;
  args.version = JNI_VERSION_1_6;
  args.options = opt;
  args.nOptions = 4;
  args.ignoreUnrecognized = JNI_FALSE;
  void *libdvm_dso = dlopen("libdvm.so", RTLD_NOW);
  void *libandroid_runtime_dso = dlopen("libandroid_runtime.so", RTLD_NOW);
  if (!libdvm_dso || !libandroid_runtime_dso) {
    return -1;
  }
  JNI_CreateJavaVM_t JNI_CreateJavaVM;
  JNI_CreateJavaVM = (JNI_CreateJavaVM_t) dlsym(libdvm_dso, "JNI_CreateJavaVM");
  if (!JNI_CreateJavaVM) {
    return -2;
  }
  registerNatives_t registerNatives;
  registerNatives = (registerNatives_t) dlsym(libandroid_runtime_dso, "Java_com_android_internal_util_WithFramework_registerNatives");
  if (!registerNatives) {
    return -3;
  }
  if (JNI_CreateJavaVM(&(*p_vm), &(*p_env), &args)) {
    return -4;
  }
  if (registerNatives(*p_env, 0)) {
    return -5;
  }
  return 0;
}

......
#include <stdlib.h>
#include <stdio.h>
JavaVM * vm = NULL;
JNIEnv * env = NULL;
int status = init_jvm( & vm, & env);
if (status == 0) {
  printf("Initialization success (vm=%p, env=%p)\n", vm, env);
} else {
  printf("Initialization failure (%i)\n", status);
  return -1;
}
jstring testy = (*env)->NewStringUTF(env, "this should work now!");
const char *str = (*env)->GetStringUTFChars(env, testy, NULL);
printf("testy: %s\n", str);

上面說了JNIEnv指針僅在建立它的線程有效。若是須要在其餘線程訪問JVM,那麼必須先調用AttachCurrentThread將當前線程與JVM進行關聯,而後才能得到JNIEnv對象。而後在必要時須要調用DetachCurrentThread來解除連接。

jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
    { return functions->AttachCurrentThread(this, p_env, thr_args); }

解除與虛擬機的鏈接:

jint DetachCurrentThread()
    { return functions->DetachCurrentThread(this); }

卸載虛擬機:

jint DestroyJavaVM()
    { return functions->DestroyJavaVM(this); }

還有動態加載本地方法的兩個函數:

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved);

這個之後講利用JNI保護私密字符串會用到....

C++11

簡單說下須要用到的一些點..

Unicode編碼

看資料常常有人這麼說:

Java 默認使用 Unicode 編碼,而 Native 層是 C/C++ ,默認使用 UTF 編碼。

這是由於通常人經常把UTF-16和Unicode混爲一談,咱們在閱讀各類資料的時候要注意區別。

Dalvik 中,String 對象編碼方式爲 utf-16 編碼;

ART 中,String 對象編碼方式爲 utf-16 編碼,可是有一個狀況除外:若是 String 對象所有爲 ASCII 字符而且 Android 系統爲 8.0 及之上版本,String 對象的編碼則爲 utf-8;

咱們稱ISO/Unicode所定義的字符集爲Unicode。在Unicode中,每一個字符佔據一個碼位(Code point)。Unicode字符集總共定義了1114 112個這樣的碼位,使用從0到10FFFF的十六進制數惟一地表示全部的字符。不過不得不提的是,雖然字符集中的碼位惟一,但因爲計算機存儲數據一般是以字節爲單位的,並且出於兼容以前的ASCII、大數小段數段、節省存儲空間等諸多緣由,一般狀況下,咱們須要一種具體的編碼方式來對字符碼位進行存儲。比較常見的基於Unicode字符集的編碼方式有UTF-八、UTF-16及UTF-32。以UTF-8爲例,其採用了1~6字節的變長編碼方式編碼Unicode,英文一般使用1字節表示,且與ASCII是兼容的,而中文經常使用3字節進行表示。UTF-8編碼因爲較爲節約存儲空間,所以使用得比較普遍。

UTF-8的編碼方式:

Unicode符號範圍(十六進制) UTF-8編碼範圍(二進制) byte數
0000 0000——0000 007F 0xxxxxxx 1
0000 0080——0000 07FF 110xxxxx 10xxxxxx 2
0000 0800——0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx 3
0010 0000——0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 4

單字節有效位數爲7,第一位始終爲0。對於以ASCII編碼的字符串能夠直接當作UTF-8字符串使用。

對於空字符其表示爲\u0000

雙字節字符在UTF-8中使用兩個字節存放,且字符的開頭爲11 表示這個一個雙字節字符:

  • 高位: 110xxxxx
  • 低位: 10xxxxxx

對於須要三個字節表示的字符,其最高位使用111表示該字符的字節數:

  • 高位: 1110xxxx
  • 中位: 10xxxxxx
  • 低位: 10xxxxxx

GB2312的出現先於Unicode。早在20世紀80年代,GB2312做爲簡體中文的國家標準被頒佈使用。GB2312字符集收入6763個漢字和682個非漢字圖形字符,而在編碼上,是採用了基於區位碼的一種編碼方式,採用2字節表示一箇中文字符。GB2312在中國大陸地區及新加坡都有普遍的使用。

BIG5俗稱「大五碼」。是長期以來的繁體中文的業界標準,共收錄了13060箇中文字,也採用了2字節的方式來表示繁體中文。BIG5在中國臺灣、香港、澳門等地區有着普遍的使用。

在C++98標準中,爲了支持Unicode,定義了「寬字符」的內置類型wchar_t。在Windows上,多數wchar_t被實現爲16位寬,而在Linux上,則被實現爲32位。事實上,C++98標準定義中,wchar_t的寬度是由編譯器實現決定的。理論上,wchar_t的長度能夠是8位、16位或者32位。這樣帶來的最大的問題是,程序員寫出的包含wchar_t的代碼一般不可移植。

C++11爲了解決了Unicode類型數據的存儲問題而引入如下兩種新的內置數據類型來存儲不一樣編碼長度的Unicode數據。

  • char16_t:用於存儲UTF-16編碼的Unicode數據。
  • char32_t:用於存儲UTF-32編碼的Unicode數據。

至於UTF-8編碼的Unicode數據,C++11仍是使用8字節寬度的char類型的數組來保存。而char16_t和char32_t的長度則猶如其名稱所顯示的那樣,長度分別爲16字節和32字節,對任何編譯器或者系統都是同樣的。此外,C++11還定義了一些常量字符串的前綴。在聲明常量字符串的時候,這些前綴聲明可讓編譯器使字符串按照前綴類型產生數據。

C++11一共定義了3種這樣的前綴:

  • u8表示爲UTF-8編碼。
  • u表示爲UTF-16編碼。
  • U表示爲UTF-32編碼。

對於Unicode編碼字符的書寫,C++11中還規定了一些簡明的方式,即在字符串中用'u'加4個十六進制數編碼的Unicode碼位(UTF-16)來標識一個Unicode字符。好比'u4F60'表示的就是Unicode中的中文字符「你」,而'u597D'則是Unicode中的「好」。此外,也能夠經過'U'後跟8個十六進制數編碼的Unicode碼位(UTF-32)的方式來書寫Unicode字面常量。須要看更多Unicode碼位的編碼能夠去找下免費提供中文轉Unicode的在線轉換服務的網站。

看個簡單例子:

#include <iostream>
using namespace std;

int main(int argc, const char * argv[]) {
    

    // 不一樣編碼下的Unicode字符串的大小
    
    // 中文 你好啊
    char utf8[]  = u8"\u4F60\u597D\u554A";
    
    char16_t utf16[]  = u"\u4F60\u597D\u554A";
    // 輸出中文
    cout << utf8 <<endl;
    //打印長度
    cout << sizeof(utf8) <<endl;
    cout << sizeof(utf16) <<endl;
    //
    cout << utf8[1] <<endl;
    cout << utf16[1] <<endl;
    return 0;
}

輸出:

你好啊
10
8
\275
22909
Program ended with exit code: 0

能夠看到因爲utf-8採用了變長編碼,每一箇中文字符編碼爲3字節,再加上'0'的字符串終止符,因此UTF-8變量大小爲10字節,而UTF-16採用的是定長編碼,因此佔了8字節空間。

這裏看到 utf8[1]輸出不正確,由於UTF-8是不能直接數組式訪問。這裏直接指向了第一個UTF-8字符3字節的中的第二位。

UTF-8的優點在於支持更多的Unicode碼位,另外變長的設定更可能是爲了序列化的時候節省存儲空間,定長的UTF-16或者UTF-32更適合在內存環境中操做。在現有的C++編程中多數傾向於在即將進行I/O讀寫操做纔將定長的UTF-16編碼轉化成UTF-8編碼使用。

指針空值—nullptr

通常編程習慣中,聲明一個變量的同時,老是須要在合適的代碼位置將其初始化。

對於指針類型的變量,未初始化的懸掛指針一般會是一些難於調試的用戶程序的錯誤根源。典型的初始化指針是將其指向一個「空」的位置,好比0。

因爲大多數計算機系統不容許用戶程序寫地址爲0的內存空間,假若程序無心中對該指針所指地址賦值,一般在運行時就會致使程序退出。雖然程序退出並不是什麼好事,但這樣一來錯誤也容易被程序員找到。所以在大多數的代碼中,咱們經常能看見指針初始化的語法以下:

int *ptr1 = NULL;
// 
int *ptr2 = 0;

通常狀況下,NULL是一個宏定義。在JNI的C頭文件(stddef.h)裏咱們能夠找到以下代碼:

#undef NULL
#ifdef __cplusplus
#  if !defined(__MINGW32__) && !defined(_MSC_VER)
#    define NULL __null
#  else
#    define NULL 0
#  endif

能夠看到,NULL可能被定義爲字面常量0,也可能預處理轉換爲編譯器內部標識(__null),其實這是通過改進的,在C++98標準中,字面常量0的類型既能夠是一個整型,也能夠是一個無類型指針(void*),咱們常常稱之爲字面常量0的二義性

在C++11中,出於兼容性的考慮,字面常量0的二義性並無被消除。但標準仍是爲二義性給出了新的答案,就是nullptr。在C++11中,nullptr並不是整型類別,甚至也不是指針類型,可是能轉換成任意指針類型。指針空值類型被命名爲nullptr_t,咱們能夠在__nullptr中找出以下定義:

namespace std
{
    typedef decltype(nullptr) nullptr_t;
}

nullptr也是一個nullptr_t的對象,nullptr是有類型的,且僅能夠被隱式轉化爲指針類型。就是說nullptr到任何指針的轉換是隱式的

另外C++11中規定用戶不能得到nullptr的地址。其緣由主要是由於nullptr被定義爲一個右值常量,取其地址並無意義。可是nullptr_t對象的地址能夠被用戶使用

運行時常量與編譯時常量

在C++中,咱們經常會遇到常量的概念。常量表示該值不可修改,一般是經過const關鍵字來修飾的。好比jni中的獲取jchar:

const jchar *mStr = env->GetStringChars(str, nullptr);

const還能夠修飾函數參數、函數返回值、函數自己、類等。在不一樣的使用條件下,const有不一樣的意義,不過大多數狀況下,const描述的都是一些「運行時常量性」的概念,即具備運行時數據的不可更改性。不過有的時候,咱們須要的倒是編譯時期的常量性,這是const關鍵字沒法保證的。

C++11中能夠在函數返回類型前加入關鍵字constexpr來使其成爲常量表達式函數。不過並不是全部的函數都有資格成爲常量表達式函數。事實上,常量表達式函數的要求很是嚴格,總結起來,大概有如下幾點:

  • 函數體只有單一的return返回語句。
  • 函數必須返回值(不能是void函數)。
  • 在使用前必須已有定義。
  • return返回語句表達式中不能使用很是量表達式的函數、全局數據,且必須是一個常量表達式。
constexpr int data(){return 1;}

常量表達式實際上能夠做用的實體不只限於函數,還能夠做用於數據聲明,以及類的構造函數等.

const放在號前,表示指針指向的內容不能被修改,const放在*號後,表示指針不能被修改。

*號先後都有const關鍵字表示指針和指向的內容都不能被修改。

constexpr關鍵字只能放在號前面,而且表示指針的內容不能被修改。

可是constexpr關鍵字是不能用於修飾自定義類型的定義:

#include <iostream>
using namespace std;

struct DataType{
    constexpr  DataType(int data):x(data){}
    int x;
};

constexpr DataType mData = {10};

int main(int argc, const char * argv[]) {
    
    cout <<    "mData= "<< mData.x <<endl;
   
    return 0;
}

對DataType的構造函數進行了定義,加上了constexpr關鍵字。

智能指針與垃圾回收

單獨起一篇寫。

String 字符串操做

JNI把Java中的全部對象看成一個C指針傳遞到本地方法中,指針指向JVM中的內部數據結構,而內部的數據結構在內存中的存儲方式是不可見的。只能從JNIEnv指針指向的函數表中選擇合適的JNI函數來操做JVM中的數據結構,String在Java是一個引用類型,因此要使用合適的 JNI 函數來將 jstring 轉成 C/C++ 字符串,例如用GetStringUTFChars這樣的JNI函數來訪問字符串的內容。固然咱們也能夠得到 Java 字符串的直接指針,不須要把它轉換成 C 風格的字符串。

C/C++ 中的基本類型用 typedef 從新定義了一個新的名字,在 JNI 中能夠直接訪問。Java 層的字符串到了 JNI 就成了 jstring 類型的,但 jstring 指向的是 JVM 內部的一個字符串,它不是 C 風格的字符串 char*,因此不能像使用 C 風格字符串同樣來使用 jstring 。

得到字符串

JNI 支持將 jstring 轉換成 UTF 編碼和 Unicode 編碼兩種。

  • GetStringUTFChars(jstring string, jboolean* isCopy)

將 jstring 轉換成 UTF 編碼的字符串

  • GetStringChars(jstring string, jboolean* isCopy)

其中,jstring 類型參數就是咱們須要轉換的字符串,而 isCopy 參數的值爲 JNI_TRUE 或者 JNI_FALSE ,表明是否返回 JVM 源字符串的一份拷貝。若是爲JNI_TRUE 則返回拷貝,而且要爲產生的字符串拷貝分配內存空間;若是爲JNI_FALSE 就直接返回了 JVM 源字符串的指針,意味着能夠經過指針修改源字符串的內容,但這就違反了 Java 中字符串不能修改的規定,在實際開發中,直接填 nullptr 。

當調用完 GetStringUTFChars 方法時須要作徹底檢查。由於 JVM 須要爲產生的新字符串分配內存空間,若是分配失敗就會返回 nullptr,而且會拋出 OutOfMemoryError 異常,因此要對 GetStringUTFChars 結果進行判斷。JNI 的異常和 Java 中的異常處理流程是不同的,Java 遇到異常若是沒有捕獲,程序會當即中止運行。而 JNI 遇到未決的異常不會改變程序的運行流程,也就是程序會繼續往下走,這樣後面針對這個字符串的全部操做都是很是危險的,所以,咱們須要用 return 語句跳事後面的代碼,並當即結束當前方法。

當使用完 UTF 編碼的字符串時,須要調用 ReleaseStringUTFChars 方法釋放所申請的內存空間。

..... 
const char *chars = env->GetStringUTFChars((jstring) str1, nullptr);
 if (chars == nullptr) {
      return nullptr;
 }
 env->ReleaseStringUTFChars((jstring) str1, chars);

直接字符串指針

若是一個字符串內容很大,有 1 M 多,而咱們只是須要讀取字符串內容,這種狀況下再把它轉換爲 C 風格字符串,不只畫蛇添足(經過直接字符串指針也能夠讀取內容),並且還須要爲 C 風格字符串分配內存。

爲此,JNI 提供了 GetStringCriticalReleaseStringCritical 函數來返回字符串的直接指針,這樣只須要分配一個指針的內存空間。

不過這對函數有一個很大的限制,在這兩個函數之間的本地代碼不能調用任何會讓線程阻塞或等待 JVM 中其它線程的本地函數或 JNI 函數。由於經過 GetStringCritical 獲得的是一個指向 JVM 內部字符串的直接指針,獲取這個直接指針後會致使暫停 GC 線程,當 GC 被暫停後,若是其它線程觸發 GC 繼續運行的話,都會致使阻塞調用者。因此在 Get/ReleaseStringCritical 這對函數中間的任何本地代碼都不能夠執行致使阻塞的調用或爲新對象在 JVM 中分配內存,不然,JVM 有可能死鎖。另一定要記住檢查是否由於內存溢出而致使它的返回值爲 nullptr,由於 JVM 在執行 GetStringCritical 這個函數時,仍有發生數據複製的可能性,尤爲是當 JVM 內部存儲的數組不連續時,爲了返回一個指向連續內存空間的指針,JVM 必須複製全部數據。

extern "C"
JNIEXPORT jstring JNICALL
Java_tt_reducto_ndksample_StringTypeOps_splicingStringCritical(JNIEnv *env,
                                                               jobject thiz,
                                                               jstring str) {
    const jchar *c_str = nullptr;
    char buf[128] = "hello ";
    char *pBuff = buf + 6;

    c_str = env->GetStringCritical(str, nullptr);
    if (c_str == nullptr) {
        // error handle
        return nullptr;
    }
    while (*c_str) {
        *pBuff++ = *c_str++;
    }
    //
    env->ReleaseStringCritical(str, c_str);
    //
    return env->NewStringUTF(buf);
}

得到字符串的長度

前面說了因爲 UTF-8 編碼的字符串以 \0 結尾,而 Unicode 字符串不是,因此對於兩種編碼得到字符串長度的函數也是不一樣的。

得到 Unicode 編碼的字符串的長度:

  • GetStringLength

得到 UTF-8 編碼的字符串的長度,或者使用 C 語言的 strlen 函數:

  • GetStringUTFLength

得到指定範圍的字符串內容

JNI 提供了函數來得到字符串指定範圍的內容,這裏的字符串指的是 Java 層的字符串。函數會把源字符串複製到一個預先分配的緩衝區內。

  • GetStringRegion(得到 Unicode 編碼的字符串指定內容)
  • GetStringUTFRegion(得到 UTF-8 編碼的字符串指定內容)
/**
 *  截取字符串
 */
extern "C"
JNIEXPORT jstring JNICALL
Java_tt_reducto_ndksample_StringTypeOps_splitString(JNIEnv *env, jobject thiz, jstring str) {
    // 獲取長度
    jsize len = env->GetStringLength(str);

    jchar outputBuf[len / 2];
    // 截取一部份內容放到緩衝區
    env->GetStringRegion(str, 0, len / 2, outputBuf);
    // 從緩衝區中獲取 Java 字符串
    return env->NewString(outputBuf, len / 2);
}

中文處理

看到有不少在JNI中對gbk與UTF-8作編碼轉換,這個是比較麻煩的,由於UTF-8編碼,GBK解碼,要看UTF-8編碼的二進制是否都能符合GBK的編碼規則,但GBK編碼,UTF-8解碼,GBK編出的二進制,是很難匹配上UTF-8的編碼規則。

」安卓「這兩個字的UTF-8編碼 與 GBK編碼下的二進制爲:

11100101 10101110 10001001 11100101 10001101 10010011 // UTF-8
10110000 10110010 11010111 10111111    // GBK

GBK編碼的二進制數據,徹底匹配不了UTF-8的編碼規則,只能被編碼成��׿

這個符號都是爲找不到對應規則隨意匹配的一個特殊字符。

而後��׿的UTF-8二進制位爲:

11101111 101111111 0111101 11101111 10111111 10111101 11010111 10111111

這個二進制和以前二進制不相同,因此轉化不到最初的字符串,按照GBK的編碼規則,「11101111 10111111」編碼成「錕」,「10111101 11101111」 編碼成「斤」,「10111111 10111101」編碼成「拷」,「11010111 10111111」編碼成「卓」。

字符串函數彙總

JNI 函數 描述
GetStringChars / ReleaseStringChars 得到或釋放一個指向 Unicode 編碼的字符串的指針(指 C/C++ 字符串)
GetStringUTFChars / ReleaseStringUTFChars 得到或釋放一個指向 UTF-8 編碼的字符串的指針(指 C/C++ 字符串)
GetStringLength 返回 Unicode 編碼的字符串的長度
getStringUTFLength 返回 UTF-8 編碼的字符串的長度
NewString 將 Unicode 編碼的 C/C++ 字符串轉換爲 Java 字符串
NewStringUTF 將 UTF-8 編碼的 C/C++ 字符串轉換爲 Java 字符串
GetStringCritical / ReleaseStringCritical 得到或釋放一個指向字符串內容的指針(指 Java 字符串)
GetStringRegion 獲取或者設置 Unicode 編碼的字符串的指定範圍的內容
GetStringUTFRegion 獲取或者設置 UTF-8 編碼的字符串的指定範圍的內容

數組操做

基本數據類型數組

對於基本數據類型數組,JNI 都有和 Java 相對應的結構,在使用起來和基本數據類型的使用相似。

在 Android JNI 基礎知識篇提到了 Java 數組類型對應的 JNI 數組類型。好比,Java int 數組對應了 jintArray,boolean 數組對應了 jbooleanArray。

如同 String 的操做同樣,JNI 提供了對應的轉換函數:GetArrayElements、ReleaseArrayElements。

1    intArray = env->GetIntArrayElements(int_array, nullptr);
2    env->ReleaseIntArrayElements(int_array, intArray, 0);

另外,JNI 還提供了以下的函數:

  • GetTypeArrayRegion / SetTypeArrayRegion(將數組內容複製到 C 緩衝區內,或將緩衝區內的內容複製到數組上)
  • GetArrayLength(獲得數組中的元素個數,也就是長度)
  • NewTypeArray(返回一個指定數據類型的數組,而且經過 SetTypeArrayRegion 來給指定類型數組賦值)
  • GetPrimitiveArrayCritical / ReleasePrimitiveArrayCritical(返回一個指定基礎數據類型數組的直接指針,這兩個操做之間不能作任何阻塞的操做。)
// Java 傳遞 數組 到 Native 進行數組求和
external fun intArraySum(intArray: IntArray): Int

對應的 C++ 代碼以下:

/**
 *  計算遍歷數組求和。
 */
extern "C"
JNIEXPORT jint JNICALL
Java_tt_reducto_ndksample_StringTypeOps_intArraySum(JNIEnv *env, jobject thiz,
                                                    jintArray int_array) {
    // 聲明
    jint *intArray;
    //
    int sum = 0;
    //
    intArray = env->GetIntArrayElements(int_array, nullptr);
    if (intArray == nullptr) {
        return 0;
    }
    // 獲得數組的長度
    int length = env->GetArrayLength(int_array);
    for (int i = 0; i < length; ++i) {
        sum += intArray[i];
    }

    // 也能夠經過 GetIntArrayRegion 獲取數組內容
    jint  buf[length];
    //
    env->GetIntArrayRegion(int_array, 0, length, buf);
    // 重置
    sum = 0;
    for (int i = 0; i < length; ++i) {
        sum += buf[i];
    }
    // 釋放內存
    env->ReleaseIntArrayElements(int_array, intArray, 0);

    return sum;
}

這裏使用了兩種方式獲取數組中內容:

若是咱們對包含1,000個元素的數組調用GetIntArrayElements(),則可能會致使分配和複製至少4,000個字節(1,000 * 4)。
而後,當使用ReleaseIntArrayElements()通知JVM更新數組的內容時,可能會觸發另外一個4,000字節的拷貝來更新數組。

即便您使用較新版本 GetPrimitiveArrayCritical(),規範仍容許JVM複製整個數組。

GetTypeArrayRegion()SetTypeArrayRegion() 方法容許咱們只獲取或者更新數組的一部分,而不是整個數組。經過使用這些方法,能夠確保應用程序只操做所須要的部分數據,從而提升執行效率。

釋放基本數據類型數組:

void Release<PrimitiveType>ArrayElements(JNIEnv *env,ArrayType array, NativeType *elems, jint mode);
mode 行爲
0 copy back the content and free the elems buffer
JNI_COMMIT copy back the content but do not free the elems buffer
JNI_ABORT free the buffer without copying back the possible changes

對象數組

即引用類型數組,數組中的每一個類型都是引用類型,JNI 只提供了以下函數來操做:

  • GetObjectArrayElement / SetObjectArrayElement

與本數據類型不一樣,不能一次獲得數據中的全部對象元素或者一次複製多個對象元素到緩衝區。只能經過以上函數來訪問或者修改指定位置的元素內容。

字符串和數組都是引用類型,所以也只能經過上面的方法來訪問。

咱們經過 JNI生成一個對象數組:

kotlin:

data class JniArray(var msg: String)
....
// 獲取JNI中建立的對象數組
external fun getNewObjectArray():Array<JniArray>

對應JNI方法:

extern "C"
JNIEXPORT jobjectArray JNICALL
Java_tt_reducto_ndksample_StringTypeOps_getNewObjectArray(JNIEnv *env, jobject thiz) {
    // 聲明一個對象數組
    jobjectArray result;
    // 設置 數組長度
    int size = 5;
    //
    static jclass cls = nullptr;
    // 數組中對應的類
    if (cls == nullptr) {

        jclass localRefs = env->FindClass("tt/reducto/ndksample/JniArray");
        if (localRefs == nullptr) {
            return nullptr;
        }
        cls = (jclass) env->NewGlobalRef(localRefs);
        env->DeleteLocalRef(localRefs);
        if (cls == nullptr) {
            return nullptr;
        }
    } else{
        LOGD("use GlobalRef cached")
    }

    // 初始化一個對象數組,用指定的對象類型
    result = env->NewObjectArray(size, cls, nullptr);
    if (result == nullptr) {
        return nullptr;
    }

    static jmethodID mid = nullptr;
    if (mid == nullptr) {
        mid = env->GetMethodID(cls, "<init>", "(Ljava/lang/String;)V");
        if (mid == nullptr) {
            return nullptr;
        }
    } else {
        LOGD("use method cached")
    }
    char buf[64];
    for (int i = 0; i < size; ++i) {
        sprintf(buf,"%d",i);
        //
        jstring nameStr = env->NewStringUTF(buf);
        // 建立
        jobject jobjMyObj = env->NewObject(cls, mid, nameStr);
        env->SetObjectArrayElement(result, i, jobjMyObj);
        env->DeleteLocalRef(jobjMyObj);
    }

    return result;
}

數組截取

void GetArrayRegion(JNIEnv *env, ArrayType array,jsize start, jsize len, NativeType *buf);

範圍設置數組

// 給數組的部分賦值
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array,
jsize start, jsize len, const NativeType *buf);

操做基本數據類型數組的直接指針

在某些狀況下,咱們須要原始數據指針來進行一些操做。調用GetPrimitiveArrayCritical後,咱們能夠得到一個指向原始數據的指針,可是在調用ReleasePrimitiveArrayCritical函數以前,咱們要保證不能進行任何可能會致使線程阻塞的操做。因爲GC的運行會打斷線程,因此在此期間任何調用GC的線程都會被阻塞。

void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);
jint len = (*env)->GetArrayLength(env, arr1);
  jbyte *a1 = (*env)->GetPrimitiveArrayCritical(env, arr1, 0);
  jbyte *a2 = (*env)->GetPrimitiveArrayCritical(env, arr2, 0);
  /* We need to check in case the VM tried to make a copy. */
  if (a1 == NULL || a2 == NULL) {
    ... /* out of memory exception thrown */
  }
  memcpy(a1, a2, len);
  (*env)->ReleasePrimitiveArrayCritical(env, arr2, a2, 0);
  (*env)->ReleasePrimitiveArrayCritical(env, arr1, a1, 0);

類型簽名

這裏的簽名指的是在 JNI 中去查找 Java 中對應的數據類型、對應的方法時,須要將 Java 中的簽名轉換成 JNI 所能識別的。

例如查看String的函數簽名:

javap -s java.lang.String

結果:

........
  public java.lang.String(byte[], int, int, java.lang.String) throws java.io.UnsupportedEncodingException;
    descriptor: ([BIILjava/lang/String;)V
    ......
  public byte[] getBytes(java.lang.String) throws java.io.UnsupportedEncodingException;
    descriptor: (Ljava/lang/String;)[B

  public byte[] getBytes(java.nio.charset.Charset);
    descriptor: (Ljava/nio/charset/Charset;)[B

  public byte[] getBytes();
    descriptor: ()[B

    .......

對於類的簽名轉換

對於 Java 中類或者接口的轉換,須要用到 Java 中類或者接口的全限定名,把 Java 中描述類或者接口的 . 換成 / 就行了,好比 String 類型對應的 JNI 描述爲:

java/lang/String     // . 換成 /

對於數組類型,則是用 [ 來表示數組,而後跟一個字段的簽名轉換:

[I         // 表明一維整型數組,I 表示整型
[[I        // 表明二維整型數組
[Ljava/lang/String;      // 表明一維字符串數組,

對應基礎類型字段的轉換

Java 類型 JNI 對應的描述轉
boolean Z
byte B
char C
short S
int I
long J
float F
double D

對於引用類型的字段簽名轉換

大寫字母 L 開頭,而後是類的簽名轉換,最後以 ; 結尾:

Java 類型 JNI 對應的描述轉換
String Ljava/lang/String;
Class Ljava/lang/Class;
Throwable Ljava/lang/Throwable
int[] "[I"
Object[] "[Ljava/lang/Object;"

對於方法的簽名轉換

首先是將方法內全部參數轉換成對應的字段描述,並所有寫在小括號內,而後在小括號外再緊跟方法的返回值類型描述。

Java 類型 JNI 對應的描述轉換
String f(); ()Ljava/lang/String;
long f(int i, Class c); (ILjava/lang/Class;)J
String(byte[] bytes); ([B)V

這裏要注意的是在 JNI 對應的描述轉換中不要出現空格。

瞭解並掌握這些轉換後,就能夠進行更多的操做了,實現 Java 與 C++ 的相互調用。

好比,有一個自定義的 data class,而後再 Native 中打印類的對象數組的某一個字段值:

data class JniArray(var msg: String)

看下對應的字段的Bytecode

public final class tt/reducto/ndksample/JniArray {
 ....
 L0
    LINENUMBER 15 L0
    ALOAD 0
    GETFIELD tt/reducto/ndksample/JniArray.msg : Ljava/lang/String;
    ARETURN
   L1
   
   ......
 }

方法:

external fun getObjectArrayElement(jniArray: Array<JniArray>):String?

具體 C++ 代碼以下:

extern "C"
JNIEXPORT jstring JNICALL
Java_tt_reducto_ndksample_StringTypeOps_getObjectArrayElement(JNIEnv *env, jobject thiz,
                                                              jobjectArray jni_array) {
    jobject arr;
    // 數組長度
    int size = env->GetArrayLength(jni_array);
    // 數組中對應的類
    jclass cls = env->FindClass("tt/reducto/ndksample/JniArray");
   
    // 類對應的字段描述
    jfieldID fid = env->GetFieldID(cls, "msg", "Ljava/lang/String;");
   
    // 類的字段具體的值
    jstring jstr;
    const char *str = nullptr;
      // 拼接
    string tmp;
    for (int i = 0; i < size; ++i) {
        // 獲得數組中的每個元素
        arr = env->GetObjectArrayElement(jni_array, i);
        // 每個元素具體字段的值
        jstr = (jstring) (env->GetObjectField(arr, fid));
        str = env->GetStringUTFChars(jstr, nullptr);
        if (str == nullptr) {
            continue;
        }
        tmp += str;
        LOGD("str is %s", str)
        env->ReleaseStringUTFChars(jstr, str);
    }

    return env->NewStringUTF(tmp.c_str());
}

緩存方式

初始化時緩存

在類加載時,進行緩存。當類被加載進內存時,會先調用類的靜態代碼塊,因此能夠在類的靜態代碼塊中進行緩存。

public class StringTypeOps {
      static {
          // 靜態代碼塊中進行緩存
        nativeInit();
    }
        private static native void nativeInit();   
   
}
public class StringTypeOps {
      companion object {
        
        private external fun nativeInit()

        init {
            nativeInit()
        }
    }
   
}

在執行 ID 查找的 C/C++ 代碼中建立 nativeClassInit 方法。初始化類時,該代碼會執行一次。若是要取消加載類以後再從新加載,該代碼將再次執行。

若是要經過原生代碼訪問對象的字段,如下操做:

  • 使用 FindClass 獲取類的類對象引用
  • 使用 GetFieldID 獲取字段的字段 ID
  • 使用適當函數獲取字段的內容,例如 GetIntField

一樣,如需調用方法,首先要獲取類對象引用,而後獲取方法 ID。方法 ID 一般只是指向內部運行時數據結構的指針。查找方法 ID 可能須要進行屢次字符串比較,但一旦獲取此類 ID,即可以很是快速地進行實際調用以獲取字段或調用方法。

若是性能很重要,通常建議查找一次這些值並將結果緩存在原生代碼中。因爲每一個進程只能包含一個 JavaVM,所以將這些數據存儲在靜態本地結構中是一種合理的作法。

在取消加載類以前,類引用、字段 ID 和方法 ID 保證有效。只有在與 ClassLoader 關聯的全部類能夠進行垃圾回收時,系統纔會取消加載類,這種狀況不多見,但在 Android 中並不是不可能。但請注意,jclass 是類引用,必須經過調用 NewGlobalRef 來保護

若是您在加載類時緩存方法 ID,並在取消加載類後從新加載時自動從新緩存方法 ID,那麼初始化方法 ID 的正確作法是,將與如下相似的一段代碼添加到相應類中:

// 全局變量做爲緩存
// Java字符串的類和獲取方法ID
jclass gStringClass;

jmethodID gmidStringInit;

jmethodID gmidStringGetBytes;

......
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_tt_reducto_ndksample_StringTypeOps_chineseString(JNIEnv *env, jobject thiz,
                                                      jstring str) {
    .....                                                 
    gStringClass = env->FindClass("java/lang/String");
    if (gStringClass == nullptr) {
        return nullptr;
    }
    //  public byte[] getBytes(java.lang.String) throws java.io.UnsupportedEncodingException;
    gmidStringGetBytes = (env)->GetMethodID(gStringClass, "getBytes", "(Ljava/lang/String;)[B");
    if (gmidStringGetBytes == nullptr) {
        return nullptr;
    }                                                
   ....                                                   
}

要訪問Java對象的字段或者調用它們的方法,本地代碼必須調用FindClass()、GetFieldID()、GetMethodId()和GetStaticMethodID()來獲取對應的ID。在的一般狀況下,GetFieldID()、GetMethodID()和 GetStaticMethodID()爲同一個類返回的ID在JVM進程的生命週期內都不會更改。可是獲取字段或方法的ID可能須要在JVM中進行大量工做,由於字段和方法可能已經從超類繼承,JVM不得不在類繼承結構中查找它們。由於給定類的ID是相同的,因此應該查找它們一次,而後重複使用它們。一樣的,查找類對象也可能很耗時,所以它們也應該被緩存起來進行復用。

這裏 在 JNI 中直接將方法 id 緩存成全局變量了,這樣再調用時,避免了多個線程同時調用會屢次查找的狀況,提高效率。

使用時緩存

使用時緩存,就是在調用時查找一次,而後將它緩存成 static 變量,這樣下次調用時就已經被初始化過了。

直到內存釋放了,纔會緩存失效。

extern "C"
JNIEXPORT jstring JNICALL
Java_tt_reducto_ndksample_StringTypeOps_getObjectArrayElement(JNIEnv *env, jobject thiz,
                                                              jobjectArray jni_array) {
    .....
    static jfieldID fid = nullptr;
    // 類對應的字段描述
    // 從緩存中查找
    if (fid == nullptr) {
        fid = env->GetFieldID(cls, "msg", "Ljava/lang/String;");
        if (fid == nullptr) {
            return nullptr;
        }
    }
        ....

    return env->NewStringUTF(tmp.c_str());
}

經過聲明爲 static 變量進行緩存。但這種緩存方式有弊端,多個調用者同時調用時,就會出現緩存屢次的狀況,而且每次調用時都要檢查是否緩存過。

若是不能預先知道方法和字段所在類的源碼,那麼在使用時緩存比較合理。但若是知道的話,在初始化時緩存優勢較多,既避免了每次使用時檢查,還避免了在多線程被調用的狀況。

引用管理

Native 代碼並不能直接經過引用來訪問其內部的數據接口,必需要經過調用 JNI 接口來間接操做這些引用對象,而且 JNI 還提供了和 Java 相對應的引用類型,所以,咱們就須要經過管理好這些引用來管理 Java 對象,避免在使用時被 GC 回收。

JNI 提供了三種引用類型:

  • 局部引用
  • 全局引用
  • 弱全局引用

在Native的環境,同時要注意內存問題,由於Native的代碼都是要手動的申請內存,手動的釋放。

固然,業務邏輯裏面的申請和釋放用標準的new/delete或者malloc/free,或者用智能指針之類的。JNI部分是有封裝好的方法的,好比NewGlobalRef,NewLocalRef, DeleteGlobalRef, DeleteLocalRef等。

須要注意的是用這些方法建立出來的引用要及時的刪除。由於這些引用都是在JVM中一個表中存放的,而這個表是有容量限制,當到達必定數量後就不能再存放了,就會報出異常。因此要及時刪除建立出來的引用。

局部引用

傳遞給原生方法的每一個參數,以及 JNI 函數返回的幾乎每一個對象都屬於「局部引用」。這意味着,局部引用在當前線程中的當前原生方法運行期間有效。在原生方法返回後,即便對象自己繼續存在,該引用也無效。

好比:NewObject、FindClass、NewObjectArray 函數等等。

局部引用會阻止 GC 回收所引用的對象,同時,它不能在本地函數中跨函數傳遞,不能跨線程使用。

在以前 JNI 調用時緩存字段和方法 ID,把字段 ID 經過 static 變量緩存起來。

jfieldIDjmethodID 屬於不透明類型,不是對象引用。而若是把 FindClass 函數建立的局部引用也經過 static 變量緩存起來,那麼在函數退出後,局部引用被自動釋放了,static 靜態變量中存儲的就是一個被釋放後的內存地址,成爲了一個野指針,再次調用時就會引發程序崩潰了。

建立的任何局部引用都必須手動刪除。若是在循環中建立局部引用的任何原生代碼可能須要執行某些手動刪除操做:

for (int i = 0; i < len; ++i) {
        jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
        ... /* process jstr */
        (*env)->DeleteLocalRef(env, jstr);
    }
  • 循環體中建立了大量的局部引用對象時,會形成 JNI 局部引用表的溢出,因此須要及時釋放局部引用,防止溢出。
  • 局部引用使用完了就刪除,沒必要要等到函數結尾。
  • 若是Native 方法不會返回,那麼自動釋放局部引用就失效了,這時候就必需要手動釋放。好比,在某個一直等待的循環中,若是不及時釋放局部引用,很快就會溢出。

記住「不過分分配」局部引用。JNI 的規範指出,JVM 要確保每一個 Native 方法至少能夠建立 16 個局部引用,所以若是須要更多,則應該按需刪除,或使用 EnsureLocalCapacity/PushLocalFrame 保留。

例如 :

// Use EnsureLocalCapacity
    int len = 20;
    if (env->EnsureLocalCapacity(len) < 0) {
        // 建立失敗,outof memory
    }
    for (int i = 0; i < len; ++i) {
        jstring  jstr = env->GetObjectArrayElement(arr,i);
        // 處理 字符串
        // 建立了足夠多的局部引用,這裏就不用刪除了,顯然佔用更多的內存
    }

這樣在循環體中處理局部引用時能夠不進行刪除了,可是顯然會消耗更多的內存空間。

PushLocalFrame 與 PopLocalFrame 是配套使用的函數對。

PushLocalFrame爲函數中須要用到的局部引用建立了一個引用堆棧,若是以前調用PushLocalFrame已經建立了Frame,在當前的本地引用棧中仍然是有效的,例如每次遍歷中調用env->GetObjectArrayElement(arr, i);

返回一個局部引用時,JVM會自動將該引用壓入當前局部引用棧中。而PopLocalFrame負責銷燬棧中全部的引用。它們能夠爲局部引用建立一個指定數量內嵌的空間,在這個函數對之間的局部引用都會在這個空間內,直到釋放後,全部的局部引用都會被釋放掉,不用再擔憂每個局部引用的釋放問題。

// Use PushLocalFrame & PopLocalFrame
   for (int i = 0; i < len; ++i) {
       if (env->PushLocalFrame(len)) { // 建立指定數據的局部引用空間
           //outof  memory
       }
       jstring jstr = env->GetObjectArrayElement(arr, i);
       // 處理字符串
       // 期間建立的局部引用,都會在 PushLocalFrame 建立的局部引用空間中
       // 調用 PopLocalFrame 直接釋放這個空間內的全部局部引用
       env->PopLocalFrame(nullptr); 
   }

全局引用

全局引用也會阻止它所引用的對象被回收。可是它不會在方法返回時被自動釋放,必需要經過手動釋放才行,並且,全局引用能夠跨方法、跨線程使用。

全局引用只能經過 NewGlobalRef函數來建立,而後經過 DeleteGlobalRef 函數來手動釋放。

JNIEXPORT jstring JNICALL
Java_tt_reducto_ndksample_StringTypeOps_getObjectArrayElement(JNIEnv *env, jobject thiz,
                                                              jobjectArray jni_array) {
    jobject arr;
    // 數組長度
    int size = env->GetArrayLength(jni_array);
    static jclass cls = nullptr;
    // 數組中對應的類
    if (cls == nullptr) {
        jclass localRefs = env->FindClass("tt/reducto/ndksample/JniArray");
        if (localRefs == nullptr) {
            return nullptr;
        }
        cls = (jclass) env->NewGlobalRef(localRefs);
        env->DeleteLocalRef(localRefs);
        if (cls == nullptr) {
            return nullptr;
        }
    }
    ........
}

謹慎使用全局引用。

雖然使用全局引用不可避免,但它們很難調試,而且可能會致使難以診斷的內存(不良)行爲。在全部其餘條件相同的狀況下,全局引用越少,解決方案的效果可能越好。

弱全局引用

弱全局引用有點相似於 Java 中的弱引用,它所引用的對象能夠被 GC 回收,而且它也能夠跨方法、跨線程使用。

使用 NewWeakGlobalRef 方法建立,使用 DeleteWeakGlobalRef 方法釋放。

extern "C"
JNIEXPORT jstring JNICALL
Java_tt_reducto_ndksample_StringTypeOps_getObjectArrayElement(JNIEnv *env, jobject thiz,
                                                              jobjectArray jni_array) {
    jobject arr;
    // 數組長度
    int size = env->GetArrayLength(jni_array);
    static jclass cls = nullptr;
    // 數組中對應的類
    if (cls == nullptr) {
        jclass localRefs = env->FindClass("tt/reducto/ndksample/JniArray");
        if (localRefs == nullptr) {
            return nullptr;
        }
        cls = (jclass) env->NewWeakGlobalRef(localRefs);
      
        if (cls == nullptr) {
            return nullptr;
        }
    }

    static jfieldID fid = nullptr;
    // 類對應的字段描述
    // 從緩存中查找
    if (fid == nullptr) {
        fid = env->GetFieldID(cls, "msg", "Ljava/lang/String;");
        if (fid == nullptr) {
            return nullptr;
        }
    }
    jboolean isGC = env->IsSameObject(cls, nullptr);
    if(isGC ){
        LOGD("weak reference has been gc")
        return nullptr;
    } else{

        jstring jstr;
        // 類的字段具體的值
        // 類字段具體值轉換成 C/C++ 字符串
        const char *str = nullptr;
        string tmp;
        for (int i = 0; i < size; ++i) {
            // 獲得數組中的每個元素
            arr = env->GetObjectArrayElement(jni_array, i);
            // 每個元素具體字段的值
            jstr = (jstring) (env->GetObjectField(arr, fid));

            str = env->GetStringUTFChars(jstr, nullptr);
            if (str == nullptr) {
                continue;
            }
            tmp += str;
            LOGD("str is %s", str)
            env->ReleaseStringUTFChars(jstr, str);
        }
        return env->NewStringUTF(tmp.c_str());
    }


}

引用比較

如需比較兩個引用是否引用同一對象,必須使用 IsSameObject 函數。

切勿在原生代碼中使用 == 比較各個引用。

由於JNI env不是指向Java對象的直接指針。在垃圾回收期間,Java對象能夠在Heap上移動。它們的內存地址可能會更改,可是JNI env必須保持有效。

JNI env對用戶是不透明的,也就是說,env的實現是特定於JVM的。IsSameObject提供了抽象層。

在HotSpot中,JVM env是指向可變對象引用的指針。

若是使用此符號,您就不能假設對象引用在原生代碼中是常量或惟一值

同時,還能夠用 isSameObject 來比較弱全局引用所引用的對象是否被 GC 了,返回 JNI_TRUE 則表示回收了,JNI_FALSE 則表示未被回收。

env->IsSameObject(obj1, obj2) // 比較局部引用 和 全局引用是否相同
env->IsSameObject(obj, nullptr)  // 比較局部引用或者全局引用是否爲 NULL
env->IsSameObject(wobj, nullptr) // 比較弱全局引用所引用對象是否被 GC 回收

函數返回的 GetStringUTFCharsGetByteArrayElements 等原始數據指針不屬於對象。這些指針能夠在線程之間傳遞,而且在匹配的 Release 調用完成以前一直有效。

異常處理

通常咱們在JNI中須要處理的兩種異常:

  • Native 代碼調用 Java 層代碼時發生了異常要處理
  • Native 代碼本身拋出了一個異常讓 Java 層去處理

JNI沒有像Java同樣有try…catch…final這樣的異常處理機制,面且在本地代碼中調用某個JNI接口時若是發生了異常,後續的本地代碼不會當即中止執行,而會繼續往下執行後面的代碼。

jint        (*Throw)(JNIEnv*, jthrowable);
jint        (*ThrowNew)(JNIEnv *, jclass, const char *);
jthrowable  (*ExceptionOccurred)(JNIEnv*);
void        (*ExceptionDescribe)(JNIEnv*);
void        (*ExceptionClear)(JNIEnv*);
void        (*FatalError)(JNIEnv*, const char*);

還有一個 單獨的:

jboolean    (*ExceptionCheck)(JNIEnv*);
  • ExceptionCheck:檢查是否發生了異常,如有異常返回JNI_TRUE,不然返回JNI_FALSE
  • ExceptionOccurred:檢查是否發生了異常,如有異常返回該異常的引用,不然返回NULL
  • ExceptionDescribe:打印異常的堆棧信息
  • ExceptionClear:清除異常堆棧信息
  • ThrowNew:在當前線程觸發一個異常,並自定義輸出異常信息
  • Throw:丟棄一個現有的異常對象,在當前線程觸發一個新的異常
  • FatalError:致命異常,用於輸出一個異常信息,並終止當前VM實例(即退出程序)

Native 調用 Java 方法時的異常

咱們拿上面getObjectArrayElement代碼舉例:

故意寫錯字段:

.....
   // 類對應的字段描述
   jfieldID fid = env->GetFieldID(cls, "ms", "Ljava/lang/String;");
   jthrowable mjthrowable = env->ExceptionOccurred();
    if (mjthrowable) {
        // 打印異常日誌
        env->ExceptionDescribe();
        // 清除異常不產生崩潰
        env->ExceptionClear();
        // 清除引用
        env->DeleteLocalRef(cls);
    }
    ....

這樣 log就會輸出:

java.lang.NoSuchFieldError: no "Ljava/lang/String;" field "ms" in class "Ltt/reducto/ndksample/JniArray;" or its superclasses

ExceptionClear 方法則是關鍵的不會讓應用直接崩潰的方法,相似於 Java 的 catch 捕獲異常處理,它會消除此次異常。

這樣就把由 Native 調用 Java 時的一個異常進行了處理,當處理完異常以後,別忘了釋放對應的資源。

不過,咱們這樣僅僅是消除了此次異常,還應該讓調用者有異常的發生,那麼就須要經過 Native 來拋出一個異常告訴 Java 調用者了。

Native 拋出 Java 中的異常

有時在 Native 代碼中進行一些操做,須要拋出異常到 Java ,交由上層去處理。

好比 Java 調用 Native 方法傳遞了某個參數,而這個參數有問題,那麼 Native 就能夠拋出異常讓 Java 去處理這個參數異常的問題。

Native 拋出異常的代碼大體都是相同的,能夠抽出一個通用函數來:

JNI中拋異常工具代碼:

void
JNI_ThrowByName(JNIEnv *env, const char *name, const char *msg)
{
    //查找異常類
    jclass cls = env->FindClass(name);
    //判斷是否找到該異常類
    if (cls != NULL) {
        env->ThrowNew(cls, msg);//拋出指定名稱的異常
    }
    //釋放局部變量
    env->DeleteLocalRef(cls);
}

JNI中檢測工具代碼:

int checkExecption(JNIEnv *env) {
    if(env->ExceptionCheck()) {//檢測是否有異常
        env->ExceptionDescribe(); // 打印異常信息
        env->ExceptionClear();//清除異常信息
        return 1;
    }
    return -1;
}
java.lang.ArithmeticException: divide by zero

寫個簡單的例子:

class ExcTest {
    fun getNum() = 2 / 0
}

對應JNI函數:

extern "C"
JNIEXPORT  void JNICALL
Java_tt_reducto_ndksample_StringTypeOps_exception
        (JNIEnv *env, jobject jobj) {

    jclass cls = env->FindClass("tt/reducto/ndksample/ExcTest");

    jmethodID mid = env->GetMethodID(cls, "<init>", "()V");
    jobject obj = env->NewObject(cls, mid);
    mid = env->GetMethodID(cls, "getNum", "()I");
    // 先初始化一個類,而後調用類方法,就如博客中描述的那樣
    env->CallIntMethod(obj, mid);
    // 檢查是否發生了異常,若用異常返回該異常的引用,不然返回NULL
    jthrowable exc;
    exc = env->ExceptionOccurred();

    if (exc) {
        // 打印異常調用異常對應的Java類的printStackTrace()函數
        env->ExceptionDescribe();
        //清除引起的異常,在Java層不會打印異常的堆棧信息
        env->ExceptionClear();
        env->DeleteLocalRef(cls);
        env->DeleteLocalRef(obj);

        // 拋出一個自定義異常信息
        throwByName(env, "java/lang/ArithmeticException", "divide by zero");
    }

}

這樣咱們在kotlin中捕獲就能夠了:

try {
        StringTypeOps.exception()
       }catch (e:ArithmeticException) {
           e.printStackTrace()
       }

注意事項:

調用ThrowNew方法手動拋出異常後,native方法會繼續執行可是返回值會被忽略。

most JNI methods cannot be called with a pending exception.

儘可能不要在拋出異常後再去執行邏輯。不然會crash.

JNI DETECTED ERROR IN APPLICATION: JNI ThrowNew called with pending exception java.lang.IllegalArgumentException:

信號量捕獲

這裏不是Java併發中的信號量Semaphore.

具體能夠參考騰訊Bugly的這篇文章

另外關於愛奇藝的xCrash有興趣也能夠看看。

參考

https://developer.android.com...

  • 《深刻理解C++11》
相關文章
相關標籤/搜索