JNI技术使用篇
JNI常见在NDK开发中,Android应用开发过程中只需要通过调用Java本地方法实现一些特殊的功能。常将一些复杂的逻辑或者算法封装成库,在库上封装一层JNI层,供Java层面本地方法调用,这样的目的要么为了逻辑或算法的保密性,要么因为算法的效率或者实现编程语言的局限性。我们常在一些框架中见到本地方法的身影:
Netty
中关于通过本地方法实现不同操作系统的IO多路复用。Rocketmq
关于使用zstd-jni
实现zstd
压缩算法对消息压缩提高发送性能。JDK
中大量使用本地方法,例如java.lang.Object
中registerNatives()
方法。- ……
记得在一家模式识别的公司实习的时候,主要的内容就是在C++
层封装的识别动态库(人脸识别、车辆识别等)上封装JNI
层,供Java本地方法调用。当然Java调用C++
代码的方式不止JNI
方式一种,这又涉及一个话题跨语言调用,通过JNI
的方式,我想的因素有很多,比如有的盈利模式就是通过卖SDK
的License
,通过JNI
方式提供Java
层面的SDK
,可以方便交付。
本文将通过以下几个部分阐述:
JNI开发过程
正如下图所见:
JNI
开发的过程大致分为以下几步:
- 编写
Java
代码,根据所依赖的库编写Java
层面的本地方法(包含方法名,方法传参和返回参数的数据结构)。 - 通过
javac
或javah
生成对应的JNI
层的头文件.h
。 - 依赖一些库,编写
JNI
层代码。 - 将
JNI
层代码编译成动态库.so
,.dll
或.dylib
。 - 测试Java本地调用。
一个栗子:猫咪声解析器
本栗子全部代码见sdefaa-code-snippet
的jni-snippet
部分。
有时候真的想知道德发喵叫声表达的什么意思,如果有一个猫咪声解析器该有多好。
假设现在有一个meowParser.lib
静态库,提供了猫咪声音解析的功能。在它的头文件meowParser.h
显示该静态库包含了三个函数:
// 初始化
void init();
// 解析
char *parse(const char *);
// 销毁
void destroy();
在此基础上封装一个JNI
层,JNI
调用meowParser.lib
静态库提供的功能,将原本C++
实现的猫咪声解析功能可以通过Java
代码调用。
定义Java本地方法
猫咪声解析类:
public class MeowParser {
static {
System.load(MeowParser.class.getClassLoader().getResource("meowParserJNI.dll").getPath());
}
/**
* 解析器初始化方法
*/
public static native void init();
/**
* 解析方法
*
* @param parserContent 解析内容实体
* @return 解析结果实体
*/
public static native ParserResult parse(ParserContent parserContent) throws ParserException;
/**
* 解析器销毁方法
*/
public static native void destroy();
}
猫咪声内容实体类:
public class ParserContent {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
猫咪声解析结果实体类:
public class ParserResult {
private String meaning;
public String getMeaning() {
return meaning;
}
public void setMeaning(String meaning) {
this.meaning = meaning;
}
@Override
public String toString() {
return "ParserResult{" +
"meaning='" + meaning + '\'' +
'}';
}
}
猫咪声解析异常类:
public class ParserException extends Exception{
public ParserException() {
}
public ParserException(String message) {
super(message);
}
}
编译生成头文件
执行命令:
javac -h . ParserContent.java ParserResult.java ParserException.java MeowParser.java
在当前文件夹下生成头文件:com_sdefaa_jni_MeowParser.h
,内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_sdefaa_jni_MeowParser */
#ifndef _Included_com_sdefaa_jni_MeowParser
#define _Included_com_sdefaa_jni_MeowParser
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_sdefaa_jni_MeowParser
* Method: init
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_sdefaa_jni_MeowParser_init
(JNIEnv *, jclass);
/*
* Class: com_sdefaa_jni_MeowParser
* Method: parse
* Signature: (Lcom/sdefaa/jni/ParserContent;)Lcom/sdefaa/jni/ParserResult;
*/
JNIEXPORT jobject JNICALL Java_com_sdefaa_jni_MeowParser_parse
(JNIEnv *, jclass, jobject);
/*
* Class: com_sdefaa_jni_MeowParser
* Method: destroy
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_sdefaa_jni_MeowParser_destroy
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
编写本地代码
新建meowParserJNI.cpp
,引入相关的头文件完成功能逻辑编写。
#include "com_sdefaa_jni_MeowParser.h"
#include "meowParser.h"
#include <iostream>
void throwException(JNIEnv *env, const char *message)
{
jclass jclazz_NullPointerException = env->FindClass("com/sdefaa/jni/ParserException");
env->ThrowNew(jclazz_NullPointerException, message);
}
/*
* Class: com_sdefaa_jni_MeowParser
* Method: init
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_sdefaa_jni_MeowParser_init(JNIEnv *env, jclass clazz)
{
init();
}
/*
* Class: com_sdefaa_jni_MeowParser
* Method: parser
* Signature: (Lcom/sdefaa/jni/ParserContent;)Lcom/sdefaa/jni/ParserResult;
*/
JNIEXPORT jobject JNICALL Java_com_sdefaa_jni_MeowParser_parse(JNIEnv *env, jclass clazz, jobject parserContent)
{
if (parserContent == NULL)
{
throwException(env, "Arguments#ParserContent is null");
return NULL;
}
jclass jclazz_ParserConent = env->GetObjectClass(parserContent);
jfieldID jfId_content = env->GetFieldID(jclazz_ParserConent, "content", "Ljava/lang/String;");
jstring jstring_content = (jstring)env->GetObjectField(parserContent, jfId_content);
if (jstring_content == NULL)
{
throwException(env, "ParserContent#content is null");
return NULL;
}
const char *contentPtr = env->GetStringUTFChars(jstring_content, JNI_FALSE);
char *meaning = parse(contentPtr);
jclass jclazz_ParserResult = env->FindClass("com/sdefaa/jni/ParserResult");
jmethodID jmId_constrctor = env->GetMethodID(jclazz_ParserResult, "<init>", "()V");
jobject jobject_ParserResult = env->NewObject(jclazz_ParserResult, jmId_constrctor);
jfieldID jfId_meaning = env->GetFieldID(jclazz_ParserResult, "meaning", "Ljava/lang/String;");
jstring jstring_meaning = env->NewStringUTF(meaning);
env->SetObjectField(jobject_ParserResult, jfId_meaning, jstring_meaning);
return jobject_ParserResult;
}
/*
* Class: com_sdefaa_jni_MeowParser
* Method: destroy
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_sdefaa_jni_MeowParser_destroy(JNIEnv *env, jclass clazz)
{
destroy();
}
编译生成动态库
推荐使用cmake
编译动态库的方式,当然也可以使用gcc
编译。注意链接的头文件位置和库位置。
cmake_minimum_required(VERSION 3.0.0)
project(meowParserJNI VERSION 0.1.0)
include(CTest)
enable_testing()
add_library(meowParserJNI SHARED meowParserJNI.cpp)
include_directories("E:/Program Files/Java/jdk1.8.0_351/include" "E:/Program Files/Java/jdk1.8.0_351/include/win32")
target_link_libraries(meowParserJNI "E:/sdefaa-code-snippet/jni-snippet/cpp/meowParserJNI/meowParser.lib")
set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)
生成动态库:meowParserJNI.dll
。
Java本地代码测试
public class MeowParserTest {
/**
* 初始化
*/
@Test
public void testInit(){
MeowParser.init();
}
/**
* 销毁
*/
@Test
public void testDestroy(){
MeowParser.destroy();
}
/**
* 解析
* @throws ParserException 解析异常
*/
@Test
public void testParse() throws ParserException {
ParserContent parserContent = new ParserContent();
parserContent.setContent(MeowType.MEOW.getContent());
System.out.println( MeowParser.parse(parserContent).toString());
parserContent.setContent(MeowType.SNORE.getContent());
System.out.println( MeowParser.parse(parserContent).toString());
parserContent.setContent(MeowType.CHATTER.getContent());
System.out.println( MeowParser.parse(parserContent).toString());
parserContent.setContent(MeowType.HOWL.getContent());
System.out.println( MeowParser.parse(parserContent).toString());
parserContent.setContent(MeowType.ROAR.getContent());
System.out.println( MeowParser.parse(parserContent).toString());
parserContent.setContent(MeowType.HISS.getContent());
System.out.println( MeowParser.parse(parserContent).toString());
parserContent.setContent(MeowType.NULL.getContent());
System.out.println( MeowParser.parse(parserContent).toString());
}
/**
* 参数parserContent为null抛出ParserException异常
*/
@Test
public void testParseException1(){
Assertions.assertThrowsExactly(ParserException.class, () -> {
ParserResult parserResult = MeowParser.parse(null);
});
}
/**
* ParserContent#content为null抛出ParserException异常
*/
@Test
public void testParseException2(){
Assertions.assertThrowsExactly(ParserException.class, () -> {
ParserContent parserContent = new ParserContent();
ParserResult parserResult = MeowParser.parse(parserContent);
});
}
}
测试案例全部通过~😋
JNI与JNA
JNA是另一种Java访问本地动态库的方式,更为直接方便,仅通过Java
代码,无需编写任何JNI
本地代码。我理解的JNI
与JNA
的区别有:
JNI
方式更底层写,需要写大量的JNI
本地代码,对C
或C++
有编程要求,而JNA
方式只需要关注Java
代码。JNI
方式比JNA
功能更为强大,例如创建类实例,链接虚拟机等等,通过JNI
方式访问动态库可以在本地代码创建Java层面的实体类对象,而JNA
对调用、数据类型做了映射,Java实体对象到本地类型转换需要额外的耗时,推荐使用基础的数据类型,从这点上看,JNI
性能较好。- 从以上两点看,
JNI
需要的编程素质要求更高,尤其在处理数据类型的转换,签名的属性,内存管理要格外的注意。JNA
是通过Java
代码直接访问动态库,使用更为简单。
根据上面的描述,在JNI
和JNA
选择可以考虑:
- 要求性能,更加灵活操作,例如创建Java类,调用方法等,同时具备C/C++的编程素质选择
JNI
方式。 - 仅仅访问动态库的功能,动态库没有复杂的数据结构带来的转换性能问题或者不考虑性能问题,不想增加C/C++方面的技术成本等选择
JNA
方式。
我的选择总是JNI
。😂
常见的问题
无参构造方法的方法名是
<init>
,方法签名是()V
。签名不清楚的情况下,可以通过
javap -s
命令获取。例如:shelljavap -s ParserException.class
输出结果:
javaCompiled from "ParserException.java" public class com.sdefaa.jni.ParserException extends java.lang.Exception { public com.sdefaa.jni.ParserException(); descriptor: ()V public com.sdefaa.jni.ParserException(java.lang.String); descriptor: (Ljava/lang/String;)V }
内部类的签名是外部类签名和内部类名以$拼接。非静态内部类的默认构造方法不是无参的,包含外部类参数,具体的签名可以通过
javap -s
获得。其他
API
使用问题可以见JNI
开发完全使册。