JNI使用入门

简介

JNI 全称 Java Native Interface,Java 本地化接口。即 Java 可以通过 JNI 调用 C/C++ 代码。

  • JNI:JNI是一套编程接口,用来实现Java代码与本地的C/C++代码进行交互;
  • NDK: NDK是Google开发的一套开发和编译工具集,可以生成动态链接库,主要用于Android的JNI开发;

它有如下特点:

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

Gradle配置

注意:如果直接运行发现so库未打包到APK,可以尝试使用使用build,然后安装,

android {
    defaultConfig {
        //配置编译平台
        externalNativeBuild {
            cmake {
                cppFlags ""
                //生成 so 的平台,如下表示只生成 armeabi-v7a 一个平台,目前市面上绝大多数机器都是该平台
                abiFilters 'armeabi-v7a'
				//表示生成如下五种平台
				//abiFilters "armeabi","armeabi-v7a" , "arm64-v8a", "x86", "x86_64"
            }
        }
    }
    externalNativeBuild {
	    //指定CMake路径
        cmake {
            path "CMakeLists.txt"
        }
    }
}

数据类型

Java 数据类型和 C/C++ 数据类型存在映射关系,映射关系都定义在jni.h中,并且如果是 Java 对象的映射还会区分了 C 和 C++的不同环境。

  • C++环境:jobject, jclass, jstring, jarray 等都是继承自_jobject
  • C环境:jobject, jclass, jstring, jarray 等是空类型指针typedef void* jobject
#ifndef JNI_H_
#define JNI_H_
#include 
#include 

//基本数据类型
typedef uint8_t jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
typedef jint jsize;

//对象类型
#ifdef __cplusplus
//C++语言环境
typedef _jobject* jobject;
typedef _jclass* jclass;
typedef _jstring* jstring;
typedef _jarray* jarray;
typedef _jobjectArray* jobjectArray;
typedef _jbooleanArray* jbooleanArray;
typedef _jbyteArray* jbyteArray;
typedef _jcharArray* jcharArray;
typedef _jshortArray* jshortArray;
typedef _jintArray* jintArray;
typedef _jlongArray* jlongArray;
typedef _jfloatArray* jfloatArray;
typedef _jdoubleArray* jdoubleArray;
typedef _jthrowable* jthrowable;
typedef _jobject* jweak;
#else
//C语言环境
typedef void* jobject;
typedef jobject jclass;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jobjectArray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jobject jthrowable;
typedef jobject jweak;
#endif

JNI 描述符

基本数据描述符

下面是基本的数据类型的描述符,除了 boolean 和 long 类型分别是 Z 和 J 外,其他的描述符对应的都是Java类型名的大写首字母,void 的描述符为 V

基本类型 描述符
byte B
char C
double D
float F
int I
long J
short S
boolean Z
void V
引用类型描述符

一般引用类型描述符的规则如L + 类描述符 + ;,注意不要丢掉,例如String类型的描述符为:Ljava/lang/String;。数组的描述符特殊一点,有多少级数组就有多少个[,数组的类型为类时,则有分号,为基本类型时没有分号[ + 其类型的域描述符

引用类型 描述符
String Ljava/lang/String;
Object[] [Ljava/lang/Object;
int[] [I
double[] [D
int[][] [[I
double[][] [[D
方法描述符

方法描述符格式为:(参数……)返回类型,示例如下:

Java方法 方法描述符
void init() ()V
String getString() ()Ljava/lang/String;
int sum(int a, int b) (II)I
void main(String[] args) ([Ljava/lang/String;)V

JNIEnv 分析

JNIEnv 是 jni.h 文件最重要的部分,它的本质是指向函数表指针的指针(JavaVM也是),函数表里面定义了很多 JNI 函数,同时它也是区分 C 和 C++环境的(由上面介绍描述符时也可以看到),在 C 语言环境中,JNIEnv 是strut JNINativeInterface*的指针别名,而在C++中是_JNIEnv的指针别名

struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;

#if defined(__cplusplus)  
typedef _JNIEnv JNIEnv; //C++中的 JNIEnv 类型
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv; //C语言的 JNIEnv 类型
typedef const struct JNIInvokeInterface* JavaVM;
#endif
JNIEnv 特点
  • JNIEnv 是一个指针,指向一组 JNI 函数,通过这些函数可以实现 Java 层和 JNI 层的交互,就是说通过 JNIEnv 调用 JNI 函数可以访问 Java 虚拟机,操作 Java 对象;
  • 所有本地函数都会接收 JNIEnv 作为第一个参数;(不过 C++ 的JNI 函数已经对 JNIEnv 参数进行了封装,不用写在函数参数上)
  • 用作线程局部存储,不能在线程间共享一个 JNIEnv 变量,也就是说 JNIEnv 只在创建它的线程有效,不能跨线程传递;相同的 Java 线程调用本地方法,所使用的 JNIEnv 是相同的,一个 native 方法不能被不同的 Java 线程调用;
JavaEnv 和 JavaVM 的关系
  • 在Android中每个进程只有一个 JavaVM,每个线程都会有一个 JNIEnv,大部分 JNIAPI 通过 JNIEnv 调用
  • 一个 JNIEnv 内部包含一个 Pointer,Pointer 指向 Dalvik 的 JavaVM 对象的 Function Table,JNIEnv 内部的函数执行环境来源于 Dalvik 虚拟机;
  • Android 中每当一个Java 线程第一次要调用本地 C/C++ 代码时,Dalvik 虚拟机实例会为该 Java 线程产生一个 JNIEnv 指针;
  • Java 每条线程在和 C/C++ 互相调用时,JNIEnv 是互相独立,互不干扰的,这样就提升了并发执行时的安全性;
  • 当本地的 C/C++ 代码想要获得当前线程所想要使用的 JNIEnv 时,可以使用 Dalvik VM 对象的 JavaVM* jvm->GetEnv()方法,该方法会返回当前线程所在的 JNIEnv*
  • Java 的 dex 字节码和 C/C++ 的 .so 同时运行 Dalvik VM 之内,共同使用一个进程空间;

native头文件生成

//在java目录中执行
javac -h <输出目录> <源文件>(例如 .\com\whf\jnitestdemo\opengl\JniRendererInterface.java)

JNI 的两种注册方式

静态注册

原理:根据函数名建立 Java 方法和native函数的一一对应关系:

  • 先编写 Java 的 native 方法;
  • 然后用 javac -h 工具生成对应的头文件
  • 实现 JNI 里面的函数,再在Java中通过System.loadLibrary加载 so 库即可;
//Java方法
public native String stringFromJNI();
public native String stringFromJNI(String src);
public native int intFrom_JNI(String src);

//native方法
extern "C"
JNIEXPORT jstring JNICALL
Java_com_whf_jnitestdemo_JniTest_stringFromJNI__(JNIEnv *env, jobject instance) {
    // TODO
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_whf_jnitestdemo_JniTest_stringFromJNI__Ljava_lang_String_2(JNIEnv *env, jobject instance, jstring src) {
    // TODO
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_whf_jnitestdemo_JniTest_intFrom_1JNI(JNIEnv *env, jobject instance, jstring src_) {
    // TODO
}

可以看出 JNI 的调用函数的定义是按照一定规则命名的:JNIEXPORT 返回值 JNICALL Java_全路径类名_方法名_参数签名(JNIEnv* , jclass, 其它参数);

  • 包名或类名或方法名中含下划线 _ 要用 _1 连接;
  • 重载的本地方法命名要用双下划线 __ 连接(所有都要);
  • 参数签名的斜杠 / 改为下划线 _ 连接,分号 ; 改为 _2 连接,左方括号 [ 改为 _3 连接;
  • 对于 Java 的 native 方法,static 和非 static 方法的区别在于第二个参数,static 的为 jclass,非 static 的 为 jobject;JNI 函数中是没有修饰符的。
  • 如果使用的是 C++,在函数前面加上 extern “C”(表示按照 C 的方式编译)

优点

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

缺点

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

原理:直接告诉 native 方法其在JNI 中对应函数的指针,通过使用 JNINativeMethod 结构来保存 Java native 方法和 JNI 函数关联关系(Android源码中也大多采用动态注册,注册对照表在AndroidRuntime.cpp类中),步骤:

  • 先编写 Java 的 native 方法;
  • 编写 JNI 函数的实现(函数名可以随便命名);
  • 利用结构体 JNINativeMethod 保存Java native方法和 JNI函数的对应关系;
  • 利用 registerNatives(JNIEnv* env) 注册类的所有本地方法;
  • JNI_OnLoad 方法中调用注册方法;
  • 在Java中通过 System.loadLibrary 加载完JNI动态库之后,会调用 JNI_OnLoad 函数,完成动态注册;
//JNINativeMethod结构体
typedef struct {
    const char* name;  //Java中native方法的名字
    const char* signature;   //Java中native方法的描述符
    void* fnPtr;  //对应JNI函数的指针
} JNINativeMethod;

/**
 * @param clazz java类名,通过 FindClass 获取
 * @param methods JNINativeMethod 结构体指针
 * @param nMethods 方法个数
 */
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)

//JNI_OnLoad 
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);

使用示例如下:

//需要动态注册的方法
public class JniInterface {
    public native String dynamicRegister();
}

#include 
#include 
#include 
#include "android_log.h"

#ifdef __cplusplus
extern "C" {
#endif

//定义类名
static const char *className = "com/whf/jnitestdemo/JniInterface";

//定义对应Java native方法的 C++ 函数,函数名可以随意命名
static jstring sayHello(JNIEnv *env, jobject) {
    LOGI("hello, this is native log.");
    const char *hello = "Hello from C++.";
    return env->NewStringUTF(hello);
}

/*
 * 定义函数映射表(是一个数组,可以同时定义多个函数的映射)
 * 参数1:Java 方法名
 * 参数2:方法描述符,也就是签名
 * 参数3:C++定义对应 Java native方法的函数名
 */
static JNINativeMethod jni_Methods_table[] = {
        {"dynamicRegister", "()Ljava/lang/String;", (void *) sayHello},
};

//根据函数映射表注册函数
static int registerNativeMethods(JNIEnv *env, const char *className,
                                 const JNINativeMethod *gMethods, int numMethods) {
    jclass clazz;
    LOGI("Registering %s natives\n", className);
    clazz = (env)->FindClass(className);
    if (clazz == NULL) {
        LOGE("Native registration unable to find class '%s'\n", className);
        return JNI_ERR;
    }

    if ((env)->RegisterNatives(clazz, gMethods, numMethods) < 0) {
        LOGE("Register natives failed for '%s'\n", className);
        return JNI_ERR;
    }
    //删除本地引用
    (env)->DeleteLocalRef(clazz);
    return JNI_OK;
}

//在Java中通过System.loadLibrary加载完JNI动态库之后,会自动调用JNI_OnLoad函数,完成动态注册;
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    LOGI("call JNI_OnLoad");
    JNIEnv *env = NULL;
    //判断 JNI 版本是否为JNI_VERSION_1_4
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
        return JNI_EVERSION;
    }
    registerNativeMethods(env, className, jni_Methods_table,
                          sizeof(jni_Methods_table) / sizeof(JNINativeMethod));
    return JNI_VERSION_1_4;
}

#ifdef __cplusplus
}
#endif

你可能感兴趣的:(NDK,android)