JNI(java Native Interface)java本地接口,是为方便java调用C或者C++等本地的代码所封装的一层接口。由于java的跨平台性导致本地交互能力不好,一些和操作系统相关的特性Java无法完成,于是Java提供了JNI专门用于和本地代码交互
NDK(Native Development Kit)本地开发工具链,是Android提供的一个工具合集,帮助开发者快速开发C(或C++)的动态库,并能自动将.so和java应用一起打包成apk。NDK集成了交叉编译器(交叉编译器需要UNIX或LINUX系统环境),并提供了相应的mk文件或CMake文件隔离cpu、平台、ABI等差异,开发人员只需要简单修改mk文件或CMake文件(指出"哪些文件需要编译"、"编译特性要求"等),就可以创建出.so。
ABI(Application binary interface)应用程序二进制接口。不同CPU与指令集的每种组合都有定义的ABI(应用程序而进程接口),一段程序只有遵循这个接口规范才能在该CPU上运行,所以同样的程序代码为了兼容多个不同的cpu,需要为不同的ABI构建不同的库文件。当然对于CPU来说,不同的构建并不意味着一定互不兼容。
armeabi设备只兼容armeabi;
armeabi-v7a设备兼容armeabi-v7a、armeabi;
arm64-v8a设备兼容arm64-v8a、armeabi-v7a、armeabi;
X86设备兼容X86、armeabi;
X86_64设备兼容X86_64、X86、armeabi;
mips64设备兼容mips64、mips;
mips只兼容mips;
Android的应用层的类都是以java写的,这些java类编译为Dex型式的字节码之后,必须依靠Dalvik虚拟机来运行,在Android中Dalvik虚拟机中扮演很重要的角色。而Android中间件是由C/C++写的,这些C/C++写的组件并不是在Dalvik虚拟机上运行的,一旦使用JNI,JAVA程序就丧失了JAVA平台的两个优点:
1、程序不再跨平台。要想跨平台,必须在不同的系统环境下重新编译本地语言部分。
2、程序不再是绝对安全的,本地代码的不当使用可能导致整个程序崩溃。一个通用规则是,你应该让本地方法集中在少数及各类中。这样就降低了Java和C之间的耦合性。
在Android中提供了System.loadLibrary()或者System.load()来加载库。那这两个方法有啥区别呢?
System.load的参数必须为库文件的绝对路径,可以是任意路径,例如:System.load(“usr/lib.TestJNI.so”)
System.loadLibrary()方法的参数为库文件名,不包含库文件的扩展名。例如:System.loadLibrary(“TestJNI”);
需要注意的是,如果.so动态库不存在时,会抛出couldn’t find "libxxx.so"异常
load library error=dalvik.system.PathClassLoader[DexPathList[[
zip file "/data/app/com.frank.ffmpeg/base.apk"],
nativeLibraryDirectories=[/data/app/com.frank.ffmpeg/lib/arm64,
data/app/com.frank.ffmpeg/base.apk!/lib/arm64-v8a,/system/lib64, /vendor/lib64, /product/lib64]]]
couldn't find "libhello.so"
如果期待加载的是64bit的库,却加载到32bit的,会报错如下:
java.lang.UnsatisfiedLinkError: dlopen failed: “xxx.so” is 32-bit instead of 64-bit
java层调用带native关键字的JNI方法,需要注册java层与native层的对应关系,有静态注册和动态注册两种方式。静态注册一般是应用层使用,绑定包名+类名+方法名,在调用JNI方法时,通过类加载器查找对应的函数。动态注册一般是当Java通过System.load加载完JNI动态库后,紧接着会调用JNI_OnLoad,当JNI_OnLoad回调时,把JNINativeMethod注册到函数表。
Android 提供了两种开发jni的方式 1、用.mk文件的方式进行JNI开发
2、用CMake的方式进行JNI开发
1、创建JNIDemo工程,编写java类,例如:TextDemo.java
2、在命令行下输入javac TextDemo.java 生成TextDemo.class文件
3、在TextDemo.class目录下通过 javah -jni 包名.TextDemo(全类名)生成包名_TextDemo.h文件
4、在app目录下新建jni文件夹,在jin文件夹下创建test.c文件
5、编写test.c文件,并拷贝包名_TextDemo.h下的函数,并实现这些函数,且在其中添加jni.h头文件。
6、在jni文件夹下创建Android.mk文件,并在Android.mk文件中指定要生成的so库名称以及要使用的test.c源文件
7、在app目录下执行ndk-build命令生成对应的so文件‘
8 、在AndroidStudio里面配置ndk的路径以及so所在的目录,在local.properties文件里面添加ndk的路径:ndk.dir=E:\sdk\ndk-bundle
在build.gradle文件的buildType标签里面添加:
//放在libs目录中
sourceSets.main{
{
jniLibs.srcDirs = ['libs']
}
}
9、运行so库。
.mk方式加载so可参考博客
1、创建JNIDemo工程,编写java类,例如:TextDemo.java
2、在命令行下输入javac TextDemo.java 生成TextDemo.class文件
3、在TextDemo.class目录下通过 javah -jni 包名.TextDemo(全类名)生成包名_TextDemo.h文件
4、在app目录下新建jni文件夹,在jin文件夹下创建test.c文件
5、编写test.c文件,并拷贝包名_TextDemo.h下的函数,并实现这些函数,且在其中添加jni.h头文件。
6、在jni文件夹下创建CMakeLists.txt文件,并在CMakeLists.txt文件中指定要生成的so库名称以及要使用的test.c源文件
7、在build.gradle文件中添加cmkeLists.text文件的路径,如下:
externalNativeBuild {
cmake {
path file('jni/CMakeLists.txt')
version '3.22.1'
}
}
8、运行so库。
因为CMake是跨平台的构建工具,对JNI开发来说有比较明显的优点就是:1、编写C代码有提示,2、自动搜索正在构建的软件可能需要的程序、库和头文件的能力,3、能够为自动生成的文件创建复杂的自定义命令,其他的高级特性可自行搜索
通过上面两种方式我们可以先熟悉一下开发JNI的流程。接下来我们看一下静态注册生成的.h文件和.c文件
com_example_jnidemo_TextDemo.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_jnidemo_TextDemo */
#ifndef _Included_com_example_jnidemo_TextDemo
#define _Included_com_example_jnidemo_TextDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_jnidemo_TextDemo
* Method: getText
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring
JNICALL Java_com_example_jnidemo_TextDemo_getText
(JNIEnv *, jobject);
/*
* Class: com_example_jnidemo_TextDemo
* Method: setText
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_com_example_jnidemo_TextDemo_setText
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
在这里插入代码片
test.c
#include <jni.h>
#include "com_example_jnidemo_TextDemo.h"
#include <string.h>
#include <android/log.h>
JNIEXPORT jstring JNICALL Java_com_example_jnidemo_TextDemo_getText
(JNIEnv *env, jobject this){
//参数一是日志级别,参数二是日志TAG,参数三就是日志内容
__android_log_print(ANDROID_LOG_ERROR, "test", "invoke get from C\n");
return (*env)->NewStringUTF(env, "Hello world from c !");
}
JNIEXPORT void JNICALL Java_com_example_jnidemo_TextDemo_setText
(JNIEnv *env, jobject this, jstring string){
__android_log_print(ANDROID_LOG_ERROR, "test", "invoke set from C\n");
char* str = (char*)(*env)->GetStringUTFChars(env,string,NULL);
__android_log_print(ANDROID_LOG_ERROR, "test", "%s\n", str);
(*env)->ReleaseStringUTFChars(env, string, str);
}
可以看到静态注册是根据函数名来建立java方法和JNI函数间的一一对应关系。
静态注册的弊端:
1、后期类名、文件名改动,头文件所有函数将失效,需要手动改,超级麻烦易出错
2、代码编写不方便,由于JNI层函数的名字必须遵循特定的格式,且名字特别长
3、会导致程序员的工作量很大,因为必须为所有声明了native函数的java类编写JNI头文件
4、程序运行效率低,因为初次调用native函数时需要根据函数名在JNI层中搜索对应的本地函数,然后建立对应关系,这个过程比较耗时。