美文网首页Android收藏集
Android NDK 8 JNI基础

Android NDK 8 JNI基础

作者: seraphzxz | 来源:发表于2018-07-17 09:39 被阅读13次

    概述

    官方文档: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 方法

    项目结构如下:

    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:

    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,具体替换看下表:

    转义字符 含义
    _0XXXX Unicode 字符
    _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));
    }
    

    相关文章

      网友评论

        本文标题:Android NDK 8 JNI基础

        本文链接:https://www.haomeiwen.com/subject/glcwpftx.html