关于android:JNI的一些基础

3次阅读

共计 32252 个字符,预计需要花费 81 分钟才能阅读完成。

Cmake 配置

以下介绍全副基于 C ++ 11

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 办法如下:

object NDKLibrary {
    init {
        // 加载动静库,这里对应 CMakeLists.txt 里的 add_library NDKSample
        System.loadLibrary("NDKSample")
    }

    // 应用 external 关键字批示以原生代码模式实现的办法
    external fun plus(a: Int, b: Int): Int
}

c++:

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++ 层的数据类型,计算之后再返回成 应用层的数据类型。

(*env)-> 办法名(env, 参数列表)  // C 的语法
env-> 办法名(参数列表)         //C++ 的语法

C 语言没有对象的概念,因而要将 env 指针作为形参传入到 JNIEnv 办法中。

C++ 中 const 形容的都是一些“运行时常量性”的概念,即具备运行时数据的不可更改性。这与编译期间的常量性要区别开。

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 示意。

在 kotlin 办法中只有两个参数,在 C++ 代码就有四个参数了,至多都会蕴含后面两个参数

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-8、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…

正文完
 0