JNI:Java Native Interface,java本地开发接口。Java和C/C++的通信接口,是一个用来沟通Java和本地代码(C/C++)的协议。
编译环境:android studio 3.1.4,ndk17
Java类型 |
JNI类型 |
C/C++类型 |
大小 |
boolean |
Jboolean |
unsigned char |
无符号8位 |
Byte |
jbyte |
char |
有符号8位 |
Char |
jchar |
unsigned short |
无符号16位 |
Short |
jshort |
short |
有符号16位 |
Integer |
Jint |
int |
有符号32位 |
Long |
jlong |
long long |
有符号64位 |
Float |
jfloat |
float |
32位浮点值 |
Double |
jdouble |
double |
64位双精度浮点值 |
与java基本类型不同,引用类型对开发人员是不透明的。java的内部数据结构并不直接向原生代码公开。也就是说C/C++代码不能直接访问java代码的字段和方法。
Java类型 | C/C++类型 |
java.lang.Class | jclass |
java.lang.Throwable | jthrowable |
java.lang.Object | jobject |
java.util.Objects | jobjects |
java.lang.Object[] | jobjectArray |
Boolean[] | jbooleanArray |
Byte[] | jbyteArray |
Char[] | jcharArray |
Short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
通用数组 | jarray |
注:任何java数组在JNI里面都可以使用jarray来表示,例如:java的int[]数组,可以表示为jintArray,也可以表示为jarray。
JNI获取java的方法ID和字段ID,都需要一个很重要的参数,就是java的方法和字段的签名,这个签名需要通过下面的表来获取,这个表很重要,建议最好记住。
Java类型 | 签名 |
Boolean | Z |
Byte | B |
Char | C |
Short | S |
Integer | I |
Long | J |
Float | F |
Double | D |
Void | V |
任何Java类的全名 | L任何java类的全名; 比如:Java String类对应的签名是Ljava/lang/String; |
type[] | type[ 这个就是java数组的签名,比如Java int[]的签名是[I,java long[]的签名是[J,Java String[]的签名是[Ljava/lang/String; |
方法类型 | (参数类型)返回值类型; 比如java方法void hello(String msg, String msg2)对应的签名就是(Ljava/lang/String; Ljava/lang/String;)V 比如java方法String getNewName(String Name)对应的签名是(Ljava/lang/String;)Ljava/lang/String; 比如Java方法long add(int a, int b)对应的签名是(II)J |
Android提供了3个使用的函数用于加载JNI库,分别是System.loadLibrary(libname), Runtime.getRuntime().loadLibrary(libname),以及Runtime.getRuntime().load(libFilePath)。
用System.LoadLibrary(libname)和Runtime.getRumtime().loadLibrary()这两个函数加载so库,不需要指定so库的路径,Android会默认从系统的共享目录中去查找,Android的共享目录是Vendor/lib和System/lib,如果共享库路径找到指定名字的so库,则立即加载这个so库。所以我们给so库起名时要尽量避免与android共享库里面的so库重名。如果在共享目录库中找不到,则在APP的安装目录里面查找APP的私有so库。如果找到,则加载该so库。
用Runtime.getRumtime().load(libFilePath)加载so库,需要指定完整的so库路径,优点就是加载速度快,并且不会加载错误的so库,缺点就是需要指定完整的so库路径,使用的时候不大方便,因此大家常用的加载so库的方式还是用loadLibrary函数加载。
1.在java类中声明native方法
2.使用javah命令生成native方法的头文件
3.新建.cpp文件实现native方法
1.在Java目录下新建jniUtils目录,然后在该目录下新建HelloJni.java文件,并声明native方法。
public class HelloJni {
static {
System.loadLibrary("JniTestSample");
}
public static native String getJniString();
}
2.通过javah命令生成头文件。用AS的Terminal或Windows的cmd.exe,进入项目的Java目录。例如:cd E:\Android\AndroidProject\TestJni\app\src\main\java 然后用javah命令生成头文件,javah -jni com.example.liuz4.testjni.JniUtil.HelloJni 。生成的头文件在Java目录下。头文件com_example_liuz4_testjni_JniUtil_HelloJni.h
3.头文件的命名格式是:包名+类名,打开头文件,JNI调用的C语言函数是有固定格式的:java_包名_类名(JNIEnv *,)。
#include
/* Header for class com_example_liuz4_testjni_JniUtil_HelloJni */
#ifndef _Included_com_example_liuz4_testjni_JniUtil_HelloJni
#define _Included_com_example_liuz4_testjni_JniUtil_HelloJni
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_liuz4_testjni_JniUtil_HelloJni
* Method: getJniString
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_liuz4_testjni_JniUtil_HelloJni_getJniString
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
4.右键点击module,新建jni目录
5.将c++依赖的头文件拷贝到jni目录下,并在该目录下新建C++源文件。例如:Hello_Jni.cpp
#include "com_example_liuz4_testjni_JniUtil_HelloJni.h"
JNIEXPORT jstring JNICALL Java_com_example_liuz4_testjni_JniUtil_HelloJni_getJniString
(JNIEnv *env, jclass obj){
return env->NewStringUTF("hello JNI ...");
}
1、修改c++源码对应module的build.gradle文件,在android{}中增加如下内容:
android {
...
defaultConfig {
...
externalNativeBuild{
cmake{
cppFlags ""
//生成多个版本的so文件
abiFilters 'arm64-v8a', 'armeabi-v7a'
}
}
...
}
...
externalNativeBuild {
cmake {
path 'CMakeLists.txt'
}
}
}
2、CMakeLists.txt文件
# Sets the minimum version of CMake required to build your native library.
# This ensures that a certain set of CMake features is available to
# your build.
cmake_minimum_required(VERSION 3.4.1)
# Specifies a library name, specifies whether the library is STATIC or
# SHARED, and provides relative paths to the source code. You can
# define multiple libraries by adding multiple add.library() commands,
# and CMake builds them for you. When you build your app, Gradle
# automatically packages shared libraries with your APK.
add_library( # Specifies the name of the library.
JniTestSample
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/jni/Hello_Jni.cpp)
# Specifies a path to native header files.
# include_directories(src/main/jni/)
find_library( # Defines the name of the path variable that stores the
# location of the NDK library.
log-lib
# Specifies the name of the NDK library that
# CMake needs to locate.
log )
# Links your native library against one or more other native libraries.
target_link_libraries( # Specifies the target library.
JniTestSample
# Links the log library to the target library.
${log-lib} )
其中:
add_library函数用来配置要生成的so库的基本信息,比如so库的名字,生成静态库还是共享的,以及so库的c/c++源文件列表。add_library函数参数说明:第一个参数是so库名字;第二个参数是生成的so库的类型,静态so库是STATIC,共享so库是SHARED;第三个参数是C/C++源文件,可以包含多个源文件。
find_library函数用来从NDK目录下面查找特定的so库。find_library参数说明:第一个参数是我们给查找到的so库取个自己的名字,名字可以随便写;第二个参数是要查找的so库的名字,这个名字是从真实的so库的名字去掉lib前缀和.so后缀的名字,例如liblog.so的名字就是log。
target_link_libraries函数用来把要生成的so库和依赖的其它so库进行链接,生成我们需要的so库文件。target_link_libraries参数说明:第一个参数是我们要生成的so库的名字去掉前缀和后缀后的名字;后面参数是需要生成的so库所要依赖的库。
include_directories设置so库的头文件目录
传统的关于android使用JNI调用C/C++程序,首先javah 生产头文件,然后拷贝头文件里面的方法到C文件中进行映射调用。使用这种方法有两个缺陷:
1、这种方法生成的映射方法名不太规则也比较长
2、调用数据较慢
因此可以使用JNI动态注册方法的方式来解决这两个问题
1、JNI_OnLoad()方法:当java层代码执行System.loadLibrary,Native中的JNI_OnLoad方法被调用,此时可以注册对应于Java层调用的navtive方法。
2、JNINativeMethod结构体:JNINativeMethod包含三个元素: 方法名, 方法签名, native函数指针。该结构体用于描述需要注册的方法信息。
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
3、RegisterNatives方法:该方法是JNI环境提供的用于注册Native方法的方法。
4.3.1 Java代码实现
不需要调整,与传统调用方法一致。
4.3.2 C++代码实现
1、不需要用javah生成头文件。因为实现java的native方法,可以随意定义方法名,不用按固定格式进行命名。
2、定义JNINativeMethod数组,将native方法与c++实现的函数关联起来。
其中getJniString为java中待实现的native方法名。()Ljava/lang/String; 为方法的签名, ()表示该方法无参数, Ljava/lang/String;表示返回值为Java中的String类型。
3、实现源码示例:
#include
// 指定要注册的类,对应完整的java类名
#define JNIREG_CLASS "com/example/liuz4/testjni/JniUtil/HelloJni"
#ifndef NELEM
#define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
#endif
JNIEXPORT jstring JNICALL nativeHello(JNIEnv *env, jclass obj){
return env->NewStringUTF("this is regist Native JNI ...");
}
// Java和JNI函数的绑定表
static JNINativeMethod method_table[] = {
{ "getJniString", "()Ljava/lang/String;", (void*)nativeHello },//绑定
};
// 注册native方法到java中
static int registerNativeMethods(JNIEnv* env, const char* className,
JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
clazz = env->FindClass(className);
if (clazz == NULL) {
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return result;
}
registerNativeMethods(env, JNIREG_CLASS,
method_table, NELEM(method_table));
// 返回jni的版本
return JNI_VERSION_1_4;
}
JNIEXPORT void JNI_OnUnload(JavaVM *jvm, void *reserved){
}
本文的基础知识介绍借鉴于https://blog.csdn.net/kgdwbb/article/details/72810251