JNI与NDK

本文详细介绍了Android中JNI的使用原因,包括处理速度需求、硬件控制和代码复用。讲解了从编写java代码、生成C语言头文件、编写C代码到生成C共享库的完整过程。此外,还阐述了通过JNI调用C库函数的步骤,包括获取成员变量值和生成java对象。最后提到了直接注册JNI本地函数以提高运行效率,并简单提及了Android NDK的开发步骤。
摘要由CSDN通过智能技术生成

(一)前言

Android FrameWork由基于java语言的java层与基于C/C++语言的C/C++层组成的,每个层中的功能模块都是使用相应的语言编写的,并且每个层中的大部分模块是保持着千丝万缕的联系的。在Android FrameWork中,需要提供一种媒介或者桥梁,将java层和C/C++层有机的联系在一起,使得他们相互协调,共同完成某些任务。在这两层之间充当桥梁任务的就是java本地接口【JNI,java native interface】,它允许java类与使用C/C++等其他编程语言编写的应用程序、模块、库进行交互操作。比如,在java类中使用C语言库中的特定函数,或在C语言程序中使用java类库,都需要借助JNI来完成。


(二)使用JNI的原因

注重处理速度:与本地代码【C/C++】相比,java代码的执行速度要慢一些。如果对某些程序的执行速度有较高的要求,建议使用C/C++编写代码。而后在java中通过JNI调用基于C/C++编写的部分,常常能够获得更快的运行速度。
- 硬件控制:为了更好地控制硬件,硬件控制代码通常使用C语言编写。而后借助JNI将其与java层连接起来,从而实现对硬件的控制。
- C/C++代码的复用:在程序编写的过程中,使用已经编写好的C/C++代码,既提高了编程效率,又确保了程序的安全性与健壮性。


(三)在java中调用C库函数

在java代码中通过JNI调用C函数的步骤如下:
1. 第一步:编写java代码
2. 第二步:编译java代码
3. 第三步:生成C语言头文件
4. 第四步:编写C代码
5. 第五步:生成C共享库
6. 第六步:运行java程序

3.1 第一步:编写java代码
首先编写调用C函数的java源代码“JniHello.java”,代码如下:

class JniHello {
    // 本地方法声明
    native void printfHello();
    native void ptintfString(String str);
    // 加载库
    static{
        System.loadLibrary("hellojni");
    }
    public static void main(String[] args) {
        JniHello hello = new JniHello();
        // 调用本地方法(实际上是C语言实现的JNI本地函数)
        hello.printfHello();
        System.out.println("----------------------------------");
        hello.ptintfString("hello world from printfstring fun");
    }
}

3.2 第二步:编译java代码
事先下载安装好jdk,然后添加到系统环境变量,然后在命令行执行

javac JniHello.java

编译好之后就会生成JniHello.class文件。
3.3 第三步:生成C语言头文件
其实我最开始想的是为什么要有这一步呢,不是可以直接编写一个.c文件实现其中的方法然后进行加载调用就行了呢。其实现在了解到并不是这么简单的,了解到更深一层的原理或许就清楚为什么要添加这一步了。

其实在程序运行的时候,java虚拟机会在加载的本地运行库中查找与java本地方法相匹配的C函数,并生成映射表,而后将java本地方法与C函数连接在一起。但是java虚拟机并非是直接将C函数和java代码映射在一起,而是使用了函数原型,只有当生成了函数原型,java虚拟机即可把本地库中对应的函数与java函数链接在一起。也就是说若是想创建本地方法的的映射C函数,必须生成函数原型,函数原型存在于C/C++头文件中。

java中提供了javah工具了用来生成包含函数原型的C/C++头文件,使用方法:

javah <包含以native关键字声明的方法的java类名称>

运行javah命令,会在当前目录下生成与java类名相同名称的C语言头文件,定义了与java本地方法相链接的C函数原型。如下图:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_tmz_day01_JniHello */

#ifndef _Included_com_tmz_day01_JniHello
#define _Included_com_tmz_day01_JniHello
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_tmz_day01_JniHello
 * Method:    printfHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_tmz_day01_JniHello_printfHello
  (JNIEnv *, jobject);

/*
 * Class:     com_tmz_day01_JniHello
 * Method:    ptintfString
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_com_tmz_day01_JniHello_ptintfString
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

这是javah命令生成的两个C语言函数原型,函数原型在java类中声明的本地方法的基础上生成。查看各函数原型的注释,可以看到与各函数原型对应的java代码中的本地方法,注释中标明了三个元素:类名、本地方法名、本地方法签名。只要生成了这样的函数原型,java虚拟机就能把本地库函数与java本地方法正常地链接在一起。

接下来,分析一下函数原型。JNIEXPORT、JNICALL都是JNI的关键字,表示的是此函数被JNI调用,函数原型中必须有这两个关键字,JNI才能正常调用函数。JNI支持的函数命名形式是“Java_类名_本地方法名”。

接下来,了解一下函数原型的参数。在生成的函数模型中有两个默认参数,第一个、第二个分别是JNIEnv 与jobject,支持JNI的函数必须包含这两个共同参数。第一个参数JNIEnv 是JNI接口的指针,用来调用JNI表中的各种函数。这里的JNI函数是JNI中提供的基本函数集,用来在JNI本地函数中创建java对象或者调用相应方法。函数原型中的第二个默认参数jobject也是JNI提供的java本地类型,用来在C代码中访问java对象,此参数中保存着调用本地方法的对象的一个引用。

java是一种与平台无关的语言,其数据类型在任何平台下占用相同的内存空间,但是在C/C++这类本地语言中,即使同一种数据类型,在不同的平台下占用不同的内存空间。

在JNI编程中,java程序与C/C++函数间经常进行数据交换,这就要提供一种方法消除两种语言的数据类型的差异,那就是java本地类型,使得本地语言可以使用java数据类型。如下表所示:
java本地类型对照表

3.4 第四步:编写C代码
在C语言函数原型生成后,开始编写hellojni.c文件,具体实现JNI本地函数,首先把定义在.h头文件中的函数原型复制到.c文件中,注意,在使用javah命令生成的头文件中,函数的参数仅指定了参数的类型,并未给出参数的名称。因此复制完函数原型,开始实现C函数时,必须先在参数类型后指定具体的参数名称。
下面是编写好的hellojni.c的代码:

#include "com_tmz_day01_JniHello.h"
#include<stdio.h>

/* Class:     com_tmz_day01_JniHello
 * Method:    printfHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_tmz_day01_JniHello_printfHello
  (JNIEnv *env, jobject obj){
    printf("Hello tangmingzhang!");
    return;
  }

/*
 * Class:     com_tmz_day01_JniHello
 * Method:    ptintfString
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_com_tmz_day01_JniHello_ptintfString
  (JNIEnv *env, jobject obj, jstring string){
    // 将java String转换成C字符串
      const char *str = (*env)->GetStringUTFChars(env,string,0);
      printf("%s!\n",str);
      return; 
  } 

3.5 第五步:生成C共享库
可以在windows环境下利用Visual C++来构建共享库,也可以利用cygwin;Linux环境中可以使用gcc。

3.6 第六步:运行java程序


(四)调用JNI函数

4.1 通过JNI,获取成员变量值
1. 查找含待访问的成员变量的java类的jclass值
2. 获取此成员变量的jfieldID值。若是成员变量为静态变量,则调用名称为GetStaticFieldID的JNI函数;若是待访问的成员变量是普通对象,则调用名称为GetFieldID的JNI函数
3. 使用1,2中获得的jclass与jfieldID值,获取或者设置成员变量值

下面是GetFieldID 方法【GetStaticFieldID与其相同】

jfieldID GetFieldID (JNIEnv *env, jclass clazz, const char *name, const char *sig);
功能:返回类的实例(非静态)域的属性 ID。该域由其名称及签名指定。访问器函数的GetField 及 SetField
系列使用域 ID 检索对象域。GetFieldID() 不能用于获取数组的长度域。应使用GetArrayLength()。
参数: env:JNI 接口指针。
clazz:Java 类对象。
name: 该属性的Name名称
sig: 该属性的域签名。
返回值:属性ID。如果操作失败,则返回NULL。
抛出: NoSuchFieldError:如果找不到指定的域。
ExceptionInInitializerError:如果由于异常而导致类初始化程序失败。
OutOfMemoryError:如果系统内存不足。

4.2 通过JNI本地函数生成java对象
1. 查找指定的类,并将查找的类赋值给jclass类型的变量
2. 查找java类构造方法的ID值【类型为jmethodID】
3. 生成java对象

JNI本地函数生成的java对象有局部引用和全局引用之分:

局部引用是JNI默认的,它仅在JNI本地函数内才有效,即当JNI本地函数返回后,其内部的引用就会失效,下次java中拿到的局部引用就会为null。也就是局部引用只能使用一次而后就会变为null。
全局引用就是说可以只使用JNI本地函数创建对象赋值一次,那么在同一类中就可以使用。

下面看下JNI中的局部引用和全局引用的本地函数都有哪些:

jclass FindClass (JNIEnv *env, const char *name);
功能: 该函数用于加载本地定义的类。它将搜索由CLASSPATH 环境变量为具有指定名称的类所指定的目录和 zip文件。
参数:env JNI 接口指针。
name 类全名(即包名后跟类名,之间由”/”分隔).如果该名称以“[(数组签名字符)打头,则返回一个数组类。
返回值:返回类对象全名。如果找不到该类,则返回 NULL。
抛出: ClassFormatError 如果类数据指定的类无效。
ClassCircularityError 如果类或接口是自身的超类或超接口。
NoClassDefFoundError 如果找不到所请求的类或接口的定义。
OutOfMemoryError 如果系统内存不足。

jclass GetObjectClass (JNIEnv *env, jobject obj);
功能:通过对象获取这个类。该函数比较简单,唯一注意的是对象不能为NULL,否则获取的class肯定返回也为NULL。
参数: env JNI 接口指针。
obj Java 类对象实例。

jobject NewGlobalRef (JNIEnv *env, jobject obj);
功能:创建 obj 参数所引用对象的新全局引用。obj 参数既可以是全局引用,也可以是局部引用。全局引用通过调用
DeleteGlobalRef() 来显式撤消。
参数:env JNI 接口指针。
obj 全局或局部引用。
返回值: 返回全局引用。如果系统内存不足则返回 NULL。

void DeleteGlobalRef (JNIEnv *env, jobject globalRef);
功能: 删除 globalRef 所指向的全局引用。
参数: env JNI 接口指针。
globalRef 全局引用。

如上还提供了一个DeleteGlobalRef这个本地函数这个是用来删除使用完的全局引用的。


(五)直接注册JNI本地函数

我们知道java虚拟机在运行包括本地方法的java应用程序的时候,至少是要经过下面连个步骤的:
1. 调用S有stem.loadLibrary()方法,将包括本地方法具体实现的C/C++运行库加载到内存中。‘’
2. java虚拟机检索加载进来的库函数符号,在其中查找与java本地方法拥有相同签名的JNI本地函数符号,若是找到一致的,则将本地方法映射到具体的JNI本地函数

若是JNI支持的本地函数仅仅只有一个,java虚拟机在将本地方法与C运行库中的JNI本地函数映射在一起时,不会耗费太长时间。但是在Android framework这类负责系统下,拥有大量的包含本地方法的java类,java虚拟机加载相应运行库,再逐一检索,将各个本地方法与相应的函数映射起来,这显然会增加运行时间,降低运行的效率。

为了解决这一问题,JNI机制提供了名为RegisterNatives的JNI函数,该函数允许C/C++开发者将JNI本地函数与java类的本地方法直接映射在一起。当不调用RegisterNatives函数时,java虚拟机会自动检索并将JNI本地函数与相应的java本地方法链接在一起。但是当开发者直接调用RegisterNatives函数直接映射时,java虚拟机 就不必进行映射处理,这会极大地提高运行速度,提升运行效率。由于程序员直接将JNI函数与java方法链接在一起,在加载运行库时,java虚拟机不必为了识别JNI本地函数而将JNI函数的名称与JNI支持的命名规则进行比对,即任何名称的函数都能直接链接到java本地方法上。

如下代码中JNI_OnLoad函数就是进行直接注册的本地函数。用法如下:

/**
 * 如果要实现动态注册,这个方法一定要实现
 * 动态注册工作在这里进行
 */
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env = NULL;
    jint result = -1;

    if ((*vm)-> GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        return -1;
    }
    assert(env != NULL);

    if (!registerNatives(env)) { //注册
        return -1;
    }
    result = JNI_VERSION_1_4;

    return result;

}

稍微解释下,如果要实现动态直接注册就必须实现JNI_OnLoad方法,这个是JNI的一个入口函数,我们在Java层通过System.loadLibrary加载完动态库后,紧接着就会去查找一个叫JNI_OnLoad的方法。如果有,就会调用它,而动态注册的工作就是在这里完成的。在这里我们会去拿到JNI中一个很重要的结构体JNIEnv,env指向的就是这个结构体,通过env指针可以找到指定类名的类,并且调用JNIEnv的RegisterNatives方法来完成注册native方法和JNI函数的对应关系。


(六)使用Android NDK开发步骤

  1. 创建Android工程
  2. javac 编译含有本地方法的.java文件
  3. javah 生成JNI本地函数模型
  4. 编写.c文件根据函数模型实现本地方法、
  5. 编写Android.mk文件【向NDK编译系统提供创建本地库所需要的各种信息】
  6. 在cygwin shell进入到project根目录下执行 ndk-build
  7. 运行Android程序
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值