Android NDK开发之JNI基础

前言

以前写了一篇文章简单的介绍了Android NDK的组件和结构,以及在Android studio中开发NDK,NDK是Android底层的c/c++库,然而要在java中调用c/c++的原生功能,则须要使用JNI来实现。java

什么是JNI

JNI(Java Native Interface)是java本地接口,它主要是为了实现Java调用c、c++等本地代码所封装的一层接口。你们都知道java是跨平台开发语言,它的狂平台特性致使与本地交互的能力不够强大,一些和操做系统相关的特性Java没法完成,因此Java提供了JNI用于和Native代码进行交互。经过JNI,Java能够调用c、c++,相反,c、c++也能够调用Java的相关代码。c++

建立NDK工程

开发环境

  • Mac
  • Android studio:3.3.2

新建工程

本地的Android studio版本为3.3.2,当你建立项目的时候有一个选项是选择Native C++的模板
数组

点击next,配置项目的信息app

点击next,选择使用哪一种C++标准,选择Toolchain Default会使用默认的CMake设置便可。
函数

点击finish便可完成工程的建立。性能

工程结构

这时候主工程目录下会有cpp文件夹和.externalNativeBuild文件夹。
gradle

.externalNativeBuild文件夹:用于存放cmake编译好的文件,包括支持的各类硬件等信息,有点相似于build.gradle文件明确Gradle如何编译APP;
cpp文件夹:存放C/C++代码文件,native-lib.cpp文件默认生成的;ui

cpp文件夹下有两个文件,一个是native-lib.cpp文件,一个是CMakeLists.txt文件。CMakeLists.txt文件是cmake脚本配置文件,cmake会根据该脚本文件中的指令去编译相关的C/C++源文件,并将编译后产物生成共享库或静态块,而后Gradle将其打包到APK中。this

CMakeLists.txt的相关配置以下:编码

# 设置构建本地库所需的最小版本的cbuild。
cmake_minimum_required(VERSION 3.4.1)
# 建立并命名一个库,将其设置为静态
# 或者共享,并提供其源代码的相对路径。
# 您能够定义多个库,而cbuild为您构建它们。
# Gradle自动将共享库与你的APK打包。
add_library( native-lib       #设置库的名称。即SO文件的名称,生产的so文件为“libnative-lib.so”,                                在加载的时候“System.loadLibrary("native-lib");”
             SHARED            # 将库设置为共享库。
             native-lib.cpp    # 提供一个源文件的相对路径
             helloJni.cpp      # 提供同一个SO文件中的另外一个源文件的相对路径
           )
# 搜索指定的预构建库,并将该路径存储为一个变量。由于cbuild默认包含了搜索路径中的系统库,因此您只须要指定您想要添加的公共NDK库的名称。cbuild在完成构建以前验证这个库是否存在。
find_library(log-lib   # 设置path变量的名称。
             log       #  指定NDK库的名称 你想让CMake来定位。
             )
#指定库的库应该连接到你的目标库。您能够连接多个库,好比在这个构建脚本中定义的库、预构建的第三方库或系统库。
target_link_libraries( native-lib    # 指定目标库中。与 add_library的库名称必定要相同
                       ${log-lib}    # 将目标库连接到日志库包含在NDK。
                       )
#若是须要生产多个SO文件的话,写法以下
add_library( natave-lib       # 设置库的名称。另外一个so文件的名称
             SHARED           # 将库设置为共享库。
             nataveJni.cpp    # 提供一个源文件的相对路径
            )
target_link_libraries( natave-lib     #指定目标库中。与 add_library的库名称必定要相同
                       ${log-lib}     # 将目标库连接到日志库包含在NDK。
                        )

build.gradle中有CMake的相关配置

代码结构

java调用c、c++代码分为三个步骤:

  1. 加载so库
  2. 编写java函数
  3. 编写c函数

在MainActivity.java,static{}语句中使用了加载so库,此语句在类加载中只执行一次。

static {
        System.loadLibrary("native-lib");
}

而后,编写了原生的函数,函数名中要带有native。

public native String stringFromJNI();

最后,编写相对应的c函数,注意函数名的构成Java_com_example_myapplication_MainActivity_stringFromJNI为Java_加上包名、类型、方法名的下划线连成一块儿。

native-lib.cpp文件

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

这就是一个JNI方法调用示例。

虽然Java函数不带参数,可是原生方法却带了两个参数,第一个参数JNIEnv是指向可用JNI函数表的接口指针,第二个参数jobject是Java函数所在类的实例的Java对象引用。

JNIEnv接口指针

原生代码(c)经过JNIEnv接口指针提供的各类函数来使用虚拟机的功能,JNIEnv是一个指向线程-局部数据的指针,线程-局部数据中包含指向函数表的指针。

原生代码是c与原生代码是c++的调用JNI函数的语法不一样,在c代码中,JNIEnv是指向JNINativeInterface结构的指针,而在c++代码中,JNIEnv是c++类实例,这两种方式调用函数的方式是不同的。例如:

c代码中:

(*env)->NewStringUTF(env,"Hello from JNI");

c++代码中:

env->NewStringUTF("Hello from JNI");

实例方法与静态方法

Java程序设计有两类方法,实例方法和静态方法。实例方法与类实例相关,只能在类实例中调用。静态方法不与类死里相关,它们能够在静态上下文中直接调用。在原生代码中能够获取Java类的实例引用和类引用。例如:

类实例引用

extern "C" JNIEXPORT void JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject  thiz) {
}

类引用

extern "C" JNIEXPORT void JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
        JNIEnv *env,
        jclass  clazz) {
}

从函数中看出来,JNI提供了本身的数据类型从而让原生代码了解Java数据类型。

JNI数据类型

JNI的数据类型包含两种:基本类型和引用类型。与Java数据类型的对应关系以下:

基本数据类型:

| JNI类型 | Java类型 |
| ------ | ------ |
| jboolean | boolean |
| jbyte | byte |
| jchar | char |
| jshort | short |
| jint | int |
| jlong | long |
| jfloat | float |
| jdouble | double |
| void | void |

引用类型:

| JNI类型 | Java类型 |
| ------ | ------ |
| jobject | Object |
| jclass | Class |
| jstring | String |
| jobjectArray | Object[] |
| jbooleanArray | boolean[] |
| jbyteArray | char[] |
| jshortArray | short[] |
| jintArray | int[] |
| jlongArray | long[] |
| jfloatArray | float[] |
| jdoubleArray | double[] |
| jthrowable | Throwable |

引用数据类型的操做

JNI提供了与引用类型密切相关的一组API,这些API经过JNIEnv接口指针提供给原生函数。例如:

  • 字符串
  • 数组
  • NIO缓冲区
  • 字段
  • 方法

字符串操做

JNI把Java字符串当成引用类型处理,提供了Java与c字符串之间相互转换的必要函数,因为Java字符串对象是不可变得,因此JNI不提供修改现有Java字符串内容的函数。

建立字符串

能够在原生代码中使用NewString函数构建Unicode编码格式的字符串实例,也能够中NewStringUTF函数构建UTF-8编码格式的字符串实例,这些函数以C字符串为参数,并返回一个Java字符串引用类型jstring值。例如:

jstring javaStr = (*env)->NewStringUTF(env,"Hello");

把Java字符串转换成C字符串

为了在原生代码中使用Java字符串,须要将Java字符串转换成C字符串。用GetStringChars函数能够将Unicode格式的Java字符串转换成C字符串,用GetStringUTFChars函数能够将UTF-8格式的Java字符串转换成C字符串。例如:

const jbyte* str
jboolean isCopy;
str = (*env)->GetStringUTFChars(env,javaString,&isCopy);

释放字符串

经过JNI GetStringChars函数和GetStringUTFChars函数得到的C字符串在原生代码中使用完后要释放,不然会引发内存泄漏。JNI提供了ReleaseStringChars函数和ReleaseStringUTFChars函数来释放Unicode编码和UTF-8编码格式的字符串。例如:

(*env)->ReleaseStringUTFChars(env,javaString,str);

数组操做

建立数组

用New"Type"Array函数在原生代码中建立数组实例,其中"Type"能够是Int、Char等类型,例如:

jintArray javaArray = (*env)->NewIntArray(env,10);

访问数组元素

