JNI
MoMo Lv5
  1. 什么是JNI?它主要用来干什么。 ⭐⭐⭐⭐⭐
  2. Java 声明的Native方法如何和Native层的Native函数进行绑定的?⭐⭐⭐⭐⭐
  3. JNI如何实现数据传递?⭐⭐⭐⭐
  4. 如何全局捕获Native发生的异常?⭐⭐⭐
  5. JNIEnv与JavaVM的关系⭐⭐⭐⭐
  6. C和C++的JNIEnv的区别 ⭐⭐⭐
  7. JNI项目配置和数据映射 ⭐⭐

什么是JNI、NDK

JNI

JNI(Java Native Interface)就是Java本地化接口。在Windows,Linux,MacOS等操作系统的底层驱动都是使用C/C++开发的,因此这些系统提供的API函数都是C/C++编写的。而在安卓开发中,我们使用java编程写的代码都是在Java虚拟机中,编译成虚拟机可以运行的Java字节码.class文件,再通过JIT技术即时编译成本地机器码,所以效率是比不上C/C++的。因此,很容易联想到,我们希望能有这么一个中间件,支持我们在Java代码中与本地系统的C/C++代码做个交互,这个中间件就是JNI。

Java一次编译到处执行: JVM在不同的操作系统都有实现,Java可以一次编译到处运行,字节码文件一旦编译好了,可以放在任何平台的虚拟机上运行;

Java语言执行流程

  • 编译字节码:Java编译器编译 .java源文件,获得.class 字节码文件;
  • 装载类库:使用类装载器装载平台上的Java类库,并进行字节码验证;
  • Java虚拟机:将字节码加入到JVM中,Java解释器和即时编译器同时处理字节码文件,将处理后的结果放入运行时系统;
  • 调用JVM所在平台类库:JVM处理字节码后,转换成相应平台的操作,调用本平台底层类库进行相关处理;

image

1.2 JNI 与 NDK 的联系和区别

NDK(Native Development Kit),翻译过来是“本地开发工具”,是Google开发的一套开发和编译工具集,可快速生成C、C++的动态库,并自动把so和应用打包成apk,主要用于Android的JNI开发;

因此,JNI是一套编程接口,可以实现Java代码和本地C/C++代码进行交互。而NDK可以理解为Android实现JNI的一种工具,通过该工具打包C/C++动态库并自动打包进APK/AAR中。

JNI的两种注册方式

JNI有静态注册和动态注册两种方式,多用动态注册。

静态注册

静态注册的原理是:根据函数名建立Java方法和JNI函数的一一对应关系。步骤如下:

  1. 先声明 Java 的 native 方法;
  2. 使用 javah 工具生成对应的头文件,在Terminal控制台执行以下任一命令生成由包名加类名命名的 jni 层头文件:
  • javah packagename.classname
  • javah -o my_jni.h packagename.classname,其中 my_jni.h 为自定义的文件名;
  1. 实现对应的native方法,并在Java中通过System.loadLibrary()方法加载 so 库即可;

因为有Android Studio这个强大的工具,我们可以很轻松建立一个JNI项目工程。

image

新创建的项目就有了默认的JNI函数了,下面做简单介绍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MainActivity extends AppCompatActivity {

// Used to load the 'myapplication' library on application startup.
static {
System.loadLibrary("myapplication"); //1
}

private ActivityMainBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(stringFromJNI()); //2
}

/**
* A native method that is implemented by the 'myapplication' native library,
* which is packaged with this application.
*/
public native String stringFromJNI(); //3
}

在[注释3]先声明 native 方法,并在[注释1]加载so库,最后在[注释2]调用native函数stringFromJNI(),其对应的实现在:native-lib.cpp文件里:

1
2
3
4
5
6
7
8
9
10
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL //4
Java_com_example_myapplication_MainActivity_stringFromJNI( //5
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello World";
return env->NewStringUTF(hello.c_str()); //6
}

[注释4]是两个关键词:JNIEXPORT 和 JNICALL,这两个都是宏定义,在jni.h文件中定义:

1
2
3
#define JNIIMPORT
#define JNIEXPORT __attribute__ ((visibility ("default")))
#define JNICALL

