在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 TypeNative TypDescription
booleanjbooleanunsigned 8 bits
bytejbytesigned 8 bits
charjcharunsigned 16 bits
shortjshortsigned 16 bits
intjintsigned 32 bits
longjlongsigned 64 bits
floatjfloat32 bits
doublejdouble64 bits
voidvoidN/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. 数据类型描述符

对于基本数据类型的描述符定义如下:

DesciptorJava Data Type
Zboolean
Bbyte
Cchar
Sshort
Iint
Jlong
Ffloa
Ddouble

对于引用类型描述符是以"L"开头以";“结尾,中间接类描述符或基本类型描述符,如果是数组类型则在前面加”[",二维数组则在前面加“[[”,以此类推。示例如下:

DesciptorJava DataType
Ljava/lang/String;String
[Ljava/lang/Object;Object[]
[[Iint[][]

3. 方法描述符

jni通过方法名和方法描述符(方法签名)关联java中的方法,方法描述符由参数和返回值两部分组成,参数由“()”表示,括号里是参数的类型描述符,“()”后面接返回值的类型描述符,V表示返回值为空。示例如下:

Method DescriptorJava 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 ;

}
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