Skip to content
jni-use_cover

JNI技术使用篇

JNI常见在NDK开发中,Android应用开发过程中只需要通过调用Java本地方法实现一些特殊的功能。常将一些复杂的逻辑或者算法封装成库,在库上封装一层JNI层,供Java层面本地方法调用,这样的目的要么为了逻辑或算法的保密性,要么因为算法的效率或者实现编程语言的局限性。我们常在一些框架中见到本地方法的身影:

  • Netty中关于通过本地方法实现不同操作系统的IO多路复用。
  • Rocketmq关于使用zstd-jni实现zstd压缩算法对消息压缩提高发送性能。
  • JDK中大量使用本地方法,例如java.lang.ObjectregisterNatives()方法。
  • ……

记得在一家模式识别的公司实习的时候,主要的内容就是在C++层封装的识别动态库(人脸识别、车辆识别等)上封装JNI层,供Java本地方法调用。当然Java调用C++代码的方式不止JNI方式一种,这又涉及一个话题跨语言调用,通过JNI的方式,我想的因素有很多,比如有的盈利模式就是通过卖SDKLicense,通过JNI方式提供Java层面的SDK,可以方便交付。

本文将通过以下几个部分阐述:

JNI开发过程

正如下图所见:

JNI开发过程

JNI开发的过程大致分为以下几步:

  • 编写Java代码,根据所依赖的库编写Java层面的本地方法(包含方法名,方法传参和返回参数的数据结构)。
  • 通过javacjavah生成对应的JNI层的头文件.h
  • 依赖一些库,编写JNI层代码。
  • JNI层代码编译成动态库.so.dll.dylib
  • 测试Java本地调用。

一个栗子:猫咪声解析器

本栗子全部代码见sdefaa-code-snippetjni-snippet部分。

有时候真的想知道德发喵叫声表达的什么意思,如果有一个猫咪声解析器该有多好。

假设现在有一个meowParser.lib静态库,提供了猫咪声音解析的功能。在它的头文件meowParser.h显示该静态库包含了三个函数:

c++
// 初始化
void init();
// 解析
char *parse(const char *);
// 销毁
void destroy();

在此基础上封装一个JNI层,JNI调用meowParser.lib静态库提供的功能,将原本C++实现的猫咪声解析功能可以通过Java代码调用。

定义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();

}

猫咪声内容实体类:

java
public class ParserContent {
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

猫咪声解析结果实体类:

java
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 + '\'' +
                '}';
    }
}

猫咪声解析异常类:

java
public class ParserException extends Exception{

    public ParserException() {
    }

    public ParserException(String message) {
        super(message);
    }
}

编译生成头文件

执行命令:

shell
 javac -h . ParserContent.java ParserResult.java ParserException.java MeowParser.java

在当前文件夹下生成头文件:com_sdefaa_jni_MeowParser.h,内容如下:

c++
/* 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,引入相关的头文件完成功能逻辑编写。

java
#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
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本地代码测试

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本地代码。我理解的JNIJNA的区别有:

  • JNI方式更底层写,需要写大量的JNI本地代码,对CC++有编程要求,而JNA方式只需要关注Java代码。
  • JNI方式比JNA功能更为强大,例如创建类实例,链接虚拟机等等,通过JNI方式访问动态库可以在本地代码创建Java层面的实体类对象,而JNA对调用、数据类型做了映射,Java实体对象到本地类型转换需要额外的耗时,推荐使用基础的数据类型,从这点上看,JNI性能较好。
  • 从以上两点看,JNI需要的编程素质要求更高,尤其在处理数据类型的转换,签名的属性,内存管理要格外的注意。JNA是通过Java代码直接访问动态库,使用更为简单。

根据上面的描述,在JNIJNA选择可以考虑:

  • 要求性能,更加灵活操作,例如创建Java类,调用方法等,同时具备C/C++的编程素质选择JNI方式。
  • 仅仅访问动态库的功能,动态库没有复杂的数据结构带来的转换性能问题或者不考虑性能问题,不想增加C/C++方面的技术成本等选择JNA方式。

我的选择总是JNI。😂

常见的问题

  • 无参构造方法的方法名是<init>,方法签名是()V

  • 签名不清楚的情况下,可以通过javap -s命令获取。例如:

    shell
    javap -s ParserException.class

    输出结果:

    java
    Compiled 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开发完全使册