Android NDK开发-JNI基础篇

  • 内容
  • 评论
  • 相关

1. NDK

NDK是Google开发的一套开发和编译工具集,可以生成动态链接库,主要用于Android的JNI开发。NDK 可以自动地将 so 和 Java 应用一起打包,极大地减轻了开发人员的打包工作。

NDK 提供了一份稳定、功能有限的 API 头文件声明,Google 明确声明该 API 是稳定的,在后续所有版本中都稳定支持当前发布的 API。从该版本的 NDK 中看出,这些 API 支持的功能非常有限,包含有:C 标准库(libc)、标准数学库(libm)、压缩库(libz)、Log 库(liblog)。

NDK的优点:

  • 代码的保护。由于 apk 的 java 层代码很容易被反编译,而 C/C++ 库反汇难度较大。
  • 可以方便地使用现存的开源库。大部分现存的开源库都是用 C/C++ 代码编写的。
  • 提高程序的执行效率。将要求高性能的应用逻辑使用 C 开发,从而提高应用程序的执行效率。
  • 便于移植。用 C/C++ 写得库可以方便在其他的嵌入式平台上再次使用。

2. JNI

JNI 全称 Java Native Interface,Java 本地化接口,可以通过 JNI 调用系统提供的 API。操作系统,无论是 Linux,Windows 还是 Mac OS,或者一些汇编语言写的底层硬件驱动都是 C/C++ 写的。Java和C/C++不同 ,它不会直接编译成平台机器码,而是编译成虚拟机可以运行的Java字节码的.class文件,通过JIT技术即时编译成本地机器码,所以有效率就比不上C/C++代码,JNI技术就解决了这一痛点,JNI 可以说是 C 语言和 Java 语言交流的适配器、中间件,下面我们来看看JNI调用示意图:
JNI技术通过JVM调用到各个平台的API,虽然JNI可以调用C/C++,但是JNI调用还是比C/C++编写的原生应用还是要慢一点,不过对高性能计算来说,这点算不得什么,享受它的便利,也要承担它的弊端。

3. JNI 与 NDK 区别

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

4. JNI 作用

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

JNI在Android中作用:
JNI可以调用本地代码库(即C/C++代码),并通过 Dalvik 虚拟机与应用层和应用框架层进行交互,Android中JNI代码主要位于应用层和应用框架层;

  • 应用层: 该层是由JNI开发,主要使用标准JNI编程模型;
  • 应用框架层: 使用的是Android中自定义的一套JNI编程模型,该自定义的JNI编程模型弥补了标准JNI编程模型的不足;

5. JNI数据类型

JNI定义了一些自己的数据类型。这些数据类型是衔接Java层和C/C++层的,如果有一个对象传递下来,那么对于C/C++来说是没办法识别这个对象的,同样的如果C/C++的指针对于Java层来说它也是没办法识别的,那么就需要JNI进行匹配,所以需要定义一些自己的数据类型。

5.1 基本数据类型

下图是Java基本数据类型和本地类型的映射关系,这些基本数据类型都是可以直接在 Native 层直接使用的
Java类型 本地类型 描述
boolean jboolean C/C++8位整型
byte jbyte C/C++带符号的8位整型
char jchar C/C++无符号的16位整型
short jshort C/C++带符号的16位整型
int jint C/C++带符号的32位整型
long jlong C/C++带符号的64位整型e
float jfloat C/C++32位浮点型
double jdouble C/C++64位浮点型

5.2 引用数据类型

另外,还有引用数据类型和本地类型的映射关系:
Java类型 本地类型 描述
Object jobject 任何Java对象,或者没有对应java类型的对象
Class jclass Class对象
String jstring 字符串对象
Object[] jobjectArray 任何对象的数组
boolean[] jbooleanArray 布尔型数组
byte[] jbyteArray 比特型数组
char[] jcharArray 字符型数组
short[] jshortArray 短整型数组
int[] jintArray 整型数组
long[] jlongArray 长整型数组
float[] jfloatArray 浮点型数组
double[] jdoubleArray 双浮点型数组

需要注意的是,

  • 1)引用类型不能直接在 Native 层使用,需要根据 JNI 函数进行类型的转化后,才能使用;
  • 2)多维数组(含二维数组)都是引用类型,需要使用 jobjectArray 类型存取其值;

例如,二维整型数组就是指向一位数组的数组,其声明使用方式如下:

//获得一维数组的类引用,即jintArray类型
jclass intArrayClass = env->FindClass(“[I”);
//构造一个指向jintArray类一维数组的对象数组,该对象数组初始大小为length,类型为 jsize
jobjectArray obejctIntArray = env->NewObjectArray(length ,intArrayClass , NULL);

6.JNI 描述符

6.1域描述符

1)基本类型描述符

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

Java 类型 符号
boolean Z
byte B
char C
short S
int I
long J
float F
double D
void V
objects对象 Lfully-qualified-class-name;L类名
Arrays数组 [array-type [数组类型
methods方法 (argument-types)return-type(参数类型)返回类型

2)引用类型描述符

一般引用类型描述符的规则如下,注意不要丢掉“;”

L + 类描述符 + ;

如,String 类型的域描述符为:

Ljava/lang/String;

数组的域描述符特殊一点,如下,其中有多少级数组就有多少个“[”,数组的类型为类时,则有分号,为基本类型时没有分号

[ + 其类型的域描述符
java 数组类型 描述符
int[]  [I
double[] [D
String[] [Ljava/lang/String;
Object[] [Ljava/lang/Object;
int[][] [[I
double[][] [[D

对应在 jni.h 获取 Java 的字段的 native 函数如下,name为 Java 的字段名字,sig 为域描述符

//C
jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
jobject (*GetObjectField)(JNIEnv*, jobject, jfieldID);
//C++
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
{ return functions->GetFieldID(this, clazz, name, sig); }
jobject GetObjectField(jobject obj, jfieldID fieldID)
{ return functions->GetObjectField(this, obj, fieldID); }

具体使用,后面会讲到。

6.2 类描述符

类描述符是类的完整名称:包名+类名,java 中包名用 . 分割,jni 中改为用 / 分割
如,Java 中 java.lang.String 类的描述符为 java/lang/String
native 层获取 Java 的类对象,需要通过 FindClass() 函数获取, jni.h 的函数定义如下:

//C
jclass (*FindClass)(JNIEnv*, const char*);
//C++
jclass FindClass(const char* name)
{ return functions->FindClass(this, name); }

字符串参数就是类的引用类型描述符,如 Java 对象 cn.cfanr.jni.JniTest,对应字符串为Lcn/cfanr/jni/JniTest; 如下:

jclass jclazz = env->FindClass(“Lcn/cfanr/jni/JniTest;”);

详细用法的例子,后面会讲到。

6.3 方法描述符

方法描述符需要将所有参数类型的域描述符按照声明顺序放入括号,然后再加上返回值类型的域描述符,其中没有参数时,不需要括号,如下规则:

(参数……)返回类型
方法描述 java 方法
“()Ljava/lang/String;” String f();
“(ILjava/lang/Class;)J” long f(int i, Class c);
“([B)V” String(byte[] bytes);
 (II)I int sum(int a, int b)
([Ljava/lang/String;)V void main(String[] args)

另外,对应在 jni.h 获取 Java 方法的 native 函数如下,其中 jclass 是获取到的类对象,name 是 Java 对应的方法名字,sig 就是上面说的方法描述符

//C
jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
//C++
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
{ return functions->GetMethodID(this, clazz, name, sig); }

不过在实际编程中,如果使用 javah 工具来生成对应的 native 代码,就不需要手动编写对应的类型转换了。

7. JNI里使用Java里的数组和对象

7.1 使用数组

JNI通过JNIEnv提供的操作Java数组的功能。它提供了两个函数:一个是操作java的简单型数组的,另一个是操作对象类型数组的。

因为速度的原因,简单类型的数组作为指向本地类型的指针暴露给本地代码。因此,它们能作为常规的数组存取。这个指针是指向实际的Java数组或者Java数组的拷贝的指针。另外,数组的布置保证匹配本地类型。

为了存取Java简单类型的数组,你就要要使用GetXXXArrayElements函数(见下表),XXX代表了数组的类型。这个函数把Java数组看成参数,返回一个指向对应的本地类型的数组的指针。

函数 Java数组类型 本地类型
GetBooleanArrayElements jbooleanArray jboolean
GetByteArrayElements jbyteArray jbyte
GetCharArrayElements jcharArray jchar
GetShortArrayElements jshortArray jshort
GetIntArrayElements jintArray jint
GetLongArrayElements jlongArray jlong
GetFloatArrayElements jfloatArray jfloat
GetDoubleArrayElements jdoubleArray jdouble

JNI数组存取函数

当你对数组的存取完成后,要确保调用相应的ReleaseXXXArrayElements函数,参数是对应Java数组和GetXXXArrayElements返回的指针。如果必要的话,这个释放函数会复制你做的任何变化(这样它们就反射到java数组),然后释放所有相关的资源。

为了使用java对象的数组,你必须使用GetObjectArrayElement函数和SetObjectArrayElement函数,分别去get,set数组的元素。GetArrayLength函数会返回数组的长度。

举一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
////输入一个数组,这里输入的是一个Boolean类型的数组
JNIEXPORT void JNICALL Java_com_aoaoyi_jnidemo_ChangeMethodFromJni_setArray
(JNIEnv *env, jobject, jbooleanArray ba)
{
    jboolean* pba = (env)->GetBooleanArrayElements(ba, 0 );
    jsize len = (env)->GetArrayLength(ba);
    int i=0;
    // change even array elements
    for( i=0; i < len; i+=2 )
    {
        pba[i] = JNI_FALSE;
        printf( "boolean = %s\n", (pba[i]==JNI_TRUE ? "true" : "false") );
    }
    (env)->ReleaseBooleanArrayElements(ba, pba, 0 );
}

7.2 使用对象

JNI提供的另外一个功能是在本地代码中使用Java对象。通过使用合适的JNI函数,你可以创建Java对象,get、set 静态(static)和实例(instance)的域,调用静态(static)和实例(instance)函数。JNI通过ID识别域和方法,一个域或方法的ID是任何处理域和方法的函数的必须参数。

下表列出了用以得到静态(static)和实例(instance)的域与方法的JNI函数。每个函数接受(作为参数)域或方法的类,它们的名称,符号和它们对应返回的jfieldIDjmethodID

函数 描述
GetFieldID 得到一个实例的域的ID
GetStaticFieldID 得到一个静态的域的ID
GetMethodID 得到一个实例的方法的ID
GetStaticMethodID 得到一个静态方法的ID

※域和方法的函数

如果你有了一个类的实例,它就可以通过方法GetObjectClass得到,或者如果你没有这个类的实例,可以通过FindClass得到。

下面举例,看看如何通过使用数组和对象,从C++中的获取到Java中的DiskInfo 类对象,并返回一个DiskInfo数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
////返回一个结构数组,返回一个硬盘信息的结构数组
JNIEXPORT jobjectArray JNICALL Java_com_aoaoyi_jnidemo_ChangeMethodFromJni_getStructArray
(JNIEnv *env, jobject _obj)
{
    //申明一个object数组 
    jobjectArray args = 0;
 
    //数组大小
    jsize        len = 5;
 
    //获取object所属类,一般为ava/lang/Object就可以了
    jclass objClass = (env)->FindClass("java/lang/Object");
 
    //新建object数组
    args = (env)->NewObjectArray(len, objClass, 0);
 
    /* 下面为获取到Java中对应的实例类中的变量*/
 
    //获取Java中的实例类
    jclass objectClass = (env)->FindClass("com/aoaoyi/jnidemo/DiskInfo");
 
    //获取类中每一个变量的定义
    //名字
    jfieldID str = (env)->GetFieldID(objectClass,"name","Ljava/lang/String;");
    //序列号
    jfieldID ival = (env)->GetFieldID(objectClass,"serial","I");
 
    //给每一个实例的变量付值,并且将实例作为一个object,添加到objcet数组中
    for(int  i=0; i < len; i++ )
    {
        //给每一个实例的变量付值
        jstring jstr = WindowsTojstring(env,"我的磁盘名字是 D:");
        //(env)->SetObjectField(_obj,str,(env)->NewStringUTF("my name is D:"));
        (env)->SetObjectField(_obj,str,jstr);
        (env)->SetShortField(_obj,ival,10);
 
        //添加到objcet数组中
        (env)->SetObjectArrayElement(args, i, _obj);
    }
    //返回object数组
    return args;
 
 }

8. JNIEnv 分析

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

1
2
3
4
5
6
7
8
9
10
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

8.1 JNIEnv 特点

  • JNIEnv 是一个指针,指向一组 JNI 函数,通过这些函数可以实现 Java 层和 JNI 层的交互,就是说通过 JNIEnv 调用 JNI 函数可以访问 Java 虚拟机,操作 Java 对象;
  • 所有本地函数都会接收 JNIEnv 作为第一个参数;(不过 C++ 的JNI 函数已经对 JNIEnv 参数进行了封装,不用写在函数参数上)
  • 用作线程局部存储,不能在线程间共享一个 JNIEnv 变量,也就是说 JNIEnv 只在创建它的线程有效,不能跨线程传递;相同的 Java 线程调用本地方法,所使用的 JNIEnv 是相同的,一个 native 方法不能被不同的 Java 线程调用;

8.2 JavaEnv 和 JavaVM 的关系

  • 1)每个进程只有一个 JavaVM(理论上一个进程可以拥有多个 JavaVM 对象,但 Android 只允许一个),每个线程都会有一个 JNIEnv,大部分 JNIAPI 通过 JNIEnv 调用;也就是说,JNI 全局只有一个 JavaVM,而可能有多个 JNIEnv;
  • 2)一个 JNIEnv 内部包含一个 Pointer,Pointer 指向 Dalvik 的 JavaVM 对象的 Function Table,JNIEnv 内部的函数执行环境来源于 Dalvik 虚拟机;
  • 3)Android 中每当一个Java 线程第一次要调用本地 C/C++ 代码时,Dalvik 虚拟机实例会为该 Java 线程产生一个 JNIEnv 指针;
  • 4)Java 每条线程在和 C/C++ 互相调用时,JNIEnv 是互相独立,互不干扰的,这样就提升了并发执行时的安全性;
  • 5)当本地的 C/C++ 代码想要获得当前线程所想要使用的 JNIEnv 时,可以使用 Dalvik VM 对象的 JavaVM jvm->GetEnv()方法,该方法会返回当前线程所在的 JNIEnv
  • 6)Java 的 dex 字节码和 C/C++ 的 .so 同时运行 Dalvik VM 之内,共同使用一个进程空间;

8.3 C 语言的 JNIEnv

由上面代码可知,C 语言的JNIEnv 就是const struct JNINativeInterface*,而 JNIEnv* env就等价于JNINativeInterface** env,env 实际是一个二级指针,所以想要得到 JNINativeInterface 结构体中定义的函数指针,就需要先获取 JNINativeInterface 的一级指针对象env,然后才能通过一级指针对象调用 JNI 函数,例如:
`(*
env)->NewStringUTF(env, “hello”)`

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct JNINativeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;
    void*       reserved3;
    jint        (*GetVersion)(JNIEnv *);
    jclass      (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*, jsize);
    jclass      (*FindClass)(JNIEnv*, const char*);
    jmethodID   (*FromReflectedMethod)(JNIEnv*, jobject);
    jfieldID    (*FromReflectedField)(JNIEnv*, jobject);
    /* spec doesn't show jboolean parameter */
    jobject     (*ToReflectedMethod)(JNIEnv*, jclass, jmethodID, jboolean);
    jclass      (*GetSuperclass)(JNIEnv*, jclass);
    jboolean    (*IsAssignableFrom)(JNIEnv*, jclass, jclass);
    /* spec doesn't show jboolean parameter */
    jobject     (*ToReflectedField)(JNIEnv*, jclass, jfieldID, jboolean);
	  //……定义了一系列关于 Java 操作的函数
}

8.4 C++的 JNIEnv

typedef _JNIEnv JNIEnv;可知,C++的 JNIEnv 是 _JNIEnv 结构体,而 _JNIEnv 结构体定义了 JNINativeInterface 的结构体指针,内部定义的函数实际上是调用 JNINativeInterface 的函数,所以C++的 env 是一级指针,调用时不需要加 env 作为函数的参数,例如:env->NewStringUTF(env, "hello")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct _JNIEnv {
    /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;
#if defined(__cplusplus)
    jint GetVersion()
    { return functions->GetVersion(this); }
    jclass DefineClass(const char *name, jobject loader, const jbyte* buf, jsize bufLen)
    { return functions->DefineClass(this, name, loader, buf, bufLen); }
    jclass FindClass(const char* name)
    { return functions->FindClass(this, name); }
    jmethodID FromReflectedMethod(jobject method)
    { return functions->FromReflectedMethod(this, method); }
    jfieldID FromReflectedField(jobject field)
    { return functions->FromReflectedField(this, field); }
    jobject ToReflectedMethod(jclass cls, jmethodID methodID, jboolean isStatic)
    { return functions->ToReflectedMethod(this, cls, methodID, isStatic); }
    jclass GetSuperclass(jclass clazz)
    { return functions->GetSuperclass(this, clazz); }
    //……
}

9. JNI 的两种注册方式

Java 的 native 方法是如何链接 C/C++中的函数的呢?可以通过静态和动态的方式注册JNI。

9.1 静态注册

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

实现方式1:

  • 先编写 Java 的 native 方法;
  • 然后用 javah 工具生成对应的头文件,执行命令 javah packagename.classname可以生成由包名加类名命名的 jni 层头文件,或执行命名javah -o custom.h packagename.classname,其中 custom.h 为自定义的文件名;
  • 实现 JNI 里面的函数,再在Java中通过System.loadLibrary加载 so 库即可;

在AndroidStudio中编译后,进入项目的目录app/build/intermediates/classes/debug下,运行如下命令:

1
javah -d cpp -jni  -classpath ..\..\build\intermediates\classes\debug com.aoaoyi.poker.jni.Jni
-d 和-o
这两个参数用于设置生成的C\C++头文件的指定,该两参数选项不能同时使用,-d是为<classes>中的每个有JNI方法的java类都生成一个头文件,并存放在-d指定的目录中,-o则是生成的所有JNI方法的头文件都放在-o指定的文件中。
-version
显示当前javah的版本号.
实例2:
javah -version
javah version “1.6.0_11”
-jin
表示用于生成JNI风格的C\C++头文件,默认该参数就是开启的。不过应该不能关闭参数。
-bootclasspath和-classpath
javah操作是针对类文件,-bootclasspath和-classpath就是指定在哪里进行类文件搜索。
JDK搜索类文件先后顺序如下:Bootstrap classes,User classes
Bootstrap默认的是JDK自带的jar或zip文件,它包括jre\lib下rt.jar等文件,JDK首先搜索这些文件.
可以通过-bootclasspath来设置它。文件之间用分号”;”进行分割。
User classes搜索顺序为当前目录、环境变量 CLASSPATH、-classpath。
它们用于告知JDK搜索类文件根目录名、jar文档名、zip文档名,用分号”;”进行分隔。
例如当你自己开发了公共类并包装成一个common.jar包,在使用 common.jar中的类时,就需要用-classpath common.jar 告诉JDK从common.jar中查找该类,否则JDK就会抛出java.lang.NoClassDefFoundError异常,表明未找到类定义。
使用-classpath后JDK将不再使用CLASSPATH中的类搜索路径,如果-classpath和CLASSPATH都没有设置,则JDK使用当前路径(.)作为类搜索路径。
推荐使用-classpath来定义JDK要搜索的类路径,而不要使用环境变量CLASSPATH的搜索路径,以减少多个项目同时使用CLASSPATH时存在的潜在冲突。例如应用1要使用a1.0.jar中的类G,应用2要使用 a2.0.jar中的类G,a2.0.jar是a1.0.jar的升级包,当a1.0.jar,a2.0.jar都在CLASSPATH中,JDK搜索到第一个包中的类G时就停止搜索,如果应用1应用2的虚拟机都从CLASSPATH中搜索,就会有一个应用得不到正确版本的类G。
javah命令是针对类文件中的,你肯定需要要把你要操作的类文件的根目录包含在搜索路径中,对于包文件(Jar或zip)形式的类文件,它的根目录就是包文件。另外这里的-bootclasspath和-classpath与java或javac命令都很相似,但是javah命令没有用来设置Extension classes的-extdirs参数选项,有点奇怪!还有这里的”-classpath”也不能缩写成”-cp”
方式2:
在 Android Studio 上新建一个类Jni.java,随便写一个 native 方法,然后点击红色的方法,AS 会自动生成一个对应的 C++ 语言文件*.cppji及JNI方法。

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

那么怎么知道对应Java中的哪个类的哪个native方法呢,我们仔细观察JNI函数名的构成其实是:Java_PkgName_ClassName_NativeMethodName,以Java为前缀,并且用“_”下划线将包名、类名以及native方法名连接起来就是对应的JNI函数了。

可以看出 JNI 的调用函数的定义是按照一定规则命名的:
JNIEXPORT 返回值 JNICALL Java_全路径类名_方法名_参数签名(JNIEnv* , jclass, 其它参数);
其中 Java_ 是为了标识该函数来源于 Java。经检验(不一定正确),如果是重载的方法,则有“参数签名”,否则没有;另外如果使用的是 C++,在函数前面加上 extern “C”(表示按照 C 的方式编译),函数命名后面就不需要加上“参数签名”。

另外还需要注意几点特殊规则:

  • 1. 包名或类名或方法名中含下划线 _ 要用 _1 连接;
  • 2. 重载的本地方法命名要用双下划线 __ 连接;
  • 3. 参数签名的斜杠 “/” 改为下划线 “_” 连接,分号 “;” 改为 “_2” 连接,左方括号 “[” 改为 “_3” 连接;
    另外,对于 Java 的 native 方法,static 和非 static 方法的区别在于第二个参数,static 的为 jclass,非 static 的 为 jobject;JNI 函数中是没有修饰符的。

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

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

9.2 动态注册

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

  • 先编写 Java 的 native 方法;
  • 编写 JNI 函数的实现(函数名可以随便命名);
  • 利用结构体 JNINativeMethod 保存Java native方法和 JNI函数的对应关系;
  • 利用registerNatives(JNIEnv* env)注册类的所有本地方法;
  • 在 JNI_OnLoad 方法中调用注册方法;
  • 在Java中通过System.loadLibrary加载完JNI动态库之后,会调用JNI_OnLoad函数,完成动态注册;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
 //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);
举个例子,这样能更深刻理解:

Jni2.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Jni2 {
 
    static {
        System.loadLibrary("aoaoyi-dynamic");
    }
 
    public static native String getBaseUrl();
    public static native String getReportUrl();
    public static native String getUserProfileUrl(int userId);
 
 
}

aoaoyi-dynamic.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#include <jni.h>
#include <string>
#include <string.h>
#include <assert.h>
 
//char const *BASE_URL = "http://aoaoyi.com/api/";
std::string BASE_URL = "http://aoaoyi.com/api/";
 
#ifdef __cplusplus
extern "C"
{
#endif
 
jstring JNICALL getBaseUrl(
        JNIEnv * env,
        jobject) {
    return env->NewStringUTF(BASE_URL.c_str());
}
 
jstring JNICALL getReportUrl(
        JNIEnv * env,
        jobject) {
    std::string  reportUrl = BASE_URL;
    reportUrl += "report/";
    return env->NewStringUTF(reportUrl.c_str());
}
 
jstring JNICALL getUserProfileUrl(
        JNIEnv * env,
        jobject,
        jint pUserId) {
    std::string  profileUrl = BASE_URL;
    profileUrl += "profile/";
    profileUrl += std::to_string(pUserId);
    return (env)->NewStringUTF(profileUrl.c_str());
}
 
//指定要注册的类
#define JNI_REG_CLASS "com/aoaoyi/poker/jni/Jni2"
 
/**
 * 映射
 */
static JNINativeMethod gMethods[] = {
        { "getBaseUrl", "()Ljava/lang/String;", (void*)getBaseUrl },
        { "getReportUrl", "()Ljava/lang/String;", (void*)getReportUrl },
        { "getUserProfileUrl", "(I)Ljava/lang/String;", (void*)getUserProfileUrl },
 
};
 
/*
*
* 注册JNI函数
*/
static int registerNativeMethods(JNIEnv* env, const char* className,
                                 JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz = NULL;
    clazz = env->FindClass(className);
    if (env->ExceptionCheck() || clazz == NULL) {
        env->ExceptionClear();
        clazz = NULL;
        return JNI_FALSE;
    }
    /**
     * RegisterNatives
     *
     * @param clazz java类名,通过 FindClass 获取
     * @param methods JNINativeMethod 结构体指针
     * @param nMethods 方法个数
     */
    int nRC = env->RegisterNatives(clazz, gMethods, numMethods);
    if (env->ExceptionCheck() || nRC < 0) {
        env->ExceptionClear();
        return JNI_FALSE;
    }
 
    return JNI_TRUE;
}
 
/*
* 注册给类成员函数
*/
int registerNatives(JNIEnv* env)
{
    if (NULL == env)
    {
        return JNI_FALSE;
    }
 
    if (!registerNativeMethods(env, JNI_REG_CLASS, gMethods,
                               sizeof(gMethods) / sizeof(gMethods[0])))
        return JNI_FALSE;
 
    return JNI_TRUE;
}
 
/*
* 反注册给类成员函数
*/
void unregisterNatives(JNIEnv* env)
{
    if (NULL == env)
    {
        return;
    }
 
    jclass clazz = NULL;
    clazz = env->FindClass(JNI_REG_CLASS);
    if (env->ExceptionCheck() || clazz == NULL) {
        env->ExceptionClear();
        clazz = NULL;
        return;
    }
 
    env->UnregisterNatives(clazz);
    if (env->ExceptionCheck())
    {
        env->ExceptionClear();
    }
}
 
/*
* 成功则返回JNI版本号,失败返回-1.
*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv* env = NULL;
    jint result = -1;
 
    if (vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }
    assert(env != NULL);
    //注册
    if (!registerNatives(env)) {
        return -1;
    }
    /* 注册成功,返回JNI版本号 */
    result = JNI_VERSION_1_6;
 
    return result;
}
 
/*
* 解除注册
*/
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) {
    JNIEnv *env = NULL;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6))
        return;
    unregisterNatives(env);
    return;
}
 
#ifdef __cplusplus
}
#endif
运行结果: