JNI

[JNI] NDK Native Method Register

Posted by xiuyuantech on 2020-06-24

JNI(Java Native Interface,Java本地接口),
用于打通Java层与Native(C/C++)层。
这不是Android系统所独有的,而是Java所有。
众所周知,Java语言是跨平台的语言,
而这跨平台的背后都是依靠Java虚拟机,
虚拟机采用C/C++编写,适配各个系统,
通过JNI为上层Java提供各种服务,保证跨平台性。
这样就产生了一个问题,
Java世界的代码要怎么使用Native世界的代码呢,
这就需要一个桥梁来将它们连接在一起,
JNI就是这个桥梁。

JNI方法注册分为静态注册和动态注册,其中静态注册多用于NDK开发,而动态注册多用于Framework开发。
Java世界对应的是.java类文件,也就是我们应用开发中直接调用的类。JNI层对应的是libxxx.so或者libxxx.a,so是动态库,a是静态库。这些库完成了实际的调用的功能。

静态库 和 动态库 区别

静态库

  • 全称是静态链接库(Static Library),后缀是 .a,例如 libmedia.a

  • 调用静态库的程序在编译时会将静态库全部编译到目标代码中,运行环境中不再需要静态库,并且静态库文件体积较大

  • 调用静态库时,如果对静态库中的函数内容进行改变,不仅需要重新编译静态库,还需要对调用静态库的程序重新编译,将静态库编译到目标代码中。

动态库

  • 全称是动态链接库(Shared Library),后缀是 .so,例如 libmedia.so

  • 调用动态库的程序在编译时不能将动态库编译到目标代码中,程序执行到相关函数时才会链接该动态库对应的函数,所以运行环境中必须提供动态库,并且动态库文件体积较小

  • 调用动态库时,如果对动态库中的函数内容进行改变,只需要重新编译动态库,不需要对调用动态库的程序重新编译,即不需要干预目标代码,直接用新的动态库替换掉旧的动态库即可

静态注册

java层:

1
2
3
4
5
6
7
8
9
10
11
package com.example;
public class MainActivity {
static {
System.loadLibrary("hello-jni");
init();
}

public native String stringFromJNI();
public static native String init();

}

native层:
头文件 com_example_MainActivity.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <jni.h>
#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT void JNICALL Java_com_example_MainActivity_init
(JNIEnv *, jclass);

JNIEXPORT jstring JNICALL Java_com_example_MainActivity_stringFromJNI
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

方法被声明格式为Java_包名_类名_方法名,其中JNIEnv * 是一个指向全部JNI方法的指针,该指针只在创建它的线程有效,不能跨线程传递。 jclass是JNI的数据类型,对应Java的java.lang.Class实例。jobject同样也是JNI的数据类型,对应于Java的Object。

源文件 com_example_MainActivity.cpp

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

JNIEXPORT void JNICALL Java_com_example_MainActivity_init
(JNIEnv *, jclass){
//TODO 初始化
};

JNIEXPORT jsting JNICALL Java_com_example_MainActivity_stringFromJNI
(JNIEnv *, jobject){
return env->NewStringUTF("hello from JNI");
};

其中可以使用Java命令快速查看
javac xxx.java   用于生成java字节码文件
javah -classpath xxx.类名   用于生成C/C++头文件
javap -s xxx.类名   用于查看java方法名和对应的方法签名

动态注册

JNI中有一种结构用来记录Java的Native方法和JNI方法的关联关系,它就是JNINativeMethod,它在jni.h中被定义:

1
2
3
4
5
typedef struct {
const char* name; //Java方法的名字
const char* signature; //Java方法的签名信息
void* fnPtr; //JNI中对应的方法指针
} JNINativeMethod;

重写注册入口函数

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
static void native_init(JNIEnv* env,jclass clazz){
//TODO 初始化
};


static jstring native_stringFromJNI(JNIEnv* env,jobject obj){
return env->NewStringUTF("hello from JNI");
};

static JNINativeMethod method_table[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void *)native_stringFromJNI},
{"init", "()V", (void *)native_init}
};

static int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
clazz = (*env)->FindClass(env, className);
if (clazz == NULL)
{
return JNI_FALSE;
}

if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0)
{
return JNI_FALSE;
}

return JNI_TRUE;
}

static int registerNatives(JNIEnv* env)
{
if (!registerNativeMethods(env, "com/example/MainActivity", method_table, sizeof(method_table) / sizeof(method_table[0])))
{
return JNI_FALSE;
}

return JNI_TRUE;
}

类名路径不要用'.'而是'/' 。例如 com/example/MainActivity,否则虚拟机找不到会报错

重写JNI_OnLoad

so加载时会先调用JNI_OnLoad函数,重写该函数,在里面实现动态注册JNI方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv* env = NULL;
jint result = -1;

if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_4) != JNI_OK)
{
return -1;
}

if (!registerNatives(env)) //注册
{
return -1;
}

result = JNI_VERSION_1_4;

return result;
}

如果 java 方法是 static,则代表的是 class 对象

总结

静态注册
编写不方便,JNI 方法名字必须遵循规则且名字很长;
编写过程步骤多,不方便;
程序运行效率低,因为初次调用native函数时需要根据根据函数名在JNI层中搜索对应的本地函数,然后建立对应关系,这个过程比较耗时;

动态注册
流程更加清晰可控;
编写方便,可动态配置;
效率更高

JNI开发上手难度比较大一点,最好是学过C/C++,有一定基础的,否则你会遇到很多坑。
没有基础的同学先去学习下C/C++再来上手JNI开发。