JAVA中的JNI技术简介

JNI是Java Native Interface的缩写,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植。 [1] 从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他编程语言,只要调用约定受支持就可以了。使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的。例如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少要保证本地代码能工作在任何Java 虚拟机环境。

一图了解JNI
JAVA中的JNI技术简介_第1张图片
      简单来说,JNI是JAVA程序和C/C++程序的中间件(或者说适配器),来实现两者间的相互调用。就我的应用场景而言,我在PC端用Caffe或Pytorch等深度学习学习框架训练完成一个图像分类的深度模型,将此模型部署至Android端的时候,由于我的代码使用Python/C++编写,就需要一种方法能够让自己的程序运行在Android平台,而JNI就能很好地解决这一问题。

NDK(Native Development Kit)

      Android NDK 是一组允许您将 C 或 C++(“原生代码”)嵌入到 Android 应用中的工具,NDK描述的是工具集。 能够在 Android 应用中使用原生代码对于想执行以下一项或多项操作的开发者特别有用:

  • 在平台之间移植其应用。
  • 重复使用现有库,或者提供其自己的库供重复使用。
  • 在某些情况下提高性能,特别是像游戏这种计算密集型应用。

JNI方法注册

静态注册
NI函数名格式(需将”.”改为”_”):

Java_ + 包名(com.example.auto.jnitest)+ 类名(MainActivity) + 函数名(stringFromJNI)

静态方法的缺点:

  • 要求JNI函数的名字必须遵循JNI规范的命名格式;
  • 名字冗长,容易出错;
  • 初次调用会根据函数名去搜索JNI中对应的函数,会影响执行效率;
  • 需要编译所有声明了native函数的Java类,每个所生成的class文件都要用javah工具生成一个头文件;

动态注册
      通过提供一个函数映射表,注册给JVM虚拟机,这样JVM就可以用函数映射表来调用相应的函数,就不必通过函数名来查找需要调用的函数。

  1. Java与JNI通过JNINativeMethod的结构来建立函数映射表,它在jni.h头文件中定义,其结构内容如下:
typedef struct {
    const char* name;
    const char* signature;
    void*       fnPtr;
} JNINativeMethod;
  • 创建映射表后,调用RegisterNatives函数将映射表注册给JVM;
  • 当Java层通过System.loadLibrary加载JNI库时,会在库中查JNI_OnLoad函数。可将JNI_OnLoad视为JNI库的入口函数,需要在这里完成所有函数映射和动态注册工作,及其他一些初始化工作。

数据类型转换

基础数据类型转换
JAVA中的JNI技术简介_第2张图片
引用数据类型转换
      除了Class、String、Throwable和基本数据类型的数组外,其余所有Java对象的数据类型在JNI中都用jobject表示。Java中的String也是引用类型,但是由于使用频率较高,所以在JNI中单独创建了一个jstring类型。
JAVA中的JNI技术简介_第3张图片

  • 引用类型不能直接在 Native 层使用,需要根据 JNI 函数进行类型的转化后,才能使用;
  • 多维数组(含二维数组)都是引用类型,需要使用 jobjectArray 类型存取其值;
    例如,二维整型数组就是指向一位数组的数组,其声明使用方式如下:
//获得一维数组的类引用,即jintArray类型  
    jclass intArrayClass = env->FindClass("[I");   
    //构造一个指向jintArray类一维数组的对象数组,该对象数组初始大小为length,类型为 jsize
    jobjectArray obejctIntArray  =  env->NewObjectArray(length ,intArrayClass , NULL);

JNI函数签名信息

      由于Java支持函数重载,因此仅仅根据函数名是没法找到对应的JNI函数。为了解决这个问题,JNI将参数类型和返回值类型作为函数的签名信息。

  1. JNI规范定义的函数签名信息格式:
(参数1类型字符…)返回值类型字符
  • 函数签名例子
    JAVA中的JNI技术简介_第4张图片
  • JNI常用的数据类型及对应字符
    JAVA中的JNI技术简介_第5张图片