主要作用是注明该函数Java_com_example_myapplication_MainActivity_stringFromJNI()是JNI函数,那么当虚拟机加载so库时,就会将该函数链接到对应的Java层native方法,World”的字符串。

native层函数命名规则

上一小节的[注释3]stringFromJNI()和[注释5]Java_com_example_myapplication_MainActivity_stringFromJNI(JNIEnv* env,jobject)有匹配关系,其Natvie层函数命名遵循以下规则:

JNIEXPORT 返回值 JNICALL Java_全路径类名_方法名_参数签名(JNIEnv* , jclass, 其它参数); 同时还有以下几个需要注意的小点:

  • 如果是c++文件,如上述例子Natvie方法放在native-lib.cpp文件里,此时需要在Native函数前面加上 extern “C”
  • 如果该函数是重载的方法,则需要加上“参数签名”,参数签名见3.3.1小节,上述例子非重载方法,因此命名上不需要加“参数签名”;
  • 包名或类名或方法名中含下划线 _ 要用 _1 连接;
  • 重载的Native方法命名中的“方法名”后面要用双下划线 __ 连接;
  • 参数签名的斜杠 “/” 改为下划线 “_” 连接,分号 “;” 改为 “_2” 连接,左方括号 ‘[‘改为 ‘_3’ 连接 ;
  • 如果Java层方法是static方法,则Native层方法的第二个形参是jclass,否则是jobject,如上述例子,Java层为非static方法,所以Native层方法的第二个参数是jobject。

静态注册的优缺点

静态注册优点就是实现简单,编写好Java方法后用javah工具就可以将Java代码中声明的native方法转换为native层的代码函数,并直接实现native层代码逻辑就行。然而缺点也比较明显:

  • 每次增加新的函数或者修改函数名等,都需要手动在运行javah命令,比较麻烦。同时,生成的Native层函数名字太长了,可读性不高;
  • 首次调用Native函数时,需要根据函数名在Java层和Native层直接建立函数链接,比较耗时;

因此,无论是实用性还是效率,都推荐使用动态注册。

动态注册

动态注册的原理是通过使用 JNINativeMethod 结构来保存Java层声明的native方法和Native层函数的关联关系,直接告Java层声明的native方法其在Native层中对应函数的指针。该结构体的定义和动态注册需要用到的关键函数也在jni.h文件中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//JNINativeMethod结构体
typedef struct {
const char* name; //Java层声明的中的native方法的名字
const char* signature; //Java中native方法的函数签名
void* fnPtr; //对应Native层函数的指针
} JNINativeMethod;

/**
* @param clazz java类名,通过 FindClass 获取
* @param methods JNINativeMethod 结构体指针
* @param nMethods 方法个数
*/
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)

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

动态注册的步骤:

  1. 先声明 Java层 的 native 方法;
  2. 同步实现Native层函数的实现,函数名可以任意取!
  3. 利用结构体 JNINativeMethod 保存Java层native方法和 Native层的JNI函数的对应关系;
  4. 利用registerNatives(JNIEnv* env)注册类的所有本地方法;
  5. 在 JNI_OnLoad() 方法中调用步骤4的注册方法;
  6. 在Java中通过System.loadLibrary加载完JNI动态库之后,会调用JNI_OnLoad()方法,完成动态注册;

代码实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Java层的MainActivity.java文件:

public class MainActivity extends AppCompatActivity {

// Used to load the 'myapplication' library on application startup.
static {
System.loadLibrary("myapplication"); //7
}

private ActivityMainBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(stringFromJNI()); // 8
}

/**
* A native method that is implemented by the 'myapplication' native library,
* which is packaged with this application.
*/
public native String stringFromJNI(); //9
}

// 自定义的interface.cpp文件:

jint JNI_OnLoad(JavaVM* vm, void* reserved) //10
{
...
if (JNI_FALSE == registerMethods(m_pEnv, CLASSNAME, gMethods, NELEM(gMethods))) // 11
{
LOGE(LOG_TAG, "load method fail");
return JNI_ERR;
}
...
return JNI_VERSION_1_6;
}

//JNINativeMethod结构体定义
static JNINativeMethod gMethods[] = { //12
{"stringFromJNI", "()Ljava/lang/String", (void*)stringFromJNI},
}

