(二)JNI基础,jni基础
(二)JNI基础,jni基础
一、什么是JNI?
JNI是 Java Native Interface 的缩写,表示 Java 本地接口。从 Java 1.1 开始,JNI 标准便成为了 Java 平台的一部分,它允许Java 代码和其他语言写的代码进行交互。
二、JNI基础
2.1 JNI的功能结构
JNI 最初是由 Sun 提供的Java 与本地系统中的原生方法交互的技术,用于在Windows/Linux 系统中实现 Java 与 Native Method (本地方法) 的相互调用。JVM(Java虚拟机)在封装各种操作系统实际的差异性的同时提供了JNI 技术,使得开发者可以通过Java程序(代码)调用到操作系统相关的技术实现的库函数,从而与其他技术和系统交互,使用其他技术实现的功能。同时,其他技术和系统也可以通过JNI提供的相应原生接口调用Java应用系统内部实现的功能。
2.2 JNI的调用层次
JNI的调用层次主要风味3层,在Android系统中这三层从上到下依次为Java→JNI→C/C++(.so库),Java可以访问到C/C++中的方法,同样C/C++也可以修改Java对象,三者间关系如下图:
由图可知,JNI的调用关系为 Java→JNI→Native。
2.3 分析JNI的本质
要想弄明白JNI的本质,还要从Java的本质说起。从本质上说,Java语言的运行完全依赖与脚本引擎对Java的代码进行解释和执行。因为现代的Java可以从源代码编译成 .class 之类的中间格式的二进制文件,所以这种处理会加快 Java 脚本的运行速度。尽管如此,基本的执行方式仍然不变,有脚本引擎(被称之为 JVM)来执行。与Python、perl之类的纯脚本相比,只是把脚本变成了二进制格而已。另外,Java本身就是一门面向对象的编程语言,可以调用完善的哦功能库。当把这个脚本引擎移植到所在的平台上之后,这个脚本就很自然的实现“跨平台”了。绝大多数的脚本引擎都支持一个很显著的特性,就是可以通过C/C++编写模块,并在脚本中调用这些模块。Java也是如此,Java 一定要提供一种在脚本中调用 C/C++编写的模块的机制,才能称得上是一个完善的脚本引擎。
从本质上来看,Android平台是由 arm-linux 操作系统和一个 Dalvik 虚拟机组成的。所有在 Android 模拟器上看到的界面效果都是用 Java 语言编写的,具体请看源代码中的 framenworks/base 目录。Dalvik 虚拟机只是提供了一个标准的支持 JNI 调用的 Java 虚拟机环境。
在 Android 平台中,使用 JNI 技术封装了所有和硬件相关的操作,通过 Java 去调用 JNI 模块,而 JNI 模块使用 C/C++ 调用 Android 系统本身的 arm-linux 底层驱动,这样便实现了对硬件的调用。
2.4 Java 和 JNI基本数据类型的转换
在Android 5.0中,Java 和 JNI 基本数据类型转换信息表如下所示。
Java | Native类型 | 本地C类型 | 字长 |
---|---|---|---|
boolean | jboolean | 无符号 | 8位 |
byte | jbyte | 无符号 | 8位 |
char | jchar | 无符号 | 16位 |
short | jshort | 有符号 | 16位 |
int | jint | 有符号 | 32位 |
long | jlong | 有符号 | 64位 |
float | jfloat | 有符号 | 32位 |
double | jdouble | 有符号 | 64位 |
数组类型的对应关系如下表所示。
字符 | Java类型 | C类型 |
---|---|---|
[I | jintArray | int[] |
[F | jfloatArray | float[] |
[B | jbyteArray | byte[] |
[C | jshortArray | short[] |
[D | jdoubleArray | double[] |
[J | jlongArray | long[] |
[S | jshortArray | short[] |
[Z | jbooleanArray | boolean[] |
对象数据类型的对应关系如下。
对象 | Java类型 | C类型 |
---|---|---|
LJava/lang/String | String | jstring |
LJava/net/Socket | Socket | jobject |
2.5 JNIEnv接口
在Android 5.0中,Native Method(本地方法)中的JNIEnv作为第一个参数被传入。JNIEnv的内部结构如下图所示。
当JNIEnv不作为参数传入时,JNI提供了如下两个函数来获得JNIEnv口。
(*jvm)->AttachCurrentThread(jvm,(void **)&env,NULL);
(*jvm)->GetEnv(jvm,(void**)&env,JNI_VERSION_1_2);
#上述两个函数都利用 Java VM 接口获得了 JNIEnv接口,并且 JNI 可以将获得的 JNIEnv 封装成一个函数。
Java通过JNI机制调用C/C++写的程序,C/C++开发的Native程序需要遵循一定的 JNI 规范。例如,下面就是一个 JNI 函数声明的例子。
JNIEXPORT jint JNICALL Java_jniTest_MyTest_test(JNIEnv * env,jobject obj,jint arg);
JVM 负责从 Java Stack 转入C/C++ Native Stack。当 Java 进入 JNI调用,除了函数本身的参数(arg)外,会多多出两个参数:JNIEnv 指针和 Jobject 指针。其中,JNIEnv 是 JVM 创造的,被 Native 的 C/C++ 方法用来操纵 Java 执行栈中的数据,例如 Java Class、Java Method 等。
三、开发JNI程序
开发JNI程序的一般步骤如下所示。
3.1 开发 JNI 程序
public class JNIInterface {
public static native int NativeInit();
public static native int NativeUninit();
public static native int Start(int type);
public static native int Stop();
private static native byte[] GetMessage(int type, int defaultLen);//注:defaultLen要足够大,要能放下可能的最长的命令
public static native int FreeData(byte[] data);
public static native int Test(int param);
public static native boolean SendDataToDev(int type, byte[] data, int len);
static {
System.loadLibrary("allong");
}
}
- 注册 JNI 函数
在现实应用中,Java 的 Native 函数与JNI 函数是一一对应关系。在Android 系统中,使用 JNINativeMethod 的结构体来记录这种对应关系。
在 Android 系统中,使用一种特定的方式来定义其 Native 函数,这与传统定义的 Java JNI 的方式有所差别。其中很重要的区别是在 Android 中使用了一种 Java 和 C 函数的映射表数组,并在其中描述了函数的参数和返回值。这个数组类型是 JNINativeMethod ,具体定义如下所示。
typedef struct {
const char* name; //Java 中函数的名字
const char* signature; // 描述了函数的参数和返回值
void* fnPtr; // 函数指针,指向C 函数
} JNINativeMethod;
示例如下:
JNINativeMethod methods[] = {
{"NativeInit", "()I", (void *) NativeInit},
{"NativeUninit", "()I", (void *) NativeUninit},
{"Start", "(I)I", (void *) Start},
{"Stop", "()I", (void *) Stop},
{"GetMessage", "(II)[B", (void *) GetMessage},
{"FreeData", "([B)I", (void *) FreeData},
{"SendDataToDev", "(I[BI)Z", (void *) SendDataToDev}
};
上述代码中,比较难以理解的是第二个参数,例如:
"()V"
"(II)V"
"(Ljava/lang/String;Ljava/lang/Striing;)V"
实际上这些字符是与函数的参数一一对应的,具体说明如下所示。
- ()中的字符表示参数,后面的则代表返回值。例如,"()V"就表示 void Func() ;
- (II)V 表示 void Func(int ,int)。
具体的每一个字符的对应关系如下表所示
字符 | Java 类型 | C类型 |
---|---|---|
V | void | void |
Z | jboolean | boolean |
I | jint | int |
J | jlong | long |
D | jdouble | double |
F | jfloat | float |
B | jbyte | byte |
C | jchar | char |
S | jshort | short |
而数组则以 [ 开始,用两个字符表示,例如 “[F”,“[B”等。
上面的都是基本数据类型,如果 Java 函数的参数是 class ,则以 L 开始,以 “;”结尾,中间部分使用 “/”隔离开的包名及类名。而其对应的 C 函数名的参数则为 jobject 。一个例外是 String 类,其对应的类为 jstring,即:
- Ljava/lang/String 中的 String jstring ;
- Ljava/net/Socket 中的Socket jobject 。
如果 Java 函数位于一个嵌入类,则使用“$”作为类名间的分隔符。例如:
(Ljava/lang/String$Landroid/os/FileUtils$FileStatus;)
实现注册工作
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
UnionJNIEnvToVOid uenv;
uenv.env = NULL;
jint result = -1;
JNIEnv *env = NULL;
LOGI("JNI_OnLoad");
if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_4) != JNI_OK) {
LOGE("Error:GetEnv failed");
return JNI_VERSION_1_4;
}
env = uenv.env;
if (registerNatives(env) != JNI_TRUE) { //注册函数
LOGE("ERROR: registerNatives failed");
goto bail;
}
result = JNI_VERSION_1_4;
bail:
return result;
}
Java 虚拟机在加载 JNI 函数时,首先会调用 JNI_OnLoad 方法,在其中实现注册逻辑。
const char * classPathName = "com/yj/example/natives/JNIInterface" //native 方法所在的类名
static int registerNatives(JNIEnv *env) {
if (!registerNativeMethods(env, classPathName, methods, sizeof(methods) / sizeof(methods[0]))) {
return JNI_FALSE;
}
return JNI_TRUE;
}
static int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *gMethods,
int numMehthods) {
jclass clazz = env->FindClass(className);
if (clazz == NULL) {
LOGE("Native register unable to find class '%s'", className);
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, numMehthods)) {
LOGE("RegisterNatives failed for '%s'", className);
return JNI_FALSE;
}
return JNI_TRUE;
}
- 编写 CMakeLists.txt ,可参考 向您的项目添加 C 和 C++ 代码 中 CMakeLists.txt 的编写规则。
# Sets the minimum version of CMake required to build your native library.
# This ensures that a certain set of CMake features is available to
# your build.
cmake_minimum_required(VERSION 3.4.1)
# Specifies a library name, specifies whether the library is STATIC or
# SHARED, and provides relative paths to the source code. You can
# define multiple libraries by adding multiple add.library() commands,
# and CMake builds them for you. When you build your app, Gradle
# automatically packages shared libraries with your APK.
file(GLOB native_src "src/main/cpp/*.cpp")
add_library( # Specifies the name of the library.
allong
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${native_src} )
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
#Specifies a path to native header files
target_link_libraries( # Specifies the target library.
allong
# Links the target library to the log library
# included in the NDK.
${log-lib} )
include_directories(src/main/cpp/include/)
- 使用 ndk-build 编译生成 .so 文件,将最后生成 .so 文件拷贝到 jniLibs 目录下。
- 编写 Java 调用代码,并测试自己写的程序。
static {
System.loadLibrary("allong");
}
参考内容
相关文章
- 暂无相关文章
用户点评