JNI和NDK编程(一)JNI的开发流程

ndk开发基础学习笔记系列:

JNI和NDK编程(一)JNI的开发流程
JNI和NDK编程(二)NDK的开发流程
JNI和NDK编程(三)JNI的数据类型和类型签名
JNI和NDK编程(四)JNI调用Java方法的流程

前言

Java JNI本意为Java Native Interface(java本地接口), 它是为方便java调用C、C++等本地代码所封装的一层接口. 我们都知道,java的有点是跨平台, 但是作为优点的同时, 其在和本地交互的时候就出现了短板. Java的跨平台特性导致其本地交互的能力不够强大, 一些和操作系统相关的特性Java无法完成, 于是Java提供了JNI专门用于和本地代码交互, 这样就增强了Java语言的本地交互能力. 通过Java JNI, 用户可以调用C、C++所编写的本地代码.

NDK是android所提供的一个工具集合, 通过NDK可以在Android中更加方便地通过JNI来访问本地代码, 比如C或者C++. NDK还提供了交叉编译器, 开发人员只需要简单的修改mk文件就可以生成特定的CPU平台的动态库. 使用NDK有好处如下:

  1. 提高代码的安全性. 由于so库反编译比较困难, 因此NDK提高了Android程序的安全性.
  2. 可以很方便地使用目前已有的C/C++开源库.
  3. 便于平台间的移植. 通过C/C++实现的动态库可以很方便地在其他平台上使用.
  4. 提高程序在某些特定情形下得执行效率, 但是不能明显提升Android程序的的性能.

在Linux环境中, JNI和NDK开发所用到的动态库的格式是以.so为后缀的文件, 下面统称为so库. 另外, 由于JNI和NDK主要用于底层和嵌入式开发, 在Android的应用层开发中使用较少, 加上它们本身更加侧重于C和C++方面的编程, 本篇只介绍JNI和NDK的基础知识, 其他更加深入的知识点可以查看专门介绍JNI和NDk的书籍.

JNI的开发流程

JNI开发流程有如下几步, 首先需要在java中声明native方法, 接着用c/c++实现native的方法, 然后就可以编译运行了.

1. 在Java中声明native方法

创建一个类, 这里叫做JniTest.java, 代码如下所示.

public class JniTest {

    static {
        System.loadLibrary("jni-test");
    }

    public static void main(String arg[]) {
        JniTest jniTest = new JniTest();
        System.out.println(jniTest.get());
        jniTest.set("hello world");
    }
    
    public native String get();
    public native void set(String str);
}

可以看到上面的代码中, 生命了两个native方法: get和set(String), 这来两个就是需要在JNI中实现的方法. 在JniTest的头部有一个加载动态库的过程, 其中jni-test是so库的标识, so 库完整的名称为 libjni-test.so , 这是加载so库的规范.

2. 编译Java源文件得到class文件, 然后通过javah命令导出JNI头文件

javac com/gavinandre/jnitest/JniTest.java
javah com.gavinandre.jnitest.JniTest

在当前目录下, 会产生一个com_gavinandre_jnitest_JniTest.h头文件, 它是javah命令自动生成的,内容如下所示.

/* DO NOT EDIT THIS FILE - it is machine generated */
#include 
/* Header for class com_gavinandre_jnitest_JniTest */
#ifndef _Included_com_gavinandre_jnitest_JniTest
#define _Included_com_gavinandre_jnitest_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_gavinandre_jnitest_JniTest
 * Method:    get
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_gavinandre_jnitest_JniTest_get
  (JNIEnv *, jobject);
/*
 * Class:     com_gavinandre_jnitest_JniTest
 * Method:    set
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_com_gavinandre_jnitest_JniTest_set
  (JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif

上面的代码需要做一下说明, 首先函数名的格式遵循如下规则: Java_包名_类名_方法名. 比如JniTest中的set方法, 到这里就变成了JNIEXPORT void JNICALL Java_com_gavinandre_jnitest_JniTest_set(JNIEnv *, jobject, jstring), 其中com_gavinandre_jnitest是包名, JniTest是类名, **jstring 代表的是set方法的String类型参数: set(String str) **. 关于Java和JNI的数据类型之间的对应关系会在下文 JNI的数据类型和类型签名 中介绍, 这里只需要知道Java的String对应与JNI的jstring即可. JNIEXPORT、JNICALL、JNIEnv和jobject都是JNI标准中所定义的类型或者宏,它们的含义如下:

  • *JNIEnv : 表示一个指向JNI环境的指针, 可以通过它来访问JNI提供的接口方法.
  • jobject: 表示java对象中的this.
  • JNIEXPORT和JNICALL: 它们是JNI种所定义的宏, 可以在jni.h这个头文件中查到

下面这个宏定义是必须的, 作用是指定extern "C"内部的函数采用C语言的命名风格来编译. 否则当JNI采用C++来实现时, 由于C/C++编译过程对函数的命名风格不同, 这将导致JNI在链接时无法根据函数名找到具体的函数, 那么JNI调用就无法完成. 更多的细节实际上是有关C和C++编译时的一些问题, 这里就不在展开了.

#ifdef __cplusplus
extern "C" {
#endif

3. 实现JNI方法

JNI方法是指的Java中声明的native方法, 这里可以选择c++和c来实现. 过程都是类似的. 只有少量的区别, 下面分别用C++和C来实现JNI方法.

首先是C++, 在工程的主目录创建一个子目录, 名称任意, 这里选择jni作为子目录的名称, 然后将之前通过javah命令生成的.h头文件复制到创建的目录下, 接着创建test.cpp实现如下:

// test.cpp
#include "com_gavinandre_jnitest_JniTest.h"
#include 
JNIEXPORT jstring JNICALL Java_com_gavinandre_jnitest_JniTest_get(JNIEnv *env, jobject thiz){
    printf("invoke get in C++\n");
    return env->NewStringUTF("Hello from JNI !");
}
JNIEXPORT void JNICALL Java_com_gavinandre_jnitest_JniTest_get(JNIEnv *env, jobject thiz, jstring string){
    printf("invoke set from C++\n");
    char* str = (char*) env->GetStringUTFChars(string, NULL);
    printf("%s\n, str");
    env->ReleaseStringUTFChars(string, str);
}

然后是c,创建test.c实现如下:

// test.c
#include "com_gavinandre_jnitest_JniTest.h"
#include 
JNIEXPORT jstring JNICALL Java_com_gavinandre_jnitest_JniTest_get(JNIEnv *env, jobject thiz){
    printf("invoke get in C\n");
    return (*env)->NewStringUTF(env, "Hello from JNI .");
    }
JNIEXPORT void JNICALL Java_com_gavinandre_jnitest_JniTest_set(JNIEnv *env, jobject thiz, jstring string){
    printf("invoke get from C\n");
    char* str = (char*) (*env)->GetStringUTFChars(env, string, NULL);
    printf("%s\n, str");
    (*env)->ReleaseStringUTFChars(env, string, str);
    }

其实C\C++在实现上很相似, 但是对于env的操作方式有所不同, 因此用C++和C来实现同一个JNI方法, 它们的区别主要集中来对env的操作上, 其他都是类似的, 如下所示.

C++: env->NewStringUTF("Hello from JNI !");
C: (*env)->NewStringUTF("Hello from JNI .");

4. 编写so库并在java中调用
so库的编译这里采用gcc, 切换到jni目录中, 对于test.cpp和test.c来说, 它们的编译指令如下所示.

gcc -shared -I /usr/lib/jvm/java-8-oracle/include -fPIC test.cpp(test.c) -o libjni-test.so

上面的编译命令中, /usr/lib/jvm/java-8-oracle是本地jdk的安装路径, 在其他环境编译时将其指向本机的jdk路径即可. 而libjni-test.so则是生成的so库的名字, 在Java中可以通过如下方式加载: System.loadLibrary(“jni-test”), 其中so库名字中的"lib"和".so"是不需要明确指出的. so库编译完成后, 就可以在Java程序中调用so库了, 这里通过Java执行来执行Java程序, 切换到主目录, 执行如下指令: java -Djava.library.path=jni com.gavinandre.jnitest.JniTest , 其中-Djava.library.path=jni指明了so库的路径.

首先采用C++生成so库,程序运行后日志如下:

invoke get in C++
Hello from JNI !
invode set from C++
hello world

然后采用C生成so库,程序运行后日志如下:

invoke get in C
Hello from JNI !
invode set from C
hello world

通过上面的日志可以发现, 在Java中成功的调用C/C++的代码,这就是JNI典型的工作流程.

你可能感兴趣的:(Android,JNI/NDK,Android开发艺术探索笔记)