JNI概述
JNI的全称是Java Native Interface,它提供了若干的API实现了Java和其他语言的通信(主要是C&C++)。
使用Java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的。例如:
使用一些旧的库
与硬件、操作系统进行交互
提高程序的性能
提高应用的安全性。
那么怎么使用JNI呢,一般情况下我们首先是将写好的C/C++代码编译成对应平台的动态库(windows一般是dll文件,linux一般是so文件等),这里我们是针对Android平台,所以只讨论so库。
- 打包成so库会更安全一点,但肯定不是完全安全,只是相对反编译Java的class字节码文件来说,反汇编so动态库来分析程序的逻辑要复杂得多,没那么容易被破解。很多SDK,都使用了so库,例如百度SDK,微信SDK。
开始JNI编程之前,肯定要配置所支持的环境,请移步通过CMake在AndroidStudio项目中引入JNI编程。
下面就开始介绍Android中JNI编程的入门知识。
JNI函数的注册
其实就是Java的native方法与C/C++中的函数的连接,使二者能够识别彼此。
先看两个文件。
MainActivity.java:
package com.tsnt.jni.androidjnidemo;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = (TextView) findViewById(R.id.sample_text);
TextView tv1 = (TextView) findViewById(R.id.sample_text1);
tv.setText(stringFromJNI());
tv1.setText(getStringFromJNI());
}
//声明两个本地方法
public native String stringFromJNI();
public native String getStringFromJNI();
//加载本地库native-lib
static {
System.loadLibrary("native-lib");
}
}
native-lib.cpp:
//类似Java中的导包
#include <jni.h>
#include <string>
//extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码
//加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的
extern "C"
//静态注册的方法
//JNIEnv:它指向一个函数表,该函数表指向一系列的JNI函数,我们通过调用这些JNI函数可以实现与Java层的交互
jstring Java_com_tsnt_jni_androidjnidemo_MainActivity_getStringFromJNIStatically(
JNIEnv *env,
jobject obj) {
//声明一个string类型变量
std::string hello = "Hello from native -- registered statically";
return env->NewStringUTF(hello.c_str());
}
//动态注册的方法
jstring nativeGetStringFromJNIDynamically(JNIEnv *env, jobject obj) {
std::string hello = "Hello from native -- registered dynamically";
return env->NewStringUTF(hello.c_str());
}
//用来保存方法信息的数组
JNINativeMethod nativeMethod[] = {{"getStringFromJNIDynamically", "()Ljava/lang/String;", (void *) nativeGetStringFromJNIDynamically},};
//当我们使用System.loadLibarary()方法加载so库的时候,Java虚拟机就会找到这个函数并调用该函数
//因此可以在该函数中做一些初始化的动作
//其实这个函数就是相当于Activity中的onCreate()方法
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
JNIEnv *env;
if (jvm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
//获取MainActivity对象
jclass clz = env->FindClass("com/tsnt/jni/androidjnidemo/MainActivity");
//动态注册方法
env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod) / sizeof(nativeMethod[0]));
//返回JNI版本
return JNI_VERSION_1_4;
}
最后程序成功运行起来,就是这样的:
其实通过代码中的注释,大家已经可以理解个大概了,下面进行进一步分析。
静态注册
我们在MainActivity中声明了getStringFromJNIStatically()为native方法,他对应的JNI函数就是Java_com_tsnt_jni_androidjnidemo_MainActivity_getStringFromJNIStatically()。
在Java虚拟机加载so库时,会去寻找对应Java层的native方法。那它们两个究竟是怎么关联的呢?
我们仔细观察JNI函数名的构成形式是:Java_PkgName_ClassName_NativeMethodName,以Java为前缀,并且用“_”下划线将包名、类名以及native方法名连接起来就是对应的JNI函数了。
这里简单介绍一下生成的JNI函数包含两个固定的参数变量,分别是JNIEnv和jobject:
jobject就是当前与之链接的native方法隶属的类对象(类似于Java中的this)。
JNIEnv是个结构体,它指向一个函数表,该函数表指向一系列的JNI函数,我们通过调用这些JNI函数可以实现与Java层的交互。
这两个变量都是Java虚拟机生成并在调用时传递进来的。
然而静态注册有很多弊端,例如:
代码编写不方便,由于JNI层函数的名字必须遵循特定的格式,且名字特别长;
程序运行效率低,因为初次调用native函数时需要根据根据函数名在JNI层中搜索对应的本地函数,然后建立对应关系,这个过程比较耗时(静态注册是用到时加载,动态注册一开始就加载好了)。
下面就来说动态注册。
动态注册
动态注册的原理是这样的:JNI 允许我们提供一个函数映射表,注册给 JVM,这样 JVM 就可以用函数映射表来调用相应的函数。
JNI_OnLoad()
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
JNIEnv *env;
if (jvm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
//获取MainActivity对象
jclass clz = env->FindClass("com/tsnt/jni/androidjnidemo/MainActivity");
//动态注册方法
env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod) / sizeof(nativeMethod[0]));
//返回JNI版本
return JNI_VERSION_1_4;
}
当我们使用System.loadLibarary()方法加载so库的时候,Java虚拟机就会找到这个函数并调用该函数,因此可以在该函数中做一些初始化的动作,其实这个函数就是相当于Activity中的onCreate()方法。
该函数返回值表示当前使用的JNI的版本,其实类似于Android系统的API版本一样,不同的JNI版本中定义的一些不同的JNI函数。
该函数有两个参数,其中*jvm代表Java虚拟机实例。
其中进行的操作主要就是,获取Java对象,完成动态注册。
JNINativeMethod
注册方法的时候利用到了JNINativeMethod这个结构体,来看代码:
//用来保存方法信息的数组
JNINativeMethod nativeMethod[] = {{"getStringFromJNIDynamically", "()Ljava/lang/String;", (void *) nativeGetStringFromJNIDynamically},};
nativeMethod其实就是一个J**NINativeMethod的数组**,JNINativeMethod是这样定义的:
typedef struct {
const char* name;//Java层native方法的名字
const char* signature;//Java层native方法的描述符
void* fnPtr;//对应JNI函数的指针
} JNINativeMethod;
JNI数据类型
上面我们提到JNI定义了一些自己的数据类型。这些数据类型是衔接Java层和C/C++层的,如果有一个对象传递下来,那么对于C/C++来说是没办法识别这个对象的,同样的如果C/C++的指针对于Java层来说它也是没办法识别的,那么就需要JNI进行匹配,所以需要定义一些自己的数据类型。
基本数据类型
引用数据类型
描述符
类描述符
前面为了获取Java的AndroidJNI对象,是通过调用FindClass()函数获取的,该函数参数只有一个字符串参数,我们发现该字符串如下所示:
"com/tsnt/jni/androidjnidemo/MainActivity"
其实这个就是JNI定义了对类的描述符,它的规则就是将"com.tsnt.jni.androidjnidemo.MainActivity"
中的“.”用“/”代替。
方法描述符
前面我们动态注册native方法的时候结构体JNINativeMethod中含有方法描述符,就是确定native方法的参数和返回值,我们这里定义native方法是这样的:
public native String getStringFromJNIDynamically();
对应的描述符:“()Ljava/lang/String;”
括号中的值表示方法参数,没有参数,所以括号中为空。
括号后的值表示方法返回值,为String类型。
再举个例子:
public native void Fun(int a, int b)
对应的描述符:“(II)V”
方法描述符中的基本数据类型
方法描述符中的引用数据类型
对象类型:以”L”开头,以”;”结尾,中间是用”/” 隔开,如上表第1个
数组类型:以”[“开始,如上表第2个(n维数组的话,则是前面多少个”[“而已,如“[[[D”表示“double[][][]”)。
对象数组类型:上述两者结合,如上表第3个。
对象类型与数组类型的举例
demo地址:AndroidJNIDemo
参考: