在Android中使用JNI

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gogo_wei/article/details/82810865

在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 ;

}

猜你喜欢

转载自blog.csdn.net/gogo_wei/article/details/82810865