魅力程序猿

  • 首页
  • Java
  • Android
  • APP
    • 扑克计分器
    • Video Wallpaper
  • 联系我
  • 关于我
  • 资助
道子
向阳而生
  1. 首页
  2. Android
  3. 正文

Android NDK开发-JNI基础篇

2018年6月27日 4280点热度 0人点赞 0条评论

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函数会返回数组的长度。

举一个简单的例子:

////输入一个数组,这里输入的是一个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函数。每个函数接受(作为参数)域或方法的类,它们的名称,符号和它们对应返回的jfieldID或jmethodID。

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

※域和方法的函数

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

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

////返回一个结构数组,返回一个硬盘信息的结构数组
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*的指针别名。

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”)`

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")

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下,运行如下命令:

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函数,完成动态注册;
 //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

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

#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
运行结果:
标签: JNI基础篇
最后更新:2018年6月27日

daozi

这个人很懒,什么都没留下

点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复
搜索
联系方式

QQ群:179730949
QQ群:114559024
欢迎您加入Android大家庭
本人QQ:136049925

赐我一丝安慰
给我一点鼓励

COPYRIGHT © 2023 魅力程序猿. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

豫ICP备15000477号