请点赞关注,你的反对对我意义重大

Hi,我是小彭。本文已收录到 GitHub · Android-NoteBook 中。这里有 Android 进阶成长常识体系,有气味相投的敌人,关注公众号 [彭旭锐] 带你建设外围竞争力。

前言

  • 在 Android 生态中次要有 C/C++、Java、Kotlin 三种语言 ,它们的关系不是替换而是互补。其中,C/C++ 的语境是算法和高性能,Java 的语境是平台无关和内存治理,而 Kotlin 则交融了多种语言中的优良个性,带来了一种更现代化的编程形式;
  • JNI 是实现 Java 代码与 C/C++ 代码交互的个性, 思考一个问题 —— Java 虚拟机是如何实现两种毫不相干的语言的交互的呢? 明天,咱们来全面总结 JNI 开发常识框架,为 NDK 开发打下基础。本文局部演示代码能够从 DemoHall·HelloJni 下载查看。

这篇文章是 NDK 系列文章第 5 篇,专栏文章列表:

一、语言根底:

  • 1、NDK 学习路线:怎么学 & 我的教训
  • 2、C 语言根底
  • 3、C ++ 语言根底
  • 4、C/C++ 编译过程:从源码到程序运行

二、NDK 开发:

  • 1、JNI 根底:Java 与 Native 交互(本文)
  • 2、注册 JNI 函数:动态注册 & 动静注册
  • 3、NDK 根底:ndk-build & CMake
  • 4、so 文件加载过程剖析:了解 Android 中 loadLibrary() 的执行流程
  • 5、so 文件适配 64 位架构:Gradle 插件一键检索未适配项
  • 6、so 文件动态化:动静下载
  • 7、so 文件体积优化:文件精简

三、基础理论

  • 1、视频基础理论
  • 2、音频基础理论
  • 3、H.264 视频压缩编码
  • 4、音频压缩编码
  • 5、FFMPEG 根底
  • 6、OPENSL ES 根底
  • 7、PNG 图片:无损压缩编码

四、计算机根底

  • 1、字符编码:ASCII、Unicode、UTF-8、UTF-16、UTF-32

JNI 学习路线图:


1. 意识 JNI

1.1 为什么要应用 JNI?

JNI(Java Native Interface,Java 本地接口)是 Java 生态的个性,它扩大了 Java 虚拟机的能力,使得 Java 代码能够与 C/C++ 代码进行交互。 通过 JNI 接口,Java 代码能够调用 C/C++ 代码,C/C++ 代码也能够调用 Java 代码。

这就引出第 1 个问题(为什么要这么做):Java 为什么要调用 C/C++ 代码,而不是间接用 Java 开发需要呢?我认为次要有 4 个起因:

  • 起因 1 - Java 人造须要 JNI 技术: 尽管 Java 是平台无关性语言,但运行 Java 语言的虚拟机是运行在具体平台上的,所以 Java 虚拟机是平台相干的。因而,对于调用平台 API 的性能(例如关上文件性能,在 Window 平台是 openFile 函数,而在 Linux 平台是 open 函数)时,尽管在 Java 语言层是平台无关的,但背地只能通过 JNI 技术在 Native 层别离调用不同平台 API。相似的,对于有操作硬件需要的程序,也只能通过 C/C++ 实现对硬件的操作,再通过 JNI 调用;
  • 起因 2 - Java 运行效率不迭 C/C++: Java 代码的运行效率绝对于 C/C++ 要低一些,因而,对于有密集计算(例如实时渲染、音视频解决、游戏引擎等)需要的程序,会抉择用 C/C++ 实现,再通过 JNI 调用;
  • 起因 3 - Native 层代码安全性更高: 反编译 so 文件的难度比反编译 Class 文件高,一些跟明码相干的性能会抉择用 C/C++ 实现,再通过 JNI 调用;
  • 起因 4 - 复用现有代码: 当 C/C++ 存在程序须要的性能时,则能够间接复用。

还有第 2 个问题(为什么能够这么做):为什么两种独立的语言能够实现交互呢?因为 Java 虚拟机自身就是 C/C++ 实现的,无论是 Java 代码还是 C/C++ 代码,最终都是由这个虚拟机撑持,独特应用一个过程空间。JNI 要做的只是在两种语言之间做桥接。

1.2 JNI 开发的根本流程

一个规范的 JNI 开发流程次要蕴含以下步骤:

  • 1、创立 HelloWorld.java,并申明 native 办法 sayHi();
  • 2、应用 javac 命令编译源文件,生成 HelloWorld.class 字节码文件;
  • 3、应用 javah 命令导出 HelloWorld.h 头文件(头文件中蕴含了本地办法的函数原型);
  • 4、在源文件 HelloWorld.cpp 中实现函数原型;
  • 5、编译本地代码,生成 Hello-World.so 动静原生库文件;
  • 6、在 Java 代码中调用 System.loadLibrary(...) 加载 so 文件;
  • 7、应用 Java 命令运行 HelloWorld 程序。

该流程用示意图示意如下:

1.3 JNI 的性能误区

JNI 自身自身并不能解决性能问题,谬误地应用 JNI 反而可能引入新的性能问题,这些问题都是要留神的:

  • 问题 1 - 逾越 JNI 边界的调用: 从 Java 调用 Native 或从 Native 调用 Java 的老本很高,应用 JNI 时要限度逾越 JNI 边界的调用次数;
  • 问题 2 - 援用类型数据的回收: 因为援用类型数据(例如字符串、数组)传递到 JNI 层的只是一个指针,为防止该对象被垃圾回收虚构机会固定住(pin)对象,在 JNI 办法返回前会阻止其垃圾回收。因而,要尽量缩短 JNI 调用的执行工夫,它可能缩短对象被固定的工夫(对于援用类型数据的解决,在下文会说到)。

1.4 注册 JNI 函数的形式

Java 的 native 办法和 JNI 函数是一一对应的映射关系,建设这种映射关系的注册形式有 2 种:

  • 形式 1 - 动态注册: 基于命名约定建设映射关系;
  • 形式 2 - 动静注册: 通过 JNINativeMethod 构造体建设映射关系。

对于注册 JNI 函数的更多原理剖析,见 注册 JNI 函数。

1.5 加载 so 库的机会

so 库须要在运行时调用 System.loadLibrary(…) 加载,个别有 2 种调用机会:

  • 1、在类动态初始化中: 如果只在一个类或者很少类中应用到该 so 库,则最常见的形式是在类的动态初始化块中调用;
  • 2、在 Application 初始化时调用: 如果有很多类须要应用到该 so 库,则能够思考在 Application 初始化等场景中提前加载。

对于加载 so 库的更多原理剖析,见 so 文件加载过程剖析。


2. JNI 模板代码

本节咱们通过一个简略的 HelloWorld 程序来帮忙你相熟 JNI 的模板代码。

JNI Demo

JNIEXPORT void JNICALL Java_com_xurui_hellojni_HelloWorld_sayHi (JNIEnv *, jobject);

2.1 JNI 函数名

为什么 JNI 函数名要采纳 Java_com_xurui_HelloWorld_sayHi 的命名形式呢?—— 这是 JNI 函数动态注册约定的函数命名规定。Java 的 native 办法和 JNI 函数是一一对应的映射关系,而建设这种映射关系的注册形式有 2 种:动态注册 + 动静注册。

其中,动态注册是基于命名约定建设的映射关系,一个 Java 的 native 办法对应的 JNI 函数会采纳约定的函数名,即 Java_[类的全限定名 (带下划线)]_[办法名] 。JNI 调用 sayHi() 办法时,就会从 JNI 函数库中寻找函数 Java_com_xurui_HelloWorld_sayHi(),更多内容见 注册 JNI 函数。

2.2 关键词 JNIEXPORT

JNIEXPORT 是宏定义,示意一个函数须要裸露给共享库内部应用时。JNIEXPORT 在 Window 和 Linux 上有不同的定义:

jni.h

// Windows 平台 :#define JNIEXPORT __declspec(dllexport)#define JNIIMPORT __declspec(dllimport)// Linux 平台:#define JNIIMPORT#define JNIEXPORT  __attribute__ ((visibility ("default")))

2.3 关键词 JNICALL

JNICALL 是宏定义,示意一个函数是 JNI 函数。JNICALL 在 Window 和 Linux 上有不同的定义:

jni.h

// Windows 平台 :#define JNICALL __stdcall // __stdcall 是一种函数调用参数的约定 ,示意函数的调用参数是从右往左。// Linux 平台:#define JNICALL

2.4 参数 jobject

jobject 类型是 JNI 层对于 Java 层利用类型对象的示意。每一个从 Java 调用的 native 办法,在 JNI 函数中都会传递一个以后对象的援用。辨别 2 种状况:

  • 1、动态 native 办法: 第二个参数为 jclass 类型,指向 native 办法所在类的 Class 对象;
  • 2、实例 native 办法: 第二个参数为 jobject 类型,指向调用 native 办法的对象。

2.5 JavaVM 和 JNIEnv 的作用

JavaVMJNIEnv 是定义在 jni.h 头文件中最要害的两个数据结构:

  • JavaVM: 代表 Java 虚拟机,每个 Java 过程有且仅有一个全局的 JavaVM 对象,JavaVM 能够跨线程共享;
  • JNIEnv: 代表 Java 运行环境,每个 Java 线程都有各自独立的 JNIEnv 对象,JNIEnv 不能够跨线程共享。

JavaVM 和 JNIEnv 的类型定义在 C 和 C++ 中略有不同,但实质上是雷同的,外部由一系列指向虚拟机外部的函数指针组成。 相似于 Java 中的 Interface 概念,不同的虚拟机实现会从它们派生出不同的实现类,而向 JNI 层屏蔽了虚拟机外部实现(例如在 Android ART 虚拟机中,它们的实现别离是 JavaVMExt 和 JNIEnvExt)。

jni.h

struct _JNIEnv;struct _JavaVM;#if defined(__cplusplus)// 如果定义了 __cplusplus 宏,则依照 C++ 编译typedef _JNIEnv JNIEnv;typedef _JavaVM JavaVM;#else// 依照 C 编译typedef const struct JNINativeInterface* JNIEnv;typedef const struct JNIInvokeInterface* JavaVM;#endif/* * C++ 版本的 _JavaVM,外部是对 JNIInvokeInterface* 的包装 */struct _JavaVM {    // 相当于 C 版本中的 JNIEnv    const struct JNIInvokeInterface* functions;    // 转发给 functions 代理    jint DestroyJavaVM()    { return functions->DestroyJavaVM(this); }    ...};/* * C++ 版本的 JNIEnv,外部是对 JNINativeInterface* 的包装 */struct _JNIEnv {    // 相当于 C 版本的 JavaVM    const struct JNINativeInterface* functions;    // 转发给 functions 代理    jint GetVersion()    { return functions->GetVersion(this); }    ...};

能够看到,不论是在 C 语言中还是在 C++ 中,JNINativeInterface*JNINativeInterface* 这两个构造体指针才是 JavaVM 和 JNIEnv 的实体。不过 C++ 中加了一层包装,在语法上更简洁,例如:

示例程序

// 在 C 语言中,要应用 (*env)->// 留神看这一句:typedef const struct JNINativeInterface* JNIEnv;(*env)->FindClass(env, "java/lang/String");// 在 C++ 中,要应用 env->// 留神看这一句:jclass FindClass(const char* name)//{ return functions->FindClass(this, name); }env->FindClass("java/lang/String");

后文提到的大量 JNI 函数,其实都是定义在 JNINativeInterface 和 JNINativeInterface 外部的函数指针。

jni.h

/* * JavaVM */struct JNIInvokeInterface {    // 一系列函数指针    jint        (*DestroyJavaVM)(JavaVM*);    jint        (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);    jint        (*DetachCurrentThread)(JavaVM*);    jint        (*GetEnv)(JavaVM*, void**, jint);    jint        (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);};/* * JNIEnv */struct JNINativeInterface {    // 一系列函数指针    jint        (*GetVersion)(JNIEnv *);    jclass      (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*, jsize);    jclass      (*FindClass)(JNIEnv*, const char*);    ...};

3. 数据类型转换

这一节咱们来探讨 Java 层与 Native 层之间的数据类型转换。

3.1 Java 类型映射(重点了解)

JNI 对于 Java 的根底数据类型(int 等)和援用数据类型(Object、Class、数组等)的解决形式不同。这个原理十分重要,了解这个原理能力了解前面所有 JNI 函数的设计思路:

  • 根底数据类型: 会间接转换为 C/C++ 的根底数据类型,例如 int 类型映射为 jint 类型。因为 jint 是 C/C++ 类型,所以能够间接当作一般 C/C++ 变量应用,而不须要依赖 JNIEnv 环境对象;
  • 援用数据类型: 对象只会转换为一个 C/C++ 指针,例如 Object 类型映射为 jobject 类型。因为指针指向 Java 虚拟机外部的数据结构,所以不可能间接在 C/C++ 代码中操作对象,而是须要依赖 JNIEnv 环境对象。另外,为了防止对象在应用时忽然被回收,在本地办法返回前,虚构机会固定(pin)对象,阻止其 GC。

另外须要特地留神一点,根底数据类型在映射时是间接映射,而不会产生数据格式转换。例如,Java char 类型在映射为 jchar 后旧是放弃 Java 层的样子,数据长度仍旧是 2 个字节,而字符编码仍旧是 UNT-16 编码。

具体映射关系都定义在 jni.h 头文件中,文件摘要如下:

jni.h

typedef uint8_t  jboolean; /* unsigned 8 bits */typedef int8_t   jbyte;    /* signed 8 bits */typedef uint16_t jchar;    /* unsigned 16 bits */ /* 留神:jchar 是 2 个字节 */typedef int16_t  jshort;   /* signed 16 bits */typedef int32_t  jint;     /* signed 32 bits */typedef int64_t  jlong;    /* signed 64 bits */typedef float    jfloat;   /* 32-bit IEEE 754 */typedef double   jdouble;  /* 64-bit IEEE 754 */typedef jint     jsize;#ifdef __cplusplus// 外部的数据结构由虚拟机实现,只能从虚拟机源码看class _jobject {};class _jclass : public _jobject {};class _jstring : public _jobject {};class _jarray : public _jobject {};class _jobjectArray : public _jarray {};class _jbooleanArray : public _jarray {};...// 阐明咱们接触到到 jobject、jclass 其实是一个指针typedef _jobject*       jobject;typedef _jclass*        jclass;typedef _jstring*       jstring;typedef _jarray*        jarray;typedef _jobjectArray*  jobjectArray;typedef _jbooleanArray* jbooleanArray;...#else /* not __cplusplus */...#endif /* not __cplusplus */

我将所有 Java 类型与 JNI 类型的映射关系总结为下表:

Java 类型JNI 类型形容长度(字节)
booleanjbooleanunsigned char1
bytejbytesigned char1
charjcharunsigned short2
shortjshortsigned short2
intjint、jsizesigned int4
longjlongsigned long8
floatjfloatsigned float4
doublejdoublesigned double8
ClassjclassClass 类对象1
Stringjstrting字符串对象/
Objectjobject对象/
Throwablejthrowable异样对象/
boolean[]jbooleanArray布尔数组/
byte[]jbyteArraybyte 数组/
char[]jcharArraychar 数组/
short[]jshortArrayshort 数组/
int[]jinitArrayint 数组/
long[]jlongArraylong 数组/
float[]jfloatArrayfloat 数组/
double[]jdoubleArraydouble 数组/

3.2 字符串类型操作

下面提到 Java 对象会映射为一个 jobject 指针,那么 Java 中的 java.lang.String 字符串类型也会映射为一个 jobject 指针。可能是因为字符串的应用频率切实是太高了,所以 JNI 标准还专门定义了一个 jobject 的派生类 jstring 来示意 Java String 类型,这个绝对非凡。

jni.h

// 外部的数据结构还是看不到,由虚拟机实现class _jstring : public _jobject {};typedef _jstring*       jstring;struct JNINativeInterface {    // String 转换为 UTF-8 字符串    const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);    // 开释 GetStringUTFChars 生成的 UTF-8 字符串    void        (*ReleaseStringUTFChars)(JNIEnv*, jstring, const char*);    // 结构新的 String 字符串    jstring     (*NewStringUTF)(JNIEnv*, const char*);    // 获取 String 字符串的长度    jsize       (*GetStringUTFLength)(JNIEnv*, jstring);    // 将 String 复制到预调配的 char* 数组中    void        (*GetStringUTFRegion)(JNIEnv*, jstring, jsize, jsize, char*);};

因为 Java 与 C/C++ 默认应用不同的字符编码,因而在操作字符数据时,须要特地留神在 UTF-16 和 UTF-8 两种编码之间转换。对于字符编码,咱们在 Unicode 和 UTF-8是什么关系? 这篇文章里探讨过,这里就简略回顾一下:

  • Unicode: 统一化字符编码标准,为全世界所有字符定义对立的码点,例如 U+0011;
  • UTF-8: Unicode 规范的实现编码之一,应用 1~4 字节的变长编码。UTF-8 编码中的一字节编码与 ASCII 编码兼容。
  • UTF-16: Unicode 规范的实现编码之一,应用 2 / 4 字节的变长编码。UTF-16 是 Java String 应用的字符编码;
  • UTF-32: Unicode 规范的实现编码之一,应用 4 字节定长编码。

以下为 2 种较为常见的转换场景:

  • 1、Java String 对象转换为 C/C++ 字符串: 调用 GetStringUTFChars 函数将一个 jstring 指针转换为一个 UTF-8 的 C/C++ 字符串,并在不再应用时调用 ReleaseStringChars 函数开释内存;
  • 2、结构 Java String 对象: 调用 NewStringUTF 函数结构一个新的 Java String 字符串对象。

咱们间接看一段示例程序:

示例程序

// 示例 1:将 Java String 转换为 C/C++ 字符串jstring jStr = ...; // Java 层传递过去的 Stringconst char *str = env->GetStringUTFChars(jStr, JNI_FALSE);if(!str) {    // OutOfMemoryError    return;}// 开释 GetStringUTFChars 生成的 UTF-8 字符串env->ReleaseStringUTFChars(jStr, str);// 示例 2:结构 Java String 对象(将 C/C++ 字符串转换为 Java String)jstring newStr = env->NewStringUTF("在 Native 层结构 Java String");if (newStr) {    // 通过 JNIEnv 办法将 jstring 调用 Java 办法(jstring 自身就是 Java String 的映射,能够间接传递到 Java 层)    ...}

此处对 GetStringUTFChars 函数的第 3 个参数 isCopy 做解释:它是一个布尔值参数,将决定应用拷贝模式还是复用模式:

  • 1、JNI_TRUE: 应用拷贝模式,JVM 将拷贝一份原始数据来生成 UTF-8 字符串;
  • 2、JNI_FALSE: 应用复用模式,JVM 将复用同一份原始数据来生成 UTF-8 字符串。复用模式绝不能批改字符串内容,否则 JVM 中的原始字符串也会被批改,突破 String 不可变性。

另外还有一个基于范畴的转换函数:GetStringUTFRegion:预调配一块字符数组缓冲区,而后将 String 数据复制到这块缓冲区中。因为这个函数自身不会做任何内存调配,所以不须要调用对应的开释资源函数,也不会抛出 OutOfMemoryError。另外,GetStringUTFRegion 这个函数会做越界查看并抛出 StringIndexOutOfBoundsException 异样。

示例程序

jstring jStr = ...; // Java 层传递过去的 Stringchar outbuf[128];int len = env->GetStringLength(jStr);env->GetStringUTFRegion(jStr, 0, len, outbuf);

3.3 数组类型操作

与 jstring 的解决形式相似,JNI 标准将 Java 数组定义为 jobject 的派生类 jarray

  • 根底类型数组:定义为 jbooleanArrayjintArray 等;
  • 援用类型数组:定义为 jobjectArray

上面辨别根底类型数组和援用类型数组两种状况:

操作根底类型数组(以 jintArray 为例):

  • 1、Java 根本类型数组转换为 C/C++ 数组: 调用 GetIntArrayElements 函数将一个 jintArray 指针转换为 C/C++ int 数组;
  • 2、批改 Java 根本类型数组: 调用 ReleaseIntArrayElements 函数并应用模式 0;
  • 3、结构 Java 根本类型数组: 调用 NewIntArray 函数结构 Java int 数组。

咱们间接看一段示例程序:

示例程序

extern "C"JNIEXPORT jintArray JNICALLJava_com_xurui_hellojni_HelloWorld_generateIntArray(JNIEnv *env, jobject thiz, jint size) {    // 新建 Java int[]    jintArray jarr = env->NewIntArray(size);    // 转换为 C/C ++ int[]    int *carr = env->GetIntArrayElements(jarr, JNI_FALSE);    // 赋值    for (int i = 0; i < size; i++) {        carr[i] = i;    }    // 开释资源并回写    env->ReleaseIntArrayElements(jarr, carr, 0);    // 返回数组    return jarr;}

此处重点对 ReleaseIntArrayElements 函数的第 3 个参数 mode 做解释:它是一个模式参数:

参数 mode形容
0将 C/C++ 数组的数据回写到 Java 数组,并开释 C/C++ 数组
JNI_COMMIT将 C/C++ 数组的数据回写到 Java 数组,并不开释 C/C++ 数组
JNI_ABORT不回写数据,但开释 C/C++ 数组

另外 JNI 还提供了基于范畴函数:GetIntArrayRegionSetIntArrayRegion,应用办法和注意事项和 GetStringUTFRegion 也是相似的,也是基于一块预调配的数组缓冲区。

操作援用类型数组(jobjectArray):

  • 1、将 Java 援用类型数组转换为 C/C++ 数组: 不反对!与根本类型数组不同,援用类型数组的元素 jobject 是一个指针,不存在转换为 C/C++ 数组的概念;
  • 2、批改 Java 援用类型数组: 调用 SetObjectArrayElement 函数批改指定下标元素;
  • 3、结构 Java 援用类型数组: 先调用 FindClass 函数获取 Class 对象,再调用 NewObjectArray 函数结构对象数组。

咱们间接看一段示例程序:

示例程序

extern "C"JNIEXPORT jobjectArray JNICALLJava_com_xurui_hellojni_HelloWorld_generateStringArray(JNIEnv *env, jobject thiz, jint size) {    // 获取 String Class    jclass jStringClazz = env->FindClass("java/lang/String");    // 初始值(可为空)    jstring initialStr = env->NewStringUTF("初始值");    // 创立 Java String[]    jobjectArray jarr = env->NewObjectArray(size, jStringClazz, initialStr);    // 赋值    for (int i = 0; i < size; i++) {        char str[5];        sprintf(str, "%d", i);        jstring jStr = env->NewStringUTF(str);        env->SetObjectArrayElement(jarr, i, jStr);    }    // 返回数组    return jarr;}

4. JNI 拜访 Java 字段与办法

这一节咱们来探讨如何从 Native 层拜访 Java 的字段与办法。在开始拜访前,JNI 首先要找到想拜访的字段和办法,这就依附字段描述符和办法描述符。

4.1 字段描述符与办法描述符

在 Java 源码中定义的字段和办法,在编译后都会依照既定的规定记录在 Class 文件中的字段表和办法表构造中。例如,一个 public String str; 字段会被拆分为字段拜访标记(public)、字段简略名称(str)和字段描述符(Ljava/lang/String)。 因而,从 JNI 拜访 Java 层的字段或办法时,首先就是要获取在 Class 文件中记录的简略名称和描述符。

Class 文件的一级构造:

字段表构造: 蕴含字段的拜访标记、简略名称、字段描述符等信息。例如字段 String str 的简略名称为 str,字段描述符为 Ljava/lang/String;

办法表构造: 蕴含办法的拜访标记、简略名称、办法描述符等信息。例如办法 void fun(); 的简略名称为 fun,办法描述符为 ()V

4.2 描述符规定

  • 字段描述符: 字段描述符其实就是形容字段的类型,JVM 对每种根底数据类型定义了固定的描述符,而援用类型则是以 L 结尾的模式:
Java 类型描述符
booleanZ
byteB
charC
shortS
intI
longJ
floagF
doubleD
voidV
援用类型以 L 结尾 ; 结尾,两头是 / 分隔的包名和类名。例如 String 的字段描述符为 Ljava/lang/String;
  • 办法描述符: 办法描述符其实就是形容办法的返回值类型和参数表类型,参数类型用一对圆括号括起来,依照参数申明程序列举参数类型,返回值呈现在括号前面。例如办法 void fun(); 的简略名称为 fun,办法描述符为 ()V

4.3 JNI 拜访 Java 字段

本地代码拜访 Java 字段的流程分为 2 步:

  • 1、通过 jclass 获取字段 ID,例如:Fid = env->GetFieldId(clz, "name", "Ljava/lang/String;");
  • 2、通过字段 ID 拜访字段,例如:Jstr = env->GetObjectField(thiz, Fid);

Java 字段分为动态字段和实例字段,相干办法如下:

  • GetFieldId:获取实例办法的字段 ID
  • GetStaticFieldId:获取静态方法的字段 ID
  • Get<Type>Field:获取类型为 Type 的实例字段(例如 GetIntField)
  • Set<Type>Field:设置类型为 Type 的实例字段(例如 SetIntField)
  • GetStatic<Type>Field:获取类型为 Type 的动态字段(例如 GetStaticIntField)
  • SetStatic<Type>Field:设置类型为 Type 的动态字段(例如 SetStaticIntField)

示例程序

extern "C"JNIEXPORT void JNICALLJava_com_xurui_hellojni_HelloWorld_accessField(JNIEnv *env, jobject thiz) {    // 获取 jclass    jclass clz = env->GetObjectClass(thiz);    // 示例:批改 Java 动态变量值    // 动态字段 ID    jfieldID sFieldId = env->GetStaticFieldID(clz, "sName", "Ljava/lang/String;");    // 拜访动态字段    if (sFieldId) {        // Java 办法的返回值 String 映射为 jstring        jstring jStr = static_cast<jstring>(env->GetStaticObjectField(clz, sFieldId));        // 将 jstring 转换为 C 格调字符串        const char *sStr = env->GetStringUTFChars(jStr, JNI_FALSE);        // 开释资源        env->ReleaseStringUTFChars(jStr, sStr);        // 结构 jstring        jstring newStr = env->NewStringUTF("动态字段 - Peng");        if (newStr) {            // jstring 自身就是 Java String 的映射,能够间接传递到 Java 层            env->SetStaticObjectField(clz, sFieldId, newStr);        }    }    // 示例:批改 Java 成员变量值    // 实例字段 ID    jfieldID mFieldId = env->GetFieldID(clz, "mName", "Ljava/lang/String;");    // 拜访实例字段    if (mFieldId) {        jstring jStr = static_cast<jstring>(env->GetObjectField(thiz, mFieldId));        // 转换为 C 字符串        const char *sStr = env->GetStringUTFChars(jStr, JNI_FALSE);        // 开释资源        env->ReleaseStringUTFChars(jStr, sStr);        // 结构 jstring        jstring newStr = env->NewStringUTF("实例字段 - Peng");        if (newStr) {            // jstring 自身就是 Java String 的映射,能够间接传递到 Java 层            env->SetObjectField(thiz, mFieldId, newStr);        }    }}

4.4 JNI 调用 Java 办法

本地代码拜访 Java 办法与拜访 Java 字段相似,拜访流程分为 2 步:

  • 1、通过 jclass 获取「办法 ID」,例如:Mid = env->GetMethodID(jclass, "helloJava", "()V");
  • 2、通过办法 ID 调用办法,例如:env->CallVoidMethod(thiz, Mid);

Java 办法分为静态方法和实例办法,相干办法如下:

  • GetMethodId:获取实例办法 ID
  • GetStaticMethodId:获取静态方法 ID
  • Call<Type>Method:调用返回类型为 Type 的实例办法(例如 GetVoidMethod)
  • CallStatic<Type>Method:调用返回类型为 Type 的静态方法(例如 CallStaticVoidMethod)
  • CallNonvirtual<Type>Method:调用返回类型为 Type 的父类办法(例如 CallNonvirtualVoidMethod)

示例程序

extern "C"JNIEXPORT void JNICALLJava_com_xurui_hellojni_HelloWorld_accessMethod(JNIEnv *env, jobject thiz) {    // 获取 jclass    jclass clz = env->GetObjectClass(thiz);    // 示例:调用 Java 静态方法    // 静态方法 ID    jmethodID sMethodId = env->GetStaticMethodID(clz, "sHelloJava", "()V");    if (sMethodId) {        env->CallStaticVoidMethod(clz, sMethodId);    }    // 示例:调用 Java 实例办法    // 实例办法 ID    jmethodID mMethodId = env->GetMethodID(clz, "helloJava", "()V");    if (mMethodId) {        env->CallVoidMethod(thiz, mMethodId);    }}

4.5 缓存 ID

拜访 Java 层字段或办法时,须要先利用字段名 / 办法名和描述符进行检索,取得 jfieldID / jmethodID。这个检索过程比拟耗时,优化办法是将字段 ID 和办法 ID 缓存起来,缩小反复检索。

提醒: 从不同线程中获取同一个字段或办法 的 ID 是雷同的,缓存 ID 不会有多线程问题。

缓存字段 ID 和 办法 ID 的办法次要有 2 种:

  • 1、应用时缓存: 应用时缓存是指在首次拜访字段或办法时,将字段 ID 或办法 ID 存储在动态变量中。这样未来再次调用本地办法时,就不须要反复检索 ID 了。例如:
  • 2、类初始化时缓存: 动态初始化时缓存是指在 Java 类初始化的时候,提前缓存字段 ID 和办法 ID。能够抉择在 JNI_OnLoad 办法中缓存,也能够在加载 so 库后调用一个 native 办法进行缓存。

两种缓存 ID 形式的次要区别在于缓存产生的机会和时效性:

  • 1、机会不同: 应用时缓存是提早按需缓存,只有在首次拜访 Java 时才会获取 ID 并缓存,而类初始化时缓存是提前缓存;
  • 2、时效性不同: 应用时缓存的 ID 在类卸载后生效,在类卸载后不能应用,而类加载时缓存在每次加载 so 动静库时会从新更新缓存,因而缓存的 ID 是放弃无效的。

5. JNI 中的对象援用治理

5.1 Java 和 C/C++ 中对象内存回收区别(重点了解)

在探讨 JNI 中的对象援用治理,咱们先回顾一下 Java 和 C/C++ 在对象内存回收上的区别:

  • Java: 对象在堆 / 办法区上调配,由垃圾回收器扫描对象可达性进行回收。如果应用局部变量指向对象,在不再应用对象时能够手动显式置空,也能够等到办法返回时主动隐式置空。如果应用全局变量(static)指向对象,在不再应用对象时必须手动显式置空。
  • C/C++: 栈上调配的对象会在办法返回时主动回收,而堆上调配的对象不会随着办法返回而回收,也没有垃圾回收器治理,因而必须手动回收(free/delete)。

而 JNI 层作为 Java 层和 C/C++ 层之间的桥接层,那么它就会兼具两者的特点:对于

  • 部分 Java 对象援用: 在 JNI 层能够通过 NewObject 等函数创立 Java 对象,并且返回对象的援用,这个援用就是 Local 型的部分援用。对于部分援用,能够通过 DeleteLocalRef 函数手动显式开释(这相似于在 Java 中显式置空局部变量),也能够等到函数返回时主动开释(这相似于在 Java 中办法返回时隐式置空局部变量);
  • 全局 Java 对象援用: 因为部分援用在函数返回后肯定会开释,能够通过 NewGlobalRef 函数将部分援用降级为 Global 型全局变量,这样就能够在办法应用对象(这相似于在 Java 中应用 static 变量指向对象)。在不再应用对象时必须调用 DeleteGlobalRef 函数开释全局援用(这相似于在 Java 中显式置空 static 变量)。
提醒: 咱们这里所说的 ”置空“ 只是将指向变量的值赋值为 null,而不是回收对象,Java 对象回收是交给垃圾回收器解决的。

5.2 JNI 中的三种援用

  • 1、部分援用: 大部分 JNI 函数会创立部分援用,部分援用只有在创立援用的本地办法返回前无效,也只在创立部分援用的线程中无效。在办法返回后,部分援用会主动开释,也能够通过 DeleteLocalRef 函数手动开释;
  • 2、全局援用: 部分援用要跨办法和跨线程必须降级为全局援用,全局援用通过 NewGlobalRef 函数创立,不再应用对象时必须通过 DeleteGlobalRef 函数开释。
  • 3、弱全局援用: 弱援用与全局援用相似,区别在于弱全局援用不会持有强援用,因而不会阻止垃圾回收器回收援用指向的对象。弱全局援用通过 NewGlobalWeakRef 函数创立,不再应用对象时必须通过 DeleteGlobalWeakRef 函数开释。

示例程序

// 部分援用jclass localRefClz = env->FindClass("java/lang/String");env->DeleteLocalRef(localRefClz);// 全局援用jclass globalRefClz = env->NewGlobalRef(localRefClz);env->DeleteGlobalRef(globalRefClz);// 弱全局援用jclass weakRefClz = env->NewWeakGlobalRef(localRefClz);env->DeleteGlobalWeakRef(weakRefClz);

5.3 JNI 援用的实现原理

在 JavaVM 和 JNIEnv 中,会别离建设多个表治理援用:

  • JavaVM 内有 globals 和 weak_globals 两个表治理全局援用和弱全局援用。因为 JavaVM 是过程共享的,因而全局援用能够跨办法和跨线程共享;
  • JavaEnv 内有 locals 表治理部分援用,因为 JavaEnv 是线程独占的,因而部分援用不能跨线程。另外虚拟机在进入和退出本地办法通过 Cookie 信息记录哪些部分援用是在哪些本地办法中创立的,因而部分援用是不能跨办法的。

5.4 比拟援用是否指向雷同对象

能够应用 JNI 函数 IsSameObject 判断两个援用是否指向雷同对象(实用于三种援用类型),返回值为 JNI_TRUE 时示意雷同,返回值为 JNI_FALSE 示意不同。例如:

示例程序

jclass localRef = ...jclass globalRef = ...bool isSampe = env->IsSamObject(localRef, globalRef)

另外,当援用与 NULL 比拟时含意略有不同:

  • 部分援用和全局援用与 NULL 比拟: 用于判断援用是否指向 NULL 对象;
  • 弱全局援用与 NULL 比拟: 用于判断援用指向的对象是否被回收。

6. JNI 中的异样解决

6.1 JNI 的异样解决机制(重点了解)

JNI 中的异样机制与 Java 和 C/C++ 的解决机制都不同:

  • Java 和 C/C++: 程序应用关键字 throw 抛出异样,虚构机会中断以后执行流程,转而去寻找匹配的 catch{} 块,或者持续向外层抛出寻找匹配 catch {} 块。
  • JNI: 程序应用 JNI 函数 ThrowNew 抛出异样,程序不会中断以后执行流程,而是返回 Java 层后,虚拟机才会抛出这个异样。

因而,在 JNI 层出现异常时,有 2 种解决抉择:

  • 办法 1: 间接 return 以后办法,让 Java 层去解决这个异样(这相似于在 Java 中向办法外层抛出异样);
  • 办法 2: 通过 JNI 函数 ExceptionClear 革除这个异样,再执行异样处理程序(这相似于在 Java 中 try-catch 解决异样)。须要留神的是,当异样产生时,必须先解决-革除异样,再执行其余 JNI 函数调用。 因为当运行环境存在未解决的异样时,只能调用 2 种 JNI 函数:异样护理函数和清理资源函数。

JNI 提供了以下与异样解决相干的 JNI 函数:

  • ThrowNew: 向 Java 层抛出异样;
  • ExceptionDescribe: 打印异样形容信息;
  • ExceptionOccurred: 查看以后环境是否产生异样,如果存在异样则返回该异样对象;
  • ExceptionCheck: 查看以后环境是否产生异样,如果存在异样则返回 JNI_TRUE,否则返回 JNI_FALSE;
  • ExceptionClear: 革除以后环境的异样。

jni.h

struct JNINativeInterface {    // 抛出异样    jint        (*ThrowNew)(JNIEnv *, jclass, const char *);    // 查看异样    jthrowable  (*ExceptionOccurred)(JNIEnv*);    // 查看异样    jboolean    (*ExceptionCheck)(JNIEnv*);    // 革除异样    void        (*ExceptionClear)(JNIEnv*);};

示例程序

// 示例 1:向 Java 层抛出异样jclass exceptionClz = env->FindClass("java/lang/IllegalArgumentException");env->ThrowNew(exceptionClz, "来自 Native 的异样");// 示例 2:查看以后环境是否产生异样(相似于 Java try{})jthrowable exc = env->ExceptionOccurred(env);if(exc) {    // 解决异样(相似于 Java 的 catch{})}// 示例 3:革除异样env->ExceptionClear();

6.2 查看是否产生异样的形式

异样解决的步骤我懂了,因为虚拟机在遇到 ThrowNew 时不会中断以后执行流程,那我怎么晓得以后曾经产生异样呢?有 2 种办法:

  • 办法 1: 通过函数返回值错误码,大部分 JNI 函数和库函数都会有特定的返回值来标示谬误,例如 -1、NULL 等。在程序流程中能够多查看函数返回值来判断异样。
  • 办法 2: 通过 JNI 函数 ExceptionOccurredExceptionCheck 查看以后是否有异样产生。

7. JNI 与多线程

这一节咱们来探讨 JNI 层中的多线程操作。

7.1 不能跨线程的援用

在 JNI 中,有 2 类援用是无奈跨线程调用的,必须时刻谨记:

  • JNIEnv: JNIEnv 只在所在的线程无效,在不同线程中调用 JNI 函数时,必须应用该线程专门的 JNIEnv 指针,不能跨线程传递和应用。通过 AttachCurrentThread 函数将以后线程附丽到 JavaVM 上,取得属于以后线程的 JNIEnv 指针。如果以后线程曾经附丽到 JavaVM,也能够间接应用 GetEnv 函数。

示例程序

JNIEnv * env_child;vm->AttachCurrentThread(&env_child, nullptr);// 应用 JNIEnv*vm->DetachCurrentThread();
  • 部分援用: 部分援用只在创立的线程和办法中无效,不能跨线程应用。能够将部分援用降级为全局援用后跨线程应用。

示例程序

// 部分援用jclass localRefClz = env->FindClass("java/lang/String");// 开释全局援用(非必须)env->DeleteLocalRef(localRefClz);// 部分援用降级为全局援用jclass globalRefClz = env->NewGlobalRef(localRefClz);// 开释全局援用(必须)env->DeleteGlobalRef(globalRefClz);

7.2 监视器同步

在 JNI 中也会存在多个线程同时拜访一个内存资源的状况,此时须要保障并发平安。在 Java 中咱们会通过 synchronized 关键字来实现互斥块(背地是应用监视器字节码),在 JNI 层也提供了相似成果的 JNI 函数:

  • MonitorEnter: 进入同步块,如果另一个线程曾经进入该 jobject 的监视器,则以后线程会阻塞;
  • MonitorExit: 退出同步块,如果以后线程未进入该 jobject 的监视器,则会抛出 IllegalMonitorStateException 异样。

jni.h

struct JNINativeInterface {    jint        (*MonitorEnter)(JNIEnv*, jobject);    jint        (*MonitorExit)(JNIEnv*, jobject);}

示例程序

// 进入监视器if (env->MonitorEnter(obj) != JNI_OK) {    // 建设监视器的资源分配不胜利等}// 此处为同步块if (env->ExceptionOccurred()) {    // 必须保障有对应的 MonitorExit,否则可能呈现死锁    if (env->MonitorExit(obj) != JNI_OK) {        ...    };    return;}// 退出监视器if (env->MonitorExit(obj) != JNI_OK) {    ...};

7.3 期待与唤醒

JNI 没有提供 Object 的 wati/notify 相干性能的函数,须要通过 JNI 调用 Java 办法的形式来实现:

示例程序

static jmethodID MID_Object_wait;static jmethodID MID_Object_notify;static jmethodID MID_Object_notifyAll;voidJNU_MonitorWait(JNIEnv *env, jobject object, jlong timeout) {    env->CallVoidMethod(object, MID_Object_wait, timeout);}voidJNU_MonitorNotify(JNIEnv *env, jobject object) {    env->CallVoidMethod(object, MID_Object_notify);}voidJNU_MonitorNotifyAll(JNIEnv *env, jobject object) {    env->CallVoidMethod(object, MID_Object_notifyAll);}

7.4 创立线程的办法

在 JNI 开发中,有两种创立线程的形式:

  • 办法 1 - 通过 Java API 创立: 应用咱们相熟的 Thread#start() 能够创立线程,长处是能够不便地设置线程名称和调试;
  • 办法 2 - 通过 C/C++ API 创立: 应用 pthread_create() 或 std::thread 也能够创立线程

示例程序

// void *thr_fn(void *arg) {    printids("new thread: ");    return NULL;}int main(void) {    pthread_t ntid;    // 第 4 个参数将传递到 thr_fn 的参数 arg 中    err = pthread_create(&ntid, NULL, thr_fn, NULL);    if (err != 0) {        printf("can't create thread: %s\n", strerror(err));    }    return 0;}

8. 通用 JNI 开发模板

光说不练假把式,以下给出一个简略的 JNI 开发模板,将包含上文提到的一些比拟重要的知识点。程序逻辑很简略:Java 层传递一个媒体文件门路到 Native 层后,由 Native 层播放媒体并回调到 Java 层。为了程序简化,所有实在的媒体播放代码都移除了,只保留模板代码。

  • Java 层:start() 办法开始,调用 startNative() 办法进入 Native 层;
  • Native 层: 创立 MediaPlayer 对象,其中在子线程播放媒体文件,并通过事后持有的 JavaVM 指针获取子线程的 JNIEnv 对象回调到 Java 层 onStarted() 办法。

MediaPlayer.kt

// Java 层模板class MediaPlayer {    companion object {        init {            // 留神点:加载 so 库            System.loadLibrary("hellondk")        }    }    // Native 层指针    private var nativeObj: Long? = null    fun start(path : String) {        // 留神点:记录 Native 层指针,后续操作能力拿到 Native 的对象        nativeObj = startNative(path)    }    fun release() {        // 留神点:应用 start() 中记录的指针调用 native 办法        nativeObj?.let {            releaseNative(it)        }        nativeObj = null    }    private external fun startNative(path : String): Long    private external fun releaseNative(nativeObj: Long)    fun onStarted() {        // Native 层回调(来自 JNICallbackHelper#onStarted)        ...    }}

native-lib.cpp

// 留神点:记录 JavaVM 指针,用于在子线程取得 JNIEnvJavaVM *vm = nullptr;jint JNI_OnLoad(JavaVM *vm, void *args) {    ::vm = vm;    return JNI_VERSION_1_6;}extern "C"JNIEXPORT jlong JNICALLJava_com_pengxr_hellondk_MediaPlayer_startNative(JNIEnv *env, jobject thiz, jstring path) {    // 留神点:String 转 C 格调字符串    const char *path_ = env->GetStringUTFChars(path, nullptr);    // 结构一个 Native 对象    auto *helper = new JNICallbackHelper(vm, env, thiz);    auto *player = new MediaPlayer(path_, helper);    player->start();    // 返回 Native 对象的指针    return reinterpret_cast<jlong>(player);}extern "C"JNIEXPORT void JNICALLJava_com_pengxr_hellondk_MediaPlayer_releaseNative(JNIEnv *env, jobject thiz, jlong native_obj) {    auto * player = reinterpret_cast<MediaPlayer *>(native_obj);    player->release();}

JNICallbackHelper.h

#ifndef HELLONDK_JNICALLBACKHELPER_H#define HELLONDK_JNICALLBACKHELPER_H#include <jni.h>#include "util.h"class JNICallbackHelper {private:    // 全局共享的 JavaVM*    // 留神点:指针要初始化 0 值    JavaVM *vm = 0;    // 主线程的 JNIEnv*    JNIEnv *env = 0;    // Java 层的对象 MediaPlayer.kt    jobject job;    // Java 层的办法 MediaPlayer#onStarted()    jmethodID jmd_prepared;public:    JNICallbackHelper(JavaVM *vm, JNIEnv *env, jobject job);    ~JNICallbackHelper();    void onStarted();};#endif //HELLONDK_JNICALLBACKHELPER_H

JNICallbackHelper.cpp

#include "JNICallbackHelper.h"JNICallbackHelper::JNICallbackHelper(JavaVM *vm, JNIEnv *env, jobject job) {    // 全局共享的 JavaVM*    this->vm = vm;    // 主线程的 JNIEnv*    this->env = env;    // C 回调 Java    jclass mediaPlayerKTClass = env->GetObjectClass(job);    jmd_prepared = env->GetMethodID(mediaPlayerKTClass, "onPrepared", "()V");    // 留神点:jobject 无奈逾越线程,须要转换为全局援用    // Error:this->job = job;    this->job = env->NewGlobalRef(job);}JNICallbackHelper::~JNICallbackHelper() {    vm = nullptr;    // 留神点:开释全局援用    env->DeleteGlobalRef(job);    job = nullptr;    env = nullptr;}void JNICallbackHelper::onStarted() {    // 留神点:子线程不能间接应用持有的主线程 env,须要通过 AttachCurrentThread 获取子线程的 env    JNIEnv * env_child;    vm->AttachCurrentThread(&env_child, nullptr);    // 回调 Java 办法    env_child->CallVoidMethod(job, jmd_prepared);    vm->DetachCurrentThread();}

MediaPlayer.h

#ifndef HELLONDK_MEDIAPLAYER_H#define HELLONDK_MEDIAPLAYER_H#include <cstring>#include <pthread.h>#include "JNICallbackHelper.h"class MediaPlayer {private:    char *path = 0;    JNICallbackHelper *helper = 0;    pthread_t pid_start;public:    MediaPlayer(const char *path, JNICallbackHelper *helper);    ~MediaPlayer();    void doOpenFile();    void start();    void release();};#endif //HELLONDK_MEDIAPLAYER_H

MediaPlayer.cpp

#include "MediaPlayer.h"MediaPlayer::MediaPlayer(const char *path, JNICallbackHelper *helper) {    // 留神点:参数 path 指向的空间被回收会造成悬空指针,应复制一份    // this->path = path;    this->path = new char[strlen(path) + 1];    strcpy(this->path, path);    this->helper = helper;}MediaPlayer::~MediaPlayer() {    if (path) {        delete path;    }    if (helper) {        delete helper;    }}// 在子线程执行void MediaPlayer::doOpenFile() {    // 省略实在播放逻辑...    // 媒体文件关上胜利    helper->onStarted();}// 在子线程执行void *task_open(void *args) {    // args 是 主线程 MediaPlayer 的实例的 this变量    auto *player = static_cast<MediaPlayer *>(args);    player->doOpenFile();    return nullptr;}void MediaPlayer::start() {    // 切换到子线程执行    pthread_create(&pid_start, 0, task_open, this);}void MediaPlayer::release() {    ...}

9. 总结

到这里,JNI 的常识就讲完了,你能够依照学习路线图来看。下一篇,咱们开始讲 Android NDK 开发。关注我,带你建设外围竞争力,咱们下次见。


参考资料

  • 《JNI 编程指南》
  • JNI 提醒 —— Android 官网文档
  • Java 原生接口标准 —— Java 官网文档
  • 深刻了解 Android:卷 1(第 2 章 · 深刻了解 JNI) —— 邓凡平 著
  • 深刻了解 Android:Java 虚拟机 ART(第 11 章 · ART 中的 JNI) —— 邓凡平 著
  • Android 利用平安防护和逆向剖析(根底篇) —— 姜维 著
  • Java 性能权威指南:第 2 版(第 12.5 节:Java 原生接口) —— [美]Scott Oaks 著
  • Android 对 so 体积优化的摸索与实际 —— 洪凯 常强(美团技术团队)著
都看到这里了,就点赞 反对吧!微信搜寻公众号 [彭旭锐],你能够探讨技术,找到气味相投的敌人,建设外围竞争力,咱们下次见!

好的身材才是反动写代码的成本!