JNIEnv介绍

  • JNIEnv概念 :
          JNIEnv是一个线程相关的结构体, 该结构体代表了 Java 在本线程的运行环境。通过JNIEnv可以调用到一系列JNI系统函数。

  • JNIEnv线程相关性:
          每个线程中都有一个 JNIEnv 指针。JNIEnv只在其所在线程有效, 它不能在线程之间进行传递。
    注意:在C++创建的子线程中获取JNIEnv,要通过调用JavaVM的AttachCurrentThread函数获得。在子线程退出时,要调用JavaVM的DetachCurrentThread函数来释放对应的资源,否则会出错。

  • JNIEnv 作用:

  • 访问Java成员变量和成员方法;

  • 调用Java构造方法创建Java对象等。

JNI编译
ndkBuild
使用ndk-build编译生成.so

Cmake编译

CMake 则是一个跨平台的编译工具,它并不会直接编译出对象,而是根据自定义的语言规则(CMakeLists.txt)生成 对应 makefile 或 project 文件,然后再调用底层的编译, 在Android Studio 2.2 之后支持Cmake编译。

  • add_library 指令
 语法:add_library(libname [SHARED | STATIC | MODULE] [EXCLUDE_FROM_ALL] [source])

      将一组源文件 source 编译出一个库文件,并保存为 libname.so (lib 前缀是生成文件时 CMake自动添加上去的)。其中有三种库文件类型,不写的话,默认为 STATIC;

  • SHARED: 表示动态库,可以在(Java)代码中使用 System.loadLibrary(name) 动态调用;
  • STATIC: 表示静态库,集成到代码中会在编译时调用;
  • MODULE: 只有在使用 dyId 的系统有效,如果不支持 dyId,则被当作 SHARED 对待;
  • EXCLUDE_FROM_ALL: 表示这个库不被默认构建,除非其他组件依赖或手工构建;
#将compress.c 编译成 libcompress.so 的共享库
add_library(compress SHARED compress.c)
  • target_link_libraries 指令
语法:target_link_libraries(target library <debug | optimized> library2…)

      这个指令可以用来为 target 添加需要的链接的共享库,同样也可以用于为自己编写的共享库添加共享库链接。如:

#指定 compress 工程需要用到 libjpeg 库和 log 库
target_link_libraries(compress libjpeg ${log-lib})
  • find_library 指令
语法:find_library(<VAR> name1 path1 path2 ...)

      VAR 变量表示找到的库全路径,包含库文件名 。例如:

find_library(libX  X11 /usr/lib)
find_library(log-lib log)  #路径为空,应该是查找系统环境变量路径

Abi架构

ABI(Application binary interface)应用程序二进制接口。不同的CPU 与指令集的每种组合都有定义的 ABI (应用程序二进制接口),一段程序只有遵循这个接口规范才能在该 CPU 上运行,所以同样的程序代码为了兼容多个不同的CPU,需要为不同的 ABI 构建不同的库文件。当然对于CPU来说,不同的架构并不意味着一定互不兼容。

  • armeabi设备只兼容armeabi;
  • armeabi-v7a设备兼容armeabi-v7a、armeabi;
  • arm64-v8a设备兼容arm64-v8a、armeabi-v7a、armeabi;
  • X86设备兼容X86、armeabi;
  • X86_64设备兼容X86_64、X86、armeabi;
  • mips64设备兼容mips64、mips;
  • mips只兼容mips;
    根据以上的兼容总结,我们还可以得到一些规律:
  • armeabi的SO文件基本上可以说是万金油,它能运行在除了mips和mips64的设备上,但在非armeabi设备上运行性能还是有所损耗;
  • 64位的CPU架构总能向下兼容其对应的32位指令集,如:x86_64兼容X86,arm64-v8a兼容
  • armeabi-v7a,mips64兼容mips;

你可能感兴趣的:(Android)