JNI的意思是Java Native Interface(java本地接口),它是为了方便java调用C,C++等本地代码所封装的一层接口,我们都知道,JAVA的优点是跨平台,但是作为有蒂娜的同时,其在本地交互的时候出现了短板,java的跨平台性导致了本地交互的能力不够强大,一些和操作系统相关的特性无法满足,这才出现了java JNI
NDK是android所提供的一个工具借,通过NDK可以在android中更加方便的通过jni来访问本地代码。比如c/c++,ndk还提供了交叉编译器,开发人员只需要简单的修改mk文件就可以生成特定的CPU动态库:
- 1.提高了代码的安全性,由于so库反编译比较困难,因此NDK提高了Android程序的安全性
- 2.可以很方便的使用目前已有的C/C++开源库
- 3.由于平台间的移植,通过C/C++实现的动态库可以很方便的在其他平台使用
- 4.提高程序在某些特定情形下的执行效率,但是并不能明显提升Android程序的性能
由于jni和ndk比较适合在linux环境下开发,这里也同样用ubuntu来说明
一.JNI的开发流程
JNI的开发流程有如下几个步骤,首先需要在JAVA中声明native方法,接着用C或者C++实现native方法,然后编译运行
1.在java中声明native方法
public class JniTest {
static {
System.loadLibrary("jni-test");
}
public static void main(String [] args){
JniTest jniTest = new JniTest();
System.out.print(jniTest.get());
jniTest.set("Hello Jni");
}
public native String get();
public native void set(String str);
}
可以看到代码,声明了两个native方法,get和set(string),这两个需要在JNI中实现,jniTest的头部有一个加载动态库的过程,其中jni-test是so库的标识,so库完整的名称为libjni-test.so,这是加载so库的规范。
2.编译Java源文件得到class文件,然后通过javah命令到处JNI的头文件
具体的命令
javac jniTest.java
javah jniTest
这样就会自动生成一个头文件,这里注意下,函数名的规则是:Java_包名类名方法名,比如jniTest中的set方法,到这里就变成 JNIEXPORT void JNICALL Java_com.liuguilin_jniTest_set(JNIEnv*,jobject,jstring),关于Java和Jni的数据类型之间的关系会在后面介绍,这里只需要知道Java的String对应的JNI的jstring,JNIEXPORT,JNICALL,JNIEnv和jobject都是JNI标准定义的类型或者宏,他们的含义:
- JNIEnv*:表示一个指向JNI环境的指针,可以通过他来访问JNI提供的接口方法
- jobjct:表示Java对象的this
- JNIEXPORT,JNICALL:他们是JNI所定义的宏,可以在jni.h这个头文件中查看
下面的宏定义是必须的,他指定extern “C”内部的函数采用C语言的命名规范来编译,否则当JNI采用C++来实现时,由于命名风格不同,这将导致JNI在链接时无法根据函数名查找到具体的函数,那么JNI调用就无法完成,更多的细节实际上有关C和C++编译时的一些问题,这里就不展开了
#ifdef _cplusplus
extern "C"{
#endif
3.实现JNI方法
JNI方法是指Java中声明的native方法,这里可以选择C或者C++来实现,他们的实现过程都类似,只有少量的区别,下面分别使用C和C++来实现JNI的方法,首先,在工程里创建一个子目录,这里叫做jni,然后将之前通过javah生成的头文件复制尽力啊,接着创建test.cpp和test.c文件
test.cpp和test.c很类似,但是他们对env的操作方法有所不同,
C++:env->NewStringUTF("Hello from JNI!");
C:(*env)->NewStringUTF("Hello from JNI!");
4.编译so库并在Java中调用
so库的编译这里采用gcc,切换到jni目录中,对于test.cpp和test.c来说,他们的编译指令如下
C++:gcc -shared -I /usr/lib/jvm/java-7-openjdk-amd64/include -fPIC test.cpp -o libjni-test.so
C:gcc -shared -I /usr/lib/jvm/java-7-openjdk-amd64/include -fPIC test.c -o libjni-test.so
上面的语句中,/usr/lib/jvm/java-7-openjdk-amd64是本地的JDK安装路径,在其他环境编译时也可以将其指向本机的jdk路径即可,而libjni-test.so则是生成so库的名字,在JAVA中可以通过如下方式加载System.loadLibaray(“jni-test”),其中so库名字中的“lib”和“.so”是不需要明确指明的,so编译后,就可以调用了
首先采用C++产生so库的日志
invoke get in C++
Hello from JNI!
invoke set from C++
hello world
然后是C
invoke get from C
Hello from JNI!
invoke set from C
hello world
通过上面的例子可以方便的调用C、C++代码
二.NDK的开发流程
NDK的开发是基于JNI的
1.下载和配置NDK
首先要从Android官网下载NDK,然后配置环境变量
2.创建一个Android项目,声明native方法
public class JniActivity extends AppCompatActivity{
static {
System.loadLibrary("jni-test");
}
@Override
protected void onCreate( Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_jni);
TextView textView = findViewById(R.id.mTextView);
textView.setText(get());
set("Hello JNI");
}
public native String get();
public native void set(String str);
}
3.实现Android项目中所声明的native方法
在的外部创建jni目录,然后再jni目录下创建三个文件,test.cpp,Android.mk,Application.mk
//test-cpp
#include <jni.h>
#include <stdio.h>
#ifdef _cplusplus
extern "C"{
#endif
jstring Java_com_liuguilin_androidsample_JniTestApp_JniActivity_get(JNIEnv*env,jobject thiz){
printf("invoke get in c++ \n");
return env->NewStringUTF("Hello JNI");
}
void Java_com_liuguilin_androidsample_JniActivity_set(JNIEnv*env,jobject thiz,jstring string){
printf("invoke get in c++ \n");
char* str = env -> GetStringUTFChars(string,NULL);
printf("%s\n",str);
env->ReleaseStringUTFChars(string,str);
}
#ifdef _cplusplus
}
#endif
#Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := jni-test
LOCAL_SRC_FILES := test.cpp
include $(BUILD_SHARED_LIBRARY)
#Application.mk
APP_ABI := armeabi
这里对Android.mk和Application.mk做了简单的介绍,在Android.mk中,LOCAL_MODULE表示模块的名称,LOCAL_SRC_FILES表示需要参与编译的源文件,Application.mk中常用的配置项是APP_ABI,他表示CPU的加载平台的类型,目前市面上常见的架构平台是armeabi,x86,mips,其中在移动设备中有主要地位的是armeabi,这也是大部分apk中只包含armeabi平台的原因
4.切换到jni目录的父目录,然后通过ndk-build命令编译产生的so
这个时候NDK回切换一个和jni目录平级的目录,libs,在这面放置so,需要主要的是,ndk-build命令会默认jni目录为本地源码的目录,如果庅存放的目录名字不是jni则编译不通过
然后再创建一个jniLibs放进去即可
当然,我们需要配置一下build.gradle
sourceSets.main{
jniLibs.srcDir 'src/main/jniLibs'
}
除了手动使用ndk-build命令创建so库,还可以通过AS来自动编译,不过就比较复杂了,为了让AS自动编译JNI,我们需要配置NDK的选项
ndk{
modelName "jni-test"
}
这样就可以自动编译JNI代码了,但是这个时候AS会把所有平台的so都打包到apk中,我们一般要配置一下的
productFlavors{
arm{
ndk{
abiFilter "armeabi"
}
}
x86{
ndk{
abiFilter "x86"
}
}
三.JNI的数据类型和类型签名
JNI的数据类型包含两种,基本类型和引用类型,基本类型主要有jboolean,jchar,jint等,他们和Java中的数据类型对应关系
JNI中的引用类型主要有类,对象和数组,他们在java中的引用类型的对应关系如图:
JNI的类型签名标识了一个特定的JAVA类型,这个类型即可是方法,也可以是数据类型
类的签名比较简单,采用了L+包名+类名+.的形式,只需要将其中的.替换为/即可,比如java.lang.String,他的签名是Ljava/lang/String,注意末尾也是签名的一部分
基本数据类型采用的是一系列大写字母表示:
从表可以看出,基本数据类型的签名是有规律的,一般是首字母的大写,但是boolean除外,因为B已经被byte占用,而long的签名之所以不是L,那是因为L表示的是类的签名
对象和数组的签名稍微复杂一些,对于对象来说,他的签名就是对象所属的类的签名,比如String对象,他的签名是Ljava/lang/String,对于数组来说,他的签名为【+类型签名,比如int数组,其类型为int,而int的签名为I,所以int数组的签名为【I,同理就是可以得出如下的签名对应关系
char[] [C
float[] [F
double[] [D
long[] [J
String[] [Ljava/lang/String
Object[] [Ljava/lang/Object
对于多维数组来说,他的签名为n+[+类型签名,其中n表示数组的维度,比兔int[][]的签名[[I
方法的签名为(参数类型签名)+返回值类型签名,这有点不好理解,举个例子,如下方法:boolean fun1(int a,double b,int[]c),根据签名的规定可以知道,他的参数类型的签名连在一起读ID[I,返回值类型的签名为Z,所以整个办法的签名就是(ID[I)Z。再举例子:
int fun1() 签名为()I
void fun1(int i) 签名为(I)V
四.JNI调用JAVA方法的流程
JNI调用Java方法的流程是闲通过类名找到类,然后再根据方法名找到方法的id,最后就可以调用这个方法了,如果调用JAVA中的费静态方法,那么需要构造出类的对象后才能调用
,下面的例子演示了如何在JNI中调用java的静态方法,至于非静态的只是多了异步构造队形的过程
首先需要在java中定义一个静态方法供JNI调用
public static void methodCalledByJni(String msg) {
Log.i(TAG, "methodCalledByJni:" + msg);
}
void callJavaMethod(JNIEnv * ev,jobject thiz){
jclass clszz = env->FindClass("com/liuguilin/androidsample/JniActivity");
if(clazz == NULL){
return;
}
jmethodID id = env->GetStaticMethodID(clszz,"methodCalledByJni","(LJava/lang/String;)V");
if(id == NULL){
printf("error");
}
jstring msg = env->NewStringUTF("msg send by callJavaMethod in test.cpp");
env->CallStaticVoidMethod(clazz,id,msg);
}
从callJavaMethod的实现可以看出,程序会根据类名找到类,然后再去找这个方法,接着完成最终的调用,最后在get中的使用:
jstring Java_com_liuguilin_androidsample_JniTestApp_JniActivity_get(JNIEnv*env,jobject thiz){
printf("invoke get in c++ \n");
callJavaMethod(env,thiz);
return env->NewStringUTF("Hello JNI");
}
由于MainActivity会调用JNI中的get方法,set方法优惠调用callJavaMethod方法,而callJavaMethod方法又会反过来调用,这样就完成了一次从Java调用JNI然后从JNI中调用Java的方法,安装运行程序,可以看出,已经调用成功了
我们可以发现,JNI调用java过程和java方法的定义有很大的关联,针对不同类型的java方法,JNIEnv提供了不同的接口和调用,这只是一个初步的介绍,更多的需要读者自己去查阅