前言
JNI学习总结(基础篇)里面总结了关于JNI和NDK的一些基本概念以及开发流程。本文中学习一下里面涉及到的具体知识。
JNI基础知识
如上篇文中的介绍,如果你用的是2.2之后版本并勾选了"Include C++ support",Android Studio会自动在cpp文件夹下创建了一个名为native-lib.cpp的C++文件,代码如下:
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring JNICALL Java_com_android_peter_jnidemo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
示例代码很简单,我们来逐行分析一下。
#include <jni.h>
#include <string>
学过C++的都知道,这个表示引用的其他文件以便告诉编译器你所调用的方法来自哪,等同于Java中的Import。
extern "C"{}
这个宏定义是必须的,他指定extern "C"内部的函数采用C语言的命名风格来编译。否则当JNI采用C++来实现时,由于C和C++编译过程中对函数命名风格不同,将导致JNI在链接时无法根据函数名查找到具体的函数,调用的时候会抛出异常(No implementation found)。
JNIEXPORT jstring JNICALL Java_com_android_peter_jnidemo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject instance) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
JNIEXPORT 和JNICALL :JNI中定义的宏,可以在jni.h文件中查找到。
-
jstring:参数类型,对应于Java中的string类型,Java和JNI中数据类型对应关系如下:
JNI的数据类型签名:标识一个特定的Java类型,这个类型既可以是类也可以是方法,也可以是数据类型。
(1)基本数据类型的签名基本都是单词的首字母,但是boolean和long除外,因为B已经被byte占用,而long的表示也被Java类签名占用,所以不同。
(2)类的签名比较简单,它采用L+包名+类型+;的形式,只需要将其中的.替换为/即可。 例如java.lang.String,它的签名为Ljava/lang/String;,末尾的;也是一部分。
(3)对象的签名就是对象所属的类签名。
(4)数组的签名[+类型签名。示例如下:
char[] [C
float[] [F
double[] [D
long[] [J
String[] [Ljava/lang/String;
Object[] [Ljava/lang/Object;
(5)如果是多维数组那么就根据数组的维度多少来决定[的多少, 例如int[][]那么就是[[I
(6)方法的签名为(参数类型签名)+返回值类型签名,参数类型的签名是连在一起。
举个例子,有方法boolean fun(int a, double b, int[] c)。那么按照方法的签名规则就是(ID[I)Z
例如方法:void fun(int a, String s, int[] c), 那么签名就是(ILjava/lang/String;[I)V
例如方法:int fun(), 对应签名()I
例如方法:int fun(float f), 对应签名(F)I方法名Java_com_android_peter_jnidemo_MainActivity_stringFromJNI:需要遵循Java_包名_调用类类名_Java需要调用的方法名。
JNIEnv *env:是结构体JNINativeInterface的二级指针,重定义了大量的函数指针,这些函数指针在jni开发中很常用。可以理解为JNI的上下文环境,需要通过env调用各种接口。
jobject instance:表示Java对象中的this。
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
函数里面具体的实现逻辑,定义了一个String类型的变量并赋值,然后通过JNI提供的NewStringUTF方法转换成jstring类型并返回。常用的JNI方法可以看一下《JNI学习积累之一 ---- 常用函数大全》里面的介绍。
分析完Android Studio提供给我们的Hello world示例,接下来尝试实现自己的方法以及调用.so文件中的方法。
实现JNI方法
本示例在之前Hello world的基础上实现自己的JNI方法。
1、在cpp目录下新建一个cpp文件——my-native-lib.cpp。
2、在CMakeLists.txt文件中添加相应的配置。
3、在需要使用JNI的java类中静态加载对应的lib库(这一步自动创建的示例已经帮们做了)。
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
4、在需要使用JNI的java类中声明新方法
5、在my-native-lib.cpp文件中实现addFromJNI方法
extern "C"
JNIEXPORT jint JNICALL Java_com_android_peter_jnidemo_MainActivity_addFromJNI(JNIEnv *env, jobject instance, jint x, jint y) {
jint total;
total = x + y;
return total;
}
6、调用JNI中新的方法
通过Android Studio的Build->Analyze APK可以看到编译生成的.so文件被自动打包到了APK中去。
输出的log如下:
生成的.so文件可以在app->build->intermediates->cmake中对应CPU文件夹下面找到。
JNI调用Java方法的流程
JNI调用Java方法的流程是先通过类名或是类的实例找到类,然后再根据方法名找到方法的id,最后就可以调用这个方法了。
JNI方法调用Java类静态方法
(1)在java中定义一个静态方法供JNI调用
public static void methodCalledByJni(String msgFromJni){
Log.d("JNIDemo", "methodCalledByJni,msg: " + msgFromJni);
}
(2)在Java中声明JNI中native方法
public native void callJavaStaticMethodFromJNI();
(3)在cpp文件中实现对应的native方法
extern "C"
JNIEXPORT void JNICALL
Java_com_android_peter_jnidemo_MainActivity_callJavaStaticMethodFromJNI(JNIEnv *env, jobject instance) {
// 1、获得实例对应的class类
jclass clazz = env -> GetObjectClass(instance);
if(clazz == NULL) {
printf("get object fail!");
return;
}
// 2、通过class类找到对应的method id
jmethodID methodId = env -> GetStaticMethodID(clazz,"staticMethodCalledByJni","(Ljava/lang/String;)V");
if(methodId == NULL) {
printf("get methodId fail!");
return;
}
// 定义一个string作为参数传递给Java方法
jstring msg = env -> NewStringUTF("msg send by callJavaStaticMethodFromJNI in my-native-lib.cpp .");
// 3、调用java类中的静态方法
env -> CallStaticVoidMethod(clazz,methodId,msg);
}
(4)在Java中调用
callJavaStaticMethodFromJNI();
运行输出的log如下:
JNI方法调用Java类非静态方法
有如下两种方式来实现。
-
通过JNIEnv提供的GetObjectClass方式实现
(1)在Java中定义一个非静态方法供JNI调用
public void publicMethodCalledByJni(String msgFromJni){
Log.d(TAG, "publicMethodCalledByJni , msg: " + msgFromJni);
}
(2)在Java中声明JNI的native方法
public native void callJavaPublicMethodFromJNI();
(3)在cpp文件中实现对应的native方法
extern "C"
JNIEXPORT void JNICALL
Java_com_android_peter_jnidemo_MainActivity_callJavaPublicMethodFromJNI(JNIEnv *env, jobject instance) {
//1.获得实例对应的class类
jclass clazz = env -> GetObjectClass(instance);
if(clazz == NULL) {
printf("get object fail!");
return;
}
//2.通过class类找到对应的method id
jmethodID methodId = env -> GetMethodID(clazz,"publicMethodCalledByJni","(Ljava/lang/String;)V");
if(methodId == NULL) {
printf("get methodId fail!");
return;
}
// 定义一个string作为参数传递给Java方法
jstring msg = env -> NewStringUTF("msg send by callJavaPublicMethodFromJNI in my-native-lib.cpp .");
//3.调用java类中的方法
env -> CallVoidMethod(instance,methodId,msg);
}
(4)在Java中调用
callJavaPublicMethodFromJNI();
运行输出的log如下:
-
通过JNIEnv提供的FindClass方式实现
(1)在Java中定义一个非静态方法供JNI调用
public void publicMethodCalledByJni(String msgFromJni){
Log.d(TAG, "publicMethodCalledByJni , msg: " + msgFromJni);
}
(2)在Java中声明JNI的native方法
public native void callJavaMethodByFindClassFromJNI();
(3)在cpp文件中实现对应的native方法
extern "C"
JNIEXPORT void JNICALL
Java_com_android_peter_jnidemo_MainActivity_callJavaMethodByFindClassFromJNI(JNIEnv *env, jobject instance) {
//1.通过反射获得父类的class类
jclass clazz = env -> FindClass("com/android/peter/jnidemo/MainActivity");
if(clazz == NULL) {
printf("get object fail!");
return;
}
//2.通过class类找到对应的method id
jmethodID methodId = env -> GetMethodID(clazz,"publicMethodCalledByJni","(Ljava/lang/String;)V");
if(methodId == NULL) {
printf("get methodId fail!");
return;
}
// 定义一个string作为参数传递给Java方法
jstring msg = env -> NewStringUTF("msg send by callJavaMethodByFindClassFromJNI in my-native-lib.cpp .");
//3.调用java类中的方法
env -> CallVoidMethod(instance,methodId,msg);
}
(4)在Java中调用
callJavaMethodByFindClassFromJNI();
运行输出的log如下:
通过上面的示例可以看出,JNI方法调用Java类的流程上都是一样的,只是native方法在实现上有细微的差别,调用的JNI提供的方法不同而已。
小结
本文在前文的基础上,通过对Android Studio自动生成的Hello world示例的分析,对JNI开发的流程和相关基础知识进行了梳理。