将数组的代码复制成C数组或者让JNI提供直接指向数组元素的指针方式来访问Java数组元素。

对副本的操做

Get"Type"ArrayRegion函数将给定的基本Java数组复制到给定的C数组中,例如:

jint nativeArray[10];
    (*env)->GetIntArrayRegion(env,javaArray,0,10,nativeArray);

原生代码可使用和修改数组元素,使用Set"Type"ArrayRegion函数将C数组复制回Java数组中。例如:

(*env)->SetIntArrayRegion(env,javaArray,0,10,nativeArray);

NIO操做

JNI提供了在原生代码中使用NIO的函数,与数组操做相比,NIO性能较好,更适合在原生代码和Java应用程序之间传送大量数据。

建立直接字节缓冲区

unsigned char* buffer = (unsigned char*) malloc(1024);
jobject directBuffer = (*env)->NewDirectByteBuffer(env,buffer,1024);

注意:原生函数应用经过释放未使用的内存分配以免内存泄漏。

获取直接字节缓冲区

unsigned char* buffer;
buffer = (unsigned char*)(*env)->GetDirectBufferAddress(env,directBuffer);

访问域

Java有两类域:实例域和静态域,这两个的区别就是有没有static声明静态。

获取域ID

JNI提供了用域ID访问两类域的方法,能够经过给定实例的class对象获取域ID,用GetObjectClass函数来获取class对象。例如:

jclass clazz = (*env)->GetObjectClass(env,instance);

用GetFieldId函数来获取实例域。

jfieldId instanceFieldId = (*env)->GetFieldId(env,clazz,"instanceField","Ljava/lang/String");

用GetStaticFieldId获取静态域ID。

jfieldID staticFieldId = (*env)->GetStaticFieldID(env,clazz,"staticField","Ljava/lang/String");

其中最后一个参数是Java中表示域类型的域描述符,"Ljava/lang/String"代表域类型是String。

获取域

得到域ID以后能够用Get"Type"Field函数获取实际的实例域。例如:

jstring instanceField = (*env)->GetObjectField(env,instance,instanceFieldId);

用GetStatic"Type"Field函数得到静态域。例如:

jstring staticField = (*env)->GetStaticObjectField(env,clazz,staticFieldId);

调用方法

与域相似,Java中有两类方法:实例方法和静态方法。

获取方法ID

JNI提供了用方法ID访问两类方法的途径,能够用给定实例的class对象获取方法ID,用GetMethodID函数得到实例方法的方法ID。例如:

jmethodID instanceMethodId = (*env)->GetMethodID(env,clazz,"instanceMethod","()Ljava/lang/String;");

用GetStaticMethodID函数得到静态域的方法ID,例如:

jmethodID staticMethodId=(*env)->GetStaticMethodID(env,clazz,"staticMethod","()Ljava/lang/String;");

调用方法

以方法ID为参数经过Call"Type"Method类函数调用实际的实例方法。例如:

jstring instanceMethodResult = (*env)->CallStringMetthod(env,instance,instanceMethodId);

用CallStatic"Type"Field类函数调用静态方法,例如:

jstring staticMethodResult = (*env)->CallStaticStringMethod(env,clazz,staticMethodId);

域和方法描述符

在上面获取域ID和方法ID均分别须要域描述符和方法描述符,域描述符和方法描述符能够经过下表Java类型签名映射获取:

| Java类型 | 签名|
| ------ | ------ |
| Boolean | Z |
| Byte | B |
| Char | C |
| Short | S |
| Long | J |
| Int | I |
| Float | F |
| Double | D |
| void | V |
| fully-qualified-class | Lfully-qualified-class |
| type[] | [type |
| method type | (arg-type)ret-type |

类的签名采用"L+包名+类名+;"的形式,将其中的.替换为/便可,好比java.lang.String,它的签名为Ljava/lang/String;数组的签名就是[+类型签名,好比int数组,签名就是[I,多维数组就是[[I。方法的签名为(参数类型签名)+返回值类型签名,例如:boolean fun1(int a,double b,int[] c),其中参数类型的签名为ID[I,返回值类型的签名为Z,因此这个方法的签名就是(ID[I)Z。

相关文章
相关标签/搜索