(二)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程序的一般步骤如下所示。

  1. 编写Java中的调用类
  2. 用 javah 生成C/C++ 原生函数的头文件
  3. 在 C/C++ 中调用需要的其他函数功能实现原生函数,原则上可以调用任何资源
  4. 将项目依赖生成的所有原生库和资源加入到 Java 项目
  5. 生成 Java程序

3.1 开发 JNI 程序

  1. 使用 Android Studio创建工程,在项目目录下创建 cpp 文件夹用来存放 C/C++ 文件
    在这里插入图片描述
  2. 创建 C/C++ 源文件,并将原文件关联到项目,根据不同的构建工具选择构建文件,如果采用的是ndk-build,则需要编写Android.mk 文件,并将它指向项目,如果采用的是 CMake 工具,则需要编写 CMakeLists.txt 文件,并将它指向项目。具体详细操作可参考:向您的项目添加 C 和 C++ 代码一文。
  3. 编写 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");
    }
}
  1. 注册 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;
}
  1. 编写 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/)
  1. 使用 ndk-build 编译生成 .so 文件,将最后生成 .so 文件拷贝到 jniLibs 目录下。
    在这里插入图片描述
  2. 编写 Java 调用代码,并测试自己写的程序。
   static {
        System.loadLibrary("allong");
    }

参考内容

  1. 向您的项目添加 C 和 C++ 代码
  2. 《深入理解 Android 系统》 张元亮 编著 清华大学出版社
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章