在[注释7]加载好库后,会自动调用[注释10]JNI_OnLoad()方法并最终执行[注释11]registerMethods()方法,对[注释12]定义的结构体进行注册。这样Java声明的native函数就可以使用啦。

JNI语法

JNI项目配置

使用Android Studio新创建的JNI工程可以快速的实现Java层和Native层函数的交互,这是因为Android Studio已经自动的帮我们完成了项目配置,主要有以下几点:

build.gradle

build.gradle 文件配置:注意两个externalNativeBuild {}的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
android {
compileSdkVersion 30
buildToolsVersion "30.0.1"

defaultConfig {
minSdkVersion 21
targetSdkVersion 30
..

externalNativeBuild {
cmake {
cppFlags "-std=c++11" //使用 C++11 标准
abiFilters "armeabi-v7a", "arm64-v8a" // 生成.so库的目标平台
}
}
}

externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt') //13:配置 CMake 文件的路径
version '3.18.1'
}
}

3.1.2 CMakeLists.txt文件

CMakeLists.txt文件 :上面[注释13]配置的CMake文件CMakeLists.txt是Android Studio自动生成的CMake脚本配置文件,以下是Android Studio首次自动生成还未进行自定义修改的CMakeLists.txt文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.18.1) # 1.指定cmake版本

# Declares and names the project.

project("myapplication")

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
myapplication # 2.生成函数库的名字,System.loadLibrary()方法加载的动态库的名字

# Sets the library as a shared library.
SHARED # 3. 生成动态函数

# Provides a relative path to your source file(s).
native-lib.cpp) # 4. 依赖的cpp文件,每添加一个 C/C++文件都要添加到这里,不然不会被编译

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
log-lib # 5. 设置path变量的名称

# Specifies the name of the NDK library that
# you want CMake to locate.
log) # 6.指定要查询库的名字

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
myapplication # 7. 目标库, 和上面注释 '#2' 生成的函数库名字一致

# Links the target library to the log library
# included in the NDK.
${log-lib}) # 8. 连接的库,根据log-lib变量对应liblog.so函数库

那如果需要加上自定义的修改,如添加新的c/c++文件,添加新的动态库,或者修改动态库的默认名字,需要如何修改呢?

  • 在add_library()这个方法里可以在[注释#2]修改动态库名字,在[注释#4]继续添加新的c/c++文件;
  • 当然也可以另写一个add_library()方法添加新的动态库,然后记得在[注释#7]下面也加上新的动态库的名字;
  • 添加文件夹:上面native-lib.cpp文件路径是src/main/cpp/native-lib.cpp,假如我们需要添加一个src/main/cpp/include文件夹来包含项目需求用到的其他.h头文件,则需要加上:include_directories(${CMAKE_SOURCE_DIR}/include)将指定目录添加到编译器的头文件搜索路径之下。

至于其他的语法请自行百度“Cmake命令“。

数据映射

基本数据类型映射

Java层基本数据类型和Native层数据类型会有如下映射关系,这些都是在jni.h里面有定义的:

Java类型 本地类型 描述

boolean jboolean C/C++无符号8位整型(unsigned char)
byte jbyte C/C++带符号8位整型(char)
char jchar C/C++无符号16位整型(unsigned short)
short jshort C/C++带符号16位整型(short)
int jint C/C++带符号32位整型(int)
long jlong C/C++带符号64位整型(long)
float jfloat C/C++32位浮点型(float)
double jdouble C/C++64位浮点型(double)

引用数据类型映射

Java引用数据类型和Native层的映射如下:

Java类型 本地类型 描述

Object jobject 任何Java对象
Class jclass Java中的类对象
String jstring 字符串对象
Object[] jobjectArray 任何Java对象的数组
boolean[] jbooleanArray 布尔型数组
byte[] jbyteArray 比特型数组
char[] jcharArray 字符型数组
short[] jshortArray 短整型数组
int[] jintArray 整型数组
long[] jlongArray 长整型数组
float[] jfloatArray 浮点型数组
double[] jdoubleArray 双浮点型数组

不过需要注意,引用类型是不能直接在Native层使用的,需要根据JNI函数进行类型转换后才可以使用。举个例子,Java层声明了public native int getLength(int[] arr);则对应的Native层函数应该对int[]进行如下转换:

1
2
3
4
5
6
7
8
9
10
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jint JNICALL
Java_com_example_myapplication_MainActivity_getLength((JNIEnv *env, jobject jobj, jintArray arr_) {
jint len = 0;
jint *arr = env->GetIntArrayElements(arr_, 0); //根据JNI函数进行类型转换后才可以使用
len = env->GetArrayLength(arr_); //获取int数组的长度
return len;
}

方法和变量ID

Java层的方法Natvie层也不能直接使用,需要通过JNI提供的函数获取方法的ID,再根据这个ID通过JNI提供的函数获取到对应的方法,经过这几个步骤才可以在Native层调用Java层的方法,变量也是一样的道理。ID的结构体定义也在jni.h文件里:

1
2
3
4
5
struct _jfieldID;                       /* opaque structure */
typedef struct _jfieldID* jfieldID; /* field IDs */

struct _jmethodID; /* opaque structure */
typedef struct _jmethodID* jmethodID; /* method IDs */

举个例子,Java层的Person类有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义
public int mAge = 10;
public void setAge(int age) {
this.mAge = sex;
}
public int getAge(){
return mAge;
}

public native void agePlusOne();

// 调用
Person person = new Person();
Log.d(TAG, "调用前:getAge() = " + person.getAge());
methodJni.agePlusOne();
Log.d(TAG, "调用后:getAge() = " + person.getAge());

对应的Native层代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extern "C"
JNIEXPORT void JNICALL
Java_com_example_myapplication_MainActivity_agePlusOne(JNIEnv *env, jobject jobj) {
// 1. 获取实例对应的 class
jclass jclazz = env->GetObjectClass(jobj);
// 2. 通过class获取相应的变量的 field id
jfieldID fid = env->GetFieldID(jclazz, "mAge", "I");
// 3. 通过 field id 获取对应的值
jint age = env->GetIntField(jobj, fid); //注意,不是用 jclazz, 使用第二个参数 jobj
age++;
// 4. 获取方法的 id
jmethodID mid = env->GetMethodID(jclazz, "setAge", "(I)V");
// 5. 通过该 class 调用对应的setAge()方法
env->CallVoidMethod(jobj, mid, age);
}

最終的打印結果:

1
2
MainActivity: 调用前:getAge() = 10
MainActivity: 调用后:getAge() = 11

JNI 描述符

域描述符

基础类型描述符

基础类型描述符常用于上述注册JNI函数时用到的函数签名,参数签名,除了boolean的描述符是Z,long类型的描述符是J之外,其他基础类型的描述符都是其类型名称的大写首字母:

Java基础类型 域描述符

boolean Z
long J
int I
char C
short S
float F
double D
void V
byte B

3.3.1.2 引用类型描述符

引用类型的描述符需要重点注意,类描述符前面需要加上’L’,后面需要加上’ ; ‘

1
L + 引用类描述符 + ;

比如String的域描述符就是Ljava/lang/String;

数组的域描述符也比较特殊,如果是n级数组,则域描述符有n个’ [ ‘,同时:数组类型为:

  • 基本数据类型:域描述符不加分号
  • Class类:域描述符以分号结尾

例如:

1
2
3
4
5
6
7
int[]    描述符为 [I
double[] 描述符为 [D
String[] 描述符为 [Ljava/lang/String;
Object[] 描述符为 [Ljava/lang/Object;
int[][] 描述符为 [[I
double[][] 描述符为 [[D
ClassA[] 描述符为 [com/android/example/User;

类描述符

类描述符是类的完整名称:L+包名+类名+;,java 中包名用’ . ‘分割,jni 中改为用’ / ‘分割。如com.android.example包下面的User类的类描述符就是:Lcom/android/example/User;

在Native层获取Java类对象可以通过FindClass()方法,如

1
jclass jclazz = env->FindClass("Lcom/android/example/User;");

方法描述符

方法描述符规则是:

1
(参数……)返回类型

如果没有参数,可以不需要括号。例如:

1
2
3
4
5
  Java 层方法   ——>  JNI 函数签名
String getName() ——> Ljava/lang/String;
int getTotal(int a, int b) ——> (II)I
int getLength(int[] array) ——> ([I)I
void setAge(int age) ——> (I)V

JNIEnV分析

1
Java_com_example_myapplication_MainActivity_getLength((JNIEnv *env, jobject jobj, jintArray arr_) {}

在JNI函数中,第一个形参就是JNIEnv,JNIEnv是jni.h文件最重要的部分,定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv; // 14:C++
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv; // 15:C
typedef const struct JNIInvokeInterface* JavaVM;
#endif

...
struct _JNIEnv { //16
/* do not rename this; it does not seem to be entirely opaque */
const struct JNINativeInterface* functions;
...
jstring NewStringUTF(const char* bytes) // 17
{ return functions->NewStringUTF(this, bytes); }
...
}

其中JNINativeInterface的注释”Table of interface function pointers”翻译过来就是 是接口函数表指针,因此无论是上述C++还是C针对JNIEnv的定义,都代表JNIEnv是指向JNINativeInterface的指针,即是指向函数表指针的指针。很明显上面的JavaVM也是指向函数表指针的指针

1
2
3
4
5
6
7
8
/*
* Table of interface function pointers.
*/
struct JNINativeInterface {
void* reserved0;
void* reserved1;
...
}

C和C++的JNIEnv的区别

我们具体分析下C和C++的JNIEnv的区别,先看C,对应上述源码[注释15]:

1
2
3
4
5
6
// c的定义
typedef const struct JNINativeInterface* JNIEnv;
// 在使用时,作为第一个形参:
Java_com_example_myapplication_MainActivity_getLength((JNIEnv *env, jobject jobj, jintArray arr_) {}
// 可以转换为:
Java_com_example_myapplication_MainActivity_getLength((JNINativeInterface* *env, jobject jobj, jintArray arr_) {}

也就是说此时的env实际是一个二级指针,因此要获取到JNINativeInterface这个“接口函数表指针”的,需要执行*env,此时*env才可以去调用接口函数表里面的函数:例如:

1
(*env)->NewStringUTF(env, "hello")

而C++的JNIEnv对应[注释14]:typedef _JNIEnv JNIEnv; 其中_JNIEnv 的定义对应[注释16],结构体定义了JNINativeInterface* functions,即定义了 JNINativeInterface 的结构体指针,因此

1
2
3
4
5
6
// c++的定义
`typedef _JNIEnv JNIEnv;
// 在使用时,作为第一个形参:
Java_com_example_myapplication_MainActivity_getLength((JNIEnv *env, jobject jobj, jintArray arr_) {}
// 可以转换为:
Java_com_example_myapplication_MainActivity_getLength((_JNIEnv *env, jobject jobj, jintArray arr_) {}

如果需要调用[注释17]的NewStringUTF()方法,则只需要执行:

1
2
// env不需要加' * ' 号,也不需要作为参数传入NewStringUTF()方法
env->NewStringUTF("hello")

JNIEnv的特点

  • JNIEnv是一个指针,指向一组JNI函数,如上面[注释17]的NewStringUTF()方法,通过这些方法实现了Java层和Native层的交互;
  • 在c文件的JNI函数,JNIEnv需要作为第一个函数参数,而在C++则不需要作为函数参数;

JavaEnv 和 JavaVM 的关系

看[注释15]处,有:

1
2
typedef const struct JNINativeInterface* JNIEnv; 
typedef const struct JNIInvokeInterface* JavaVM;

JNIEnv和JavaVM定义完成一样,那么这两者有什么联系和区别:

  • Android每个进程只有一个JavaVM虚拟机对象,但可以有多个线程,当新线程第一次调用JNI函数时,虚拟机会给该线程生成一个JNIEnv指针。所以JNIEnv是线程局部存储,保证多线程之间的JNI通讯都是独立的。因此一个进程只有一个 JavaVM,但可能有多个 JNIEnv;
  • JNIEnv 内部的函数执行环境来源于 Dalvik 虚拟机;
  • 当本地的 C/C++ 代码想要获得当前线程所想要使用的 JNIEnv 时,可以使用 Dalvik VM 对象的 JavaVM* jvm->GetEnv()方法,该方法会返回当前线程所在的 JNIEnv*;
  • Java 的 dex 字节码和 C/C++ 的 .so 同时运行 Dalvik VM 之内,共同使用一个进程空间;
Powered by Hexo & Theme Keep
Unique Visitor Page View