Android NDK 8 JNI基础

概述

官方文档:Java Native Interface 6.0 Specification;

Java Native Interface (JNI) 标准是 Java 平台的一部分,它允许 Java 代码和其他语言写的代码进行交互。JNI 是本地编程接口,它使得在 Java 虚拟机 (VM) 内部运行的 Java 代码能够与用其它编程语言(如 C、C++ 和汇编语言)编写的应用程序和库进行交互操作。

JNI 作用

  • 扩展:JNI 扩展了 JVM 能力,驱动开发,例如开发一个 wifi 驱动,可以将手机设置为无限路由;
  • 高效:本地代码效率高,游戏渲染,音频视频处理等方面使用 JNI 调用本地代码,C语言可以灵活操作内存;
  • 复用:在文件压缩算法 7zip 开源代码库,机器视觉 OpenCV 开放算法库等方面可以复用 C 平台上的代码,不必在开发一套完整的 Java 体系,
    避免重复发明轮子;
  • 特殊:产品的核心技术一般也采用 JNI 开发,不易破解。

一、基本流程

下面通过一个示例来了解 java 使用 jni 的基本流程。

环境如下:

Linux 4.13.0-16-generic;
gcc version 7.2.0;
openjdk version “1.8.0_151″;
javac 1.8.0_151;

java 集成开发工具:IDEA 2017。

1.1、创建 native 方法

项目结构如下:

《Android NDK 8 JNI基础》 JNI示例项目结构.png

首先在 java 中声明 native 方法,示例代码如下:

private static native void helloJni();

1.2、生成头文件

我使用的 java IDE 是 IDEA,只要在 Terminal 中输入以下指令就可以在对应的目录下生成相应的头文件,

javah -jni -classpath out/production/Jni_01 -d ./jni com.seraphzxz.Main

以上指令中 out/production/Jni_01 为目标文件所在目录,./jni 为输出目录,com.seraphzxz.Main 为目标文件,也就是声明了 native 方法的 java 类。生成的头文件名称为 com_seraphzxz_Main.h,代码如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_seraphzxz_Main */

#ifndef _Included_com_seraphzxz_Main
#define _Included_com_seraphzxz_Main
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_seraphzxz_Main
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_seraphzxz_Main_sayHello
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

1.3、实现头文件

创建实现头文件的实现类 Main.c,代码如下:

#include <stdio.h>
#include "com_seraphzxz_Main.h"

JNIEXPORT void JNICALL Java_com_seraphzxz_Main_helloJni(JNIEnv *env, jobject thisObj) {
   printf("Hello JNI.\n");
   return;
}

实现方法很简单就是打印 Hello JNI.。

1.5、生成动态链接库

这里要注意的是,在编译 so 库文件时,需要把头文件中的 #include <jni.h> 改为 #include “jni.h”,我这里把 jni.h 和 jni_md.h 都添加到了 jni 目录下,去掉了 Main.c 中的 #include <jni.h>。执行以下指令生成 .so 库:

gcc -shared -fpic -o libmain.so ./jni/Main.c

也可先编译为可重定位目标程序,也就是 .o 文件,指令如下:

gcc -c jni/Main.c

接着在转化为 .so,指令如下:

gcc -shared -o libmain.so Main.o

注意这里生成的 .so 库的命名方式 —— 添加 lib 前缀(约定)。

1.6、配置环境

因为系统的 JVM 的 java.library.path 属性即为环境变量 Path 指定的目录,但是 .so 并未放入到 Path 指定的任何一个目录中,因此需
要告诉 JVM,.so 文件所在的目录。在 IDEA 中点击 Run > Edit Configurations 并配置 MV Option:

《Android NDK 8 JNI基础》 VM_Option.png

-Djava.library.path=/.so所在目录

其中 -Djava.library.path 为固定写法,等号右面的就是 .so 库所在的目录。

不然会报以下错误:

Exception in thread "main" java.lang.UnsatisfiedLinkError: no main in java.library.path
  at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
  at java.lang.Runtime.loadLibrary0(Runtime.java:870)
  at java.lang.System.loadLibrary(System.java:1122)
  at com.seraphzxz.Main.<clinit>(Main.java:7)

1.7、加载共享库

java 中加载 .so 共享库的代码如下:

static {
    System.loadLibrary("main");
}

注意这里加载共享库的方法 loadLibrary() 中输入的参数为 “main”,而我们生成的共享库名称为 libmain.so,这是个约定,要注意一下,不然会报以下错误:

Exception in thread "main" java.lang.UnsatisfiedLinkError: no main in java.library.path
    at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
    at java.lang.Runtime.loadLibrary0(Runtime.java:870)
    at java.lang.System.loadLibrary(System.java:1122)
    at com.seraphzxz.Main.<clinit>(Main.java:7)

当然了,这只是报该错误的原因之一。

1.8、调用 native 方法

完整的 java 代码如下:

public class Main {

    static {
        System.loadLibrary("main");
    }

