在Android中使用JNI
在Android中使用JNIJNI简介JNI是Java Native Interface的缩写,使用JNI能够使运行在Java虚拟机上的程序和本地程序互相调用,本地程序可以是其它语言编写的,如C、C++ 或者汇编语言。当程序无法完全使用Java编写时(例如需要调用C/C++的库、与硬件进行交互、提高程序的性能、提高安全性防止反编译),可以通过JNI来编写本地方法。JNI还可以用于修改现有的本地..
在Android中使用JNI
JNI简介
JNI是Java Native Interface的缩写,使用JNI能够使运行在Java虚拟机上的程序和本地程序互相调用,本地程序可以是其它语言编写的,如C、C++ 或者汇编语言。当程序无法完全使用Java编写时(例如需要调用C/C++的库、与硬件进行交互、提高程序的性能、提高安全性防止反编译),可以通过JNI来编写本地方法。JNI还可以用于修改现有的本地程序,使它们可以通过Java来访问。
JNI简单使用示例
1、Java层声明本地方法;
编写Hello.java文件,用native声明本地方法,用System.loadLibrary加载需要调用的so库。
package com.example.jnitest;
public class Hello {
static {
//加载本地方法库
System.loadLibrary("hello");
}
//用native修饰本地方法
public native String helloFromNative();
}
2、Native层关联Java层本地方法,并编写具体实现;
Native层关联Java方法有两种方法,分为静态注册和动态注册
2.1 使用静态注册的方法关联Java层本地方法
静态注册需要根据函数名字搜索对应的JNI层函数来建立关联,可以用javah命令生成包含本地方法名的.h头文件,之后在C/C++中引入头文件并实现头文件中的函数即可。
使用javah命令生成本地方法对应的.h头文件。
cd app/src/main/java
#将java文件编译为class文件
javac com/example/jnitest/Hello.java
#在jni目录下生成对应的头文件
javah -d jni com.example.jnitest.Hello
编写hello.cpp文件,引入生成的头文件,实现头文件中的方法;
#include "com_example_jnitest_Hello.h"
JNIEXPORT jstring JNICALL Java_com_example_jnitest_Hello_helloFromNative
(JNIEnv *env, jobject obj){
return env->NewStringUTF("Hello From Native");
}
2.2 使用动态注册的方法关联Java层本地方法
动态注册不用生成.h头文件,但必须实现JNI_OnLoad回调函数,动态注册的工作就是在这里完成的。Java层执行System.loadLibrary加载完动态库后,JNI_OnLoad方法会被调用,可以在该方法中通过调用RegisterNatives方法完成注册操作。
编写hello.cpp文件实现本地方法;
// jni头文件
#include <jni.h>
#include <cassert>
#include <cstdlib>
#include <iostream>
using namespace std;
/*native 方法实现
方法名无要求,但要保证方法的返回值和参数格式与java层保持一致,
返回值使用与java中返回值对应的jni类型,具体对应关系参见下面的Jni数据类型章节;
native方法参数最少有2个:JNIEnv、jobect或jclass,其中JNIEnv是固定的,jobject和jclass如果java方法是实例方法则为jobject,如果java方法是静态方法则为jclass;剩下的参数与java方法中的参数保持一致,具体对应关系参见下面的Jni数据类型章节。
*/
jstring native_hello(JNIEnv *env, jobject obj){
return env->NewStringUTF("Hello From Native");
}
/*将需要注册的函数列表,放在JNINativeMethod 类型数组中
以后如果需要增加函数,只需在这里添加就行了
参数:
1.java代码中用native关键字声明的函数名
2.方法描述符(方法签名,包含参数类型和返回值类型),具体规则见下面的Jni描述符章节
3.C/C++中对应函数的函数名
*/
static JNINativeMethod getMethods[] = {
{ "helloFromNative", "()Ljava/lang/String;", (void*)native_hello},
};
//此函数通过调用JNI中 RegisterNatives 方法注册函数
static int registerNativeMethods(JNIEnv* env, const char* className,JNINativeMethod* getMethods,int methodsNum){
jclass clazz;
//找到声明native方法的Java类
clazz = env->FindClass(className);
if(clazz == NULL){
return JNI_FALSE;
}
//注册函数 参数:java类 所要注册的函数数组 注册函数的个数
if(env->RegisterNatives(clazz,getMethods,methodsNum) < 0){
return JNI_FALSE;
}
return JNI_TRUE;
}
static int registerNatives(JNIEnv* env){
//指定Java层的类描述符(具体规则见下面的Jni描述符章节),通过FindClass 方法来找到对应的类
const char* className = "com/example/jnitest/Hello";
return registerNativeMethods(env,className,getMethods, sizeof(getMethods)/ sizeof(getMethods[0]));
}
//回调函数,Java层调用System.loadLibrary后执行, 在这里面注册函数
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv* env = NULL;
//判断虚拟机状态是否有问题
if(vm->GetEnv((void**)&env,JNI_VERSION_1_6)!= JNI_OK){
return -1;
}
assert(env != NULL);
//开始注册函数, 调用顺序registerNatives -》registerNativeMethods -》env->RegisterNatives
if(!registerNatives(env)){
return -1;
}
//返回jni 的版本
return JNI_VERSION_1_6;
}
3、使用NDK编译C/C++文件生成so库
3.1、编写Android.mk和Application.mk文件;
#Android.mk
#Android.mk必须以LOCAL_PATH变量开头
LOCAL_PATH := $(call my-dir)
#清除除了LOCAL_PATH以外的LOCAL_<name>变量,例如LOCAL_MODULE与LOCAL_SRC_FILES等
include $(CLEAR_VARS)
#设置编译后生成的模块名
LOCAL_MODULE := hello
#需要编译的源文件
LOCAL_SRC_FILES := hello.cpp
#编译为共享库,即后缀名为.so
include $(BUILD_SHARED_LIBRARY)
#Application.mk
#设置NDK库函数版本号,一般和Android版本号对应
APP_PLATFORM = android-16
#设置需要编译的CPU类型,这里只编译armeabi-v7a和arm64-v8a两种,使用all可以编译所有类型
APP_ABI := armeabi-v7a arm64-v8a
#设置以静态链接方式连接C++标准库
APP_STL := c++_static
#设置编译版本,debug版本附带调试信息,支持gdb-server断点调试,release版本不带调试信息
APP_OPTIM := release
3.2、使用ndk-build命令编译生成动态链接库;
#需要将命令路径添加到Path环境变量中
ndk-build
生成的.so文件,在与jni目录同级的libs目录下
用AndroidStudio创建支持本地代码的项目
1、下载所需的组件,有CMake,LLDB,SDK,NDK;
2、新建项目,注意勾选Include C++ support;
3、生成的项目结构如下所示,本地代码文件在cpp目录下,并且多了一个CMakeLists.txt文件;
CMakeLists.txt文件中用CMake定义了本地代码的编译链接过程,Camke是一个跨平台的编译工具,AndroidStudio这里用它来编译本地代码,省去了Android.mk文件的编写,新建项目的CMakeLists.txt文件中有详细的注释,按照注释编写即可。
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp )
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib} )
4、打开cpp文件,编写代码,可以看到有代码提示;
5、运行项目,可以看到在app/build/intermediates/cmake/debug/obj目录下有生成的so库
JNI数据类型
由于Java层与C/C++层的数据类型是不一致的,互相之间无法直接识别传递的数据,因此JNI定义了一套数据类型,用于衔接Java和C/C++层。
1.基本数据类型
Java Type | Native Typ | Description |
---|---|---|
boolean | jboolean | unsigned 8 bits |
byte | jbyte | signed 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
int | jint | signed 32 bits |
long | jlong | signed 64 bits |
float | jfloat | 32 bits |
double | jdouble | 64 bits |
void | void | N/A |
2.引用类型
jobject (all Java objects)
|
|-- jclass (java.lang.Class objects)
|-- jstring (java.lang.String objects)
|-- jarray (array)
| |--jobjectArray (object arrays)
| |--jbooleanArray (boolean arrays)
| |--jbyteArray (byte arrays)
| |--jcharArray (char arrays)
| |--jshortArray (short arrays)
| |--jintArray (int arrays)
| |--jlongArray (long arrays)
| |--jfloatArray (float arrays)
| |--jdoubleArray (double arrays)
|
|--jthrowable
3.方法和属性ID
Jni调用Java中的方法的时,需要先通过env->GetMethodID方法获取它的id,再通过env->CallObjectMethodD调用;JNI获取Java中的属性时,也是先通过env->GetFieldID函数获取它的id,再通过env->GetObjectField获取;这些ID的结构体在jni.h中的定义如下:
struct _jfieldID; /* opaque structure */
typedef struct _jfieldID *jfieldID; /* field IDs */
struct _jmethodID; /* opaque structure */
typedef struct _jmethodID *jmethodID; /* method IDs */
JNI描述符
1. 类描述符
jni可以通过类描述符获取jclass对象,以实现对Java类的访问。如下所示,“com/example/jnitest/Hello”就是Hello类的描述符,规则就是将类的全路径名中的"."用“/”代替。
jclass clazz = env->FindClass(“com/example/jnitest/Hello”);
2. 数据类型描述符
对于基本数据类型的描述符定义如下:
Desciptor | Java Data Type |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | floa |
D | double |
对于引用类型描述符是以"L"开头以";“结尾,中间接类描述符或基本类型描述符,如果是数组类型则在前面加”[",二维数组则在前面加“[[”,以此类推。示例如下:
Desciptor | Java DataType |
---|---|
Ljava/lang/String; | String |
[Ljava/lang/Object; | Object[] |
[[I | int[][] |
3. 方法描述符
jni通过方法名和方法描述符(方法签名)关联java中的方法,方法描述符由参数和返回值两部分组成,参数由“()”表示,括号里是参数的类型描述符,“()”后面接返回值的类型描述符,V表示返回值为空。示例如下:
Method Descriptor | Java Method |
---|---|
“()Ljava/lang/String;” | String f(); |
“(ILjava/lang/Class;)J” | long f(int i, Class c); |
“([B)V” | void f(byte[] bytes); |
除此之外,还可以用javap命令查看指定类的方法描述符,对于不确定的方法描述符,可以用此命令确认。
JNI常见操作
Jni常见的函数都在**jni.h头文件中的JNINativeInterface_**结构体中有声明,jni.h文件在JAVA_HOME/include路径下,具体可以自行查看,这里只列出一些常见的操作。
1、在Native层返回一个字符串
Java层代码:
package com.example.jnitest
public class Hello{
...
//声明native方法,返回值类型为String
public native String getStringFromNative();
...
}
Native层代码:
...
//native层实现getStringFromNative方法,方法返回值类型为jstring
JNIEXPORT jstring JNICALL Java_com_example_jnitest_Hello_getStringFromNative(JNIEnv * env, jobject obj)
{
用env->newStringUTF方法构造一个jstring对象
jstring str = env->NewStringUTF("String from native");
return str ;
}
...
2、在Native层返回int型二维数组
Java层代码:
package com.example.jnitest
public class Hello{
...
//声明native方法,返回值类型为int[][], 参数a,b分别表示二维数组的两个维度值(int[a][b])
public native int[][] getIntTwoArrayFromNative(int a, int b);
...
}
Native层代码:
...
//native层实现getIntTwoArrayFromNative方法,方法返回值类型为jobjectArray
JNIEXPORT jobjectArray JNICALL Java_com_example_jnitest_Hello_getIntTwoArrayFromNative(JNIEnv * env, jobject obj, jint a, jint b)
{
//获得指向jintArray类的类引用
jclass intArrayObjectClass = env->FindClass("[I");
//构造一个指向jintArray类的对象数组,该对象数组初始大小为a
jobjectArray obejctArray = env->NewObjectArray(a ,intArrayObjectClass , NULL);
//构造b个jintArray对象,并将其引用赋值给obejctArray
for(int i=0;i<a;i++){
//构建jintArray对象
jintArray intArray = env->NewIntArray(b);
//初始化一个jint数组容器
jint temp[b];
//给jint数组中的元素赋值
for(int j=0;j<b;j++){
temp[j] = i + j;
}
//将jint数组中赋值给jitArray对象
env->SetIntArrayRegion(intArray, 0 , b ,temp);
//将jintArray对象赋值给obejctArray对象
env->SetObjectArrayElement(obejctArray , i ,intArray);
//删除局部引用
env->DeleteLocalRef(intArray);
}
return obejctArray;
}
...
3、 在Native层修改Java对象属性
Java层代码:
package com.example.jnitest
public class Hello {
private String name = "name at java";
...
//在Native层设置name属性值
public native void setNameFromNative(String name);
...
}
Native层代码 :
//在Native层操作Java对象,读取/设置属性等
JNIEXPORT void JNICALL Java_com_example_jnitest_Hello_setNameFromNative
(JNIEnv *env, jobject obj, jstring name) {
//获得Java层该对象实例的类引用,即Hello类引用
jclass cls = env->GetObjectClass(obj);
//获得name属性id
jfieldID nameFieldId = env->GetFieldID(cls , "name" , "Ljava/lang/String;");
if(nameFieldId == NULL){
cout << " name field not found\n";
return;
}
//获得name属性值
jstring javaNameStr = (jstring)env->GetObjectField(obj ,nameFieldId);
//转换为 char *类型
const char * c_javaName = env->GetStringUTFChars(javaNameStr , NULL);
string str_name = c_javaName;
//输出显示
cout << "the name from java is " << str_name << endl ;
//释放局部引用
env->ReleaseStringUTFChars(javaNameStr , c_javaName);
// 设置name字段的值
env->SetObjectField(obj , nameFieldId , name);
}
4、在Native层调用Java层方法
Java层代码:
package com.example.jnitest
public class Hello {
...
//Native层将会调用的方法
public void callback(){
System.out.println("I was invoked by native");
};
//在Native层调用callback()方法
public native void doCallBack();
}
Native层代码 :
//Native层实现doCallBack方法
JNIEXPORT void JNICALL Java_com_example_jnitest_Hello_doCallBack
(JNIEnv * env , jobject obj){
//获取Hello类的实例
jclass cls = env->GetObjectClass(obj);
//获取callback方法的id
jmethodID callbackID = env->GetMethodID(cls , "callback" , "()V") ;
if(callbackID == NULL){
cout << "callback method not found\n" << endl ;
return;
}
//调用callback方法
env->CallVoidMethod(obj , callbackID , NULL);
}
5、Java层传递复杂对象至Native层
创建一个Student类,包含name和age两个属性,带参数的构造方法
package com.example.jnitest
public class Student {
...
public String name;
public int age;
public Student(String name, int age){
this.name = name;
this.age = age;
}
...
}
Java层代码:
package com.example.jnitest
public class Hello {
...
//在Native层打印Student的信息
public native void printStuInfoAtNative(Student stu);
...
}
Native层该方法实现为 :
//在Native层实现printStuInfoAtNative方法,打印Student信息
//第二个jobject类实例obj_stu代表Java层传递下来的Student对象
JNIEXPORT void JNICALL Java_com_example_jnitest_Hello_printStuInfoAtNative
(JNIEnv * env, jobject obj, jobject obj_stu){
//获取Student类引用
jclass stu_cls = env->GetObjectClass(obj_stu);
if(stu_cls == NULL){
cout << "Student class not found\n" ;
return;
}
//获取Student类age属性id
jfieldID ageFieldID = env->GetFieldID(stu_cls, "age", "I");
//获取age属性值
jint age = env->GetIntField(obj_stu , ageFieldID);
//获取Student类name属性id
jfieldID nameFieldID = env->GetFieldID(stu_cls, "name", "Ljava/lang/String;");
//获取name属性值
jstring name = (jstring)env->GetObjectField(obj_stu , nameFieldID);
//转换成 char *
const char * c_name = env->GetStringUTFChars(name, NULL);
string str_name = c_name;
//释放引用
env->ReleaseStringUTFChars(name, c_name);
//输出Student类中name和age属性值
cout << " age is :" << age << " # name is " << str_name << endl ;
}
6、在Native层返回一个复杂对象
Java层代码:
package com.example.jnitest
public class Hello {
...
//在Native层返回一个Student对象
public native Student getStudentFromNative() ;
...
}
Native层代码:
//返回一个Student对象,对应的返回类型为jobject
JNIEXPORT jobject JNICALL Java_com_example_jnitest_Hello_getStudentFromNative
(JNIEnv * env, jobject obj) {
//通过类描述符,获取Student类引用
jclass stu_cls = env->FindClass("Lcom/example/jnitest/Student;");
//获取Student类的构造函数id, 构造函数名为<init>,返回类型为void即V
jmethodID stu_construct_id = env->GetMethodID(stu_cls, "<init>", "(Ljava/lang/String;I)V");
//初始化name和age值
jstring name = env->NewStringUTF("zhangsan");
jint age = 11;
//调用构造函数,传入name和age,构造一个Student对象
jobject stu_ojb = env->NewObject(stu_cls, stu_construct_id, name, age);
return stu_ojb ;
}
7、在Native层返回集合对象
Java代码:
package com.example.jnitest
public class Hello {
...
//在Native层返回Student集合
public native ArrayList<Student> getStudentListFromNative();
...
}
Native层代码 :
//返回Student集合
JNIEXPORT jobject JNICALL Java_com_example_jnitest_Hello_native_getStudentListFromNative
(JNIEnv * env, jobject obj) {
//获得ArrayList类引用
jclass list_cls = env->FindClass("Ljava/util/ArrayList;");
if(list_cls == NULL){
cout << "Ljava/util/ArrayList; not found\n" ;
return NULL;
}
//获得得构造函数Id
jmethodID list_costruct_id = env->GetMethodID(list_cls , "<init>","()V");
//创建一个Arraylist对象
jobject list_obj = env->NewObject(list_cls , list_costruct_id);
//获取Arraylist类的add()方法ID, 其方法原型为boolean add(Object object)
jmethodID list_add_id = env->GetMethodID(list_cls, "add", "(Ljava/lang/Object;)Z");
//获得Student类引用
jclass stu_cls = env->FindClass("Lcom/example/jnitest/Student;");
//获取Student类的构造函数id, 构造函数名为<init>,返回类型为void即V
jmethodID stu_construct_id = env->GetMethodID(stu_cls, "<init>", "(Ljava/lang/String;I)V");
for(int i = 0 ; i < 3 ; i++){
jstring name = env->NewStringUTF("zhangsan");
jint age = i;
//调用Student类构造函数构造Student实例
jobject stu_obj = env->NewObject(stu_cls , stu_construct_id , name, age);
//调用Arraylist类add方法,添加一个Student对象
env->CallBooleanMethod(list_obj, list_add_id, stu_obj);
}
return list_obj ;
}
更多推荐
所有评论(0)