如下介绍所有基于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' } ..... }
变数名 | 引数 | 描述 |
---|---|---|
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” |
名称 | 说明 | 功能 |
---|---|---|
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。
定义任意 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是虚拟机在JNI中的表示,一个JVM中只有一个JavaVM对象,并且对象是线程共享的。
经过JNIEnv咱们能够获取一个Java虚拟机对象,其函数以下:
jint **GetJavaVM**(JNIEnv *env, JavaVM **vm);
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保护私密字符串会用到....
简单说下须要用到的一些点..
看资料常常有人这么说:
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 表示这个一个双字节字符:
对于须要三个字节表示的字符,其最高位使用111表示该字符的字节数:
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数据。
至于UTF-8编码的Unicode数据,C++11仍是使用8字节宽度的char
类型的数组来保存。而char16_t和char32_t的长度则犹如其名称所显示的那样,长度分别为16字节和32字节,对任何编译器或者系统都是同样的。此外,C++11还定义了一些常量字符串的前缀。在声明常量字符串的时候,这些前缀声明可让编译器使字符串按照前缀类型产生数据。
C++11一共定义了3种这样的前缀:
对于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编码使用。
通常编程习惯中,声明一个变量的同时,老是须要在合适的代码位置将其初始化。
对于指针类型的变量,未初始化的悬挂指针一般会是一些难于调试的用户程序的错误根源。典型的初始化指针是将其指向一个“空”的位置,好比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来使其成为常量表达式函数。不过并不是全部的函数都有资格成为常量表达式函数。事实上,常量表达式函数的要求很是严格,总结起来,大概有如下几点:
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关键字。
单独起一篇写。
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 编码两种。
将 jstring 转换成 UTF 编码的字符串
其中,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 提供了 GetStringCritical
和 ReleaseStringCritical
函数来返回字符串的直接指针,这样只须要分配一个指针的内存空间。
不过这对函数有一个很大的限制,在这两个函数之间的本地代码不能调用任何会让线程阻塞或等待 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 编码的字符串的长度:
得到 UTF-8 编码的字符串的长度,或者使用 C 语言的 strlen 函数:
JNI 提供了函数来得到字符串指定范围的内容,这里的字符串指的是 Java 层的字符串。函数会把源字符串复制到一个预先分配的缓冲区内。
/** * 截取字符串 */ 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 还提供了以下的函数:
// 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 只提供了以下函数来操做:
与本数据类型不一样,不能一次获得数据中的全部对象元素或者一次复制多个对象元素到缓冲区。只能经过以上函数来访问或者修改指定位置的元素内容。
字符串和数组都是引用类型,所以也只能经过上面的方法来访问。
咱们经过 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
获取字段的字段 IDGetIntField
一样,如需调用方法,首先要获取类对象引用,而后获取方法 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 变量缓存起来。
jfieldID
和 jmethodID
属于不透明类型,不是对象引用。而若是把 FindClass 函数建立的局部引用也经过 static 变量缓存起来,那么在函数退出后,局部引用被自动释放了,static 静态变量中存储的就是一个被释放后的内存地址,成为了一个野指针,再次调用时就会引发程序崩溃了。
建立的任何局部引用都必须手动删除。若是在循环中建立局部引用的任何原生代码可能须要执行某些手动删除操做:
for (int i = 0; i < len; ++i) { jstring jstr = (*env)->GetObjectArrayElement(env, arr, i); ... /* process jstr */ (*env)->DeleteLocalRef(env, jstr); }
记住“不过分分配”局部引用。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 回收
函数返回的 GetStringUTFChars
和 GetByteArrayElements
等原始数据指针不属于对象。这些指针能够在线程之间传递,而且在匹配的 Release 调用完成以前一直有效。
通常咱们在JNI中须要处理的两种异常:
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*);
咱们拿上面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 ,交由上层去处理。
好比 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...