    private static native void helloJni();

    public static void main(String[] args) {

        helloJni();
    }
}

执行结果:

Hello JNI.

到这里一个完整的 JNI 调用流程就走通了,下面就来分析 Java 的 native 方法是如何与 C/C++ 中的函数链接的。

二、JNI 的注册方式

2.1、静态注册

原理:根据函数名建立 Java 方法和 JNI 函数的一一对应关系。流程如下:

  1. 先编写 Java 的 native 方法;
  2. 用 javah 工具生成对应的头文件;
  3. 实现 JNI 里面的函数,在 Java 中通过 System.loadLibrary 加载 so 库。

静态注册的方式有两个重要的关键词:JNIEXPORT 和 JNICALL,这两个关键词是宏定义,主要是注明该函数是 JNI 函数,当虚拟机加载 so 库时,如果发现函数含有这两个宏定义时,就会链接到对应的 Java 层的 native 方法。

这里顺便说一下 JNI 函数命名规则,一个本地方法的函数名分为如下几个部分:

  1. Java_ 前缀;
  2. 以“_” 为分隔符的类名全称;
  3. “_”分隔符;
  4. 方法名;

对于重载方法(overload),后面还要跟两个下划线及参数签名(因为 Java 的方法签名除了方法名,还有参数,避免冲突所以重载方法需要加上后缀避免冲突)。

对于一些特殊字符,使用转义字符来代替,例如作为分隔符的下划线如果在方法名中,则会被替换成 _1,具体替换看下表:

转义字符含义
_0XXXXUnicode 字符
_1下划线 _
_2分号 ;
_3中括号 [

使用静态连接的优点:

  • 实现比较简单,可以通过 javah 工具将 Java代码的 native 方法直接转化为对应的 native 层代码的函数;

缺点:

  • javah 生成的 native 层函数名较长,可读性很差;
  • 后期修改文件名、类名或函数名时,头文件的函数将失效,需要重新生成或手动改;
  • 程序运行效率低,首次调用 native 函数时,需要根据函数名在 JNI 层搜索对应的本地函数,建立对应关系,比较耗时。

2.2、动态注册

原理:直接告诉 native 方法其在 JNI 中对应函数的指针。通过使用 JNINativeMethod 结构来保存 Java native 方法和 JNI 函数关
联关系,步骤如下:

  1. 编写 Java 的 native 方法;
  2. 编写 JNI 函数的实现(函数名可以随便命名);
  3. 利用结构体 JNINativeMethod 保存 Java native 方法和 JNI 函数的对应关系;
  4. 利用 registerNatives(JNIEnv* env) 注册类的所有本地方法;
  5. 在 JNI_OnLoad 方法中调用注册方法;
  6. 在 Java 中通过 System.loadLibrary 加载完 JNI 动态库之后,会调用 JNI_OnLoad 函数,完成动态注册。

通过下面的代码示例来分析 JNI 的动态注册方式。

直接看实现类:

#include "jni.h"
#include <stdio.h>
#include <stdlib.h>

using namespace std;

#ifdef __cplusplus
extern "C" {
#endif

static const char *className = "com/seraphzxz/Main";

static void helloJni(JNIEnv *env, jobject, jlong handle) {
    printf("Hello JNI.");
}

static JNINativeMethod gJni_Methods_table[] = {
    {"helloJni", "()V", (void*)helloJNi},
};

static int jniRegisterNativeMethods(JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz;

    clazz = (env)->FindClass( className);
    if (clazz == NULL) {
        return -1;
    }

    int result = 0;
    if ((env)->RegisterNatives(clazz, gJni_Methods_table, numMethods) < 0) {
        result = -1;
    }

    (env)->DeleteLocalRef(clazz);
    return result;
}

// 重点看该函数
jint JNI_OnLoad(JavaVM* vm, void* reserved){

    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        return result;
    }

    jniRegisterNativeMethods(env, className, gJni_Methods_table, sizeof(gJni_Methods_table) / sizeof(JNINativeMethod));

    return JNI_VERSION_1_4;
}

#ifdef __cplusplus
}
#endif

在实际的应用中,可以将静态注册和动态注册结合起来:在 java 代码中仍然声明一个 native 函数,但是这个函数仅仅是用来去触发在 JNI 层的 native 函数的动态注册。看下面示例代码:

java 层:

static {
   System.loadLibrary("jni");
    registerNatives();
}

private static native void registerNatives();

JNI 层:

通过 javah 生成 java 层声明的 native 函数的文件,并且在实现代码中去动态注册 JNI 函数:

JNIEXPORT void JNICALL Java_com_seraphzxzi_NativeRgister_registerNatives
(JNIEnv *env, jclass clazz){
     (env)->RegisterNatives(clazz, gJni_Methods_table, sizeof(gJni_Methods_table) / sizeof(JNINativeMethod));
}
    原文作者:seraphzxz
    原文地址: https://www.jianshu.com/p/db4c39b0a53f
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