JNI(Java Native Interface)
JNI描述的是一种技术 :提供一种供Java字节码调用C/C++的解决方案。
NDK(Native Development Kit)
Android NDK 是一组允许您将 C 或 C++("原生代码")嵌入到 Android 应用中的工具,NDK描述的是工具集。
能够在 Android 应用中使用原生代码对于想执行以下一项或多项操作的开发者特别有用:
- 在平台之间移植其应用。
- 重复使用现有库,或者提供其自己的库供重复使用。
- 在某些情况下提高性能,特别是像游戏这种计算密集型应用。
JNI:Java提供的。
NDK:安卓提供的。
JNI方法注册
静态注册
当Java层调用navtie函数时,会在JNI库中根据函数名查找对应的JNI函数。如果没找到,会报错。如果找到了,则会在native函数与JNI函数之间建立关联关系,其实就是保存JNI函数的函数指针。
下次再调用native函数,就可以直接使用这个函数指针。
JNI的函数名命名格式
Java_ + 包名(com.example.auto.jnitest)+ 类名(MainActivity) + 函数名(stringFromJNI) ****(需要将包名的 ****. ****改为 ****_ ****)
得到JNI函数名:Java_com_example_auto_jnitest_MainActivity_stringFromJNI
静态注册的缺点:
- 要求JNI函数的名字必须遵循JNI规范的命名格式。
- 名字冗长,容易出错。
- 初次调用会根据函数名取搜索JNI中对应的函数,会影响执行效率。
- 需要编译所有声明了native函数的Java类,每个类所生成的class文件都需要用javah工具生成一个头文件。
动态注册
通过提供一个函数映射表,注册给JVM虚拟机,这样JVM就可以用函数映射表来调用相应的函数,就不必通过函数名来查找需要调用的函数。
Java与JNI通过JNINativeMethod的结构体对象来建立函数映射表,它在jni.h头文件中定义,其结构内容如下:
arduino
typedef struct{
const char* name;
const char* signature;
void* fnPtr;
}JNINativeMethod;
- 创建映射表后,调用RegisterNatives函数将映射表注册给JVM。
- 当Java层通过System.loadLibrary加载JNI库时,会在库中查找JNI_OnLoad函数。
我们可以将JNI_OnLoad函数视为JNI库的入口函数,需要在这里完成所有函数映射和动态注册工作,及其他的一些初始化工作。
数据类型转换
基础数据类型转换
Java类型 | JNI类型 | 描述 |
---|---|---|
boolean(布尔类型) | jboolean | 无符号8位 |
byte(字节类型) | jbyte | 有符号8位 |
char(字符型) | jchar | 无符号16位 |
shor(短整型) | jshort | 有符号16位 |
int(整型) | jint | 有符号32位 |
long(长整型) | jlong | 有符号64位 |
float(浮点型) | jfloat | 32位 |
double(双精度浮点型) | jdouble | 64位 |
引用数据类型转换
除了Class 、String 、Throwable 和基本数据 类型的数组外,其余所有Java对象的数据类型在JNI中都用jobject表示。
Java中的String也是引用类型,但是由于使用频率较高,所以在JNI中单独创建了一个jstring类型。
Java引用类型 | JNI类型 | Java引用类型 | JNI类型 |
---|---|---|---|
All objects | jobect | char[] | jcharArray |
java.lang.Class | jclass | short[] | jshortArray |
java.lang.String | jstring | int[] | jintArray |
java.lang.Throwable | jthrowable | long[] | jlongArray |
Object[] | jobjectArray | float[] | jfloatArray |
boolean[] | jbooleanArray | double[] | jdoubleArray |
byte[] | jbyteArray |
- 引用类型不能直接在Native层使用,需要根据JNI函数进行类型的转化后,才能使用。
- 多维数组(含二维数组)都是引用类型,需要使用jobjectArray类型来存取。
列如,二维整型数组就是指向一维数组的数组,其声明使用方式如下:
ini
//获得一维数组的类引用,即jintArray类型。
jclass intArrayClass = env->FindClass("[I");//在 JNI 中,"[I" 表示一维 int 数组的签名。
//构造一个指向jintArray类一维数组的对象数组,该对象数组初始大小为lenght,数组中的元素类型是intArrayClass。
jobjectArray objectIntArray = env->NewObjectArray(length,intArrayClass,NULL);
JNI函数签名sig信息
由于Java支持函数重载,因此仅仅根据函数名是没法找到对应的JNI函数的。
为了解决这个问题,JNI将参数类型和返回值类型作为了函数的签名信息。
签名信息 可用来描述对象信息, 以及描述函数信息。
描述对象类型的叫做对象签名信息。
描述函数的叫做函数签名信息。
- JNI规范定义的函数签名信息 格式:
(参数1类型字符...)返回值类型字符 - JNI常用的数据类型及对应字符:
Java类型 | 字符 | Java类型 | 字符 |
---|---|---|---|
void | V | byte | B |
boolean | Z (容易误写成B) | char | C |
int | I | short | S |
long | J(容易误写成L) | int[] | [I(数组以"I"开始) |
double | D | String | Ljava/lang/String; |
float | F | object[] | L+/分割完整类型 如 String:[Ljava/lang/String; |
array | [+类型描述符 如 int[] : [I |
注:JNI反射Java对象时,会用到签名信息来描述对象类型。
- 函数签名信息例子:
Java函数 | 函数签名 | 签名含义 |
---|---|---|
String fun() | "()Ljava/lang/String;" | - |
long fun(int i,Class c) | "(ILjava/lang/Class;)J" | I表示int,Ljava/lang/Class;表示Class类型。 |
void fun(byte[] bytes) | "([B)V" | - |
JNI编译
Android Studio现有打包so库的方式有两种:
- ndk-build编译项目
- CMake脚本构建项目
ndk-Build编译
android studio ndk-build 编译C生成.so文件(ndk基础篇),看完你就懂了
1、下载NDK及构建工具
为您的应用编译和调试原生代码,您需要以下组件:
- Android 原生开发工具包 (NDK) :这套工具集允许您为 Android 使用 C 和 C++ 代码,并提供众多平台库,让您可以管理原生 Activity 和访问物理设备组件,例如传感器和触摸输入。
- CMake:一款外部构建工具,可与 Gradle 搭配使用来构建原生库。如果您只计划使用 ndk-build,则不需要此组件。
- LLDB:一种调试程序,Android Studio 使用它来调试原生代码。(目前已知 Android4.0.4 版本及之后的版本都已将LLDB调试工具内置到了NDK中)
上述的工具都可以在Android Studio的setting->Android SDK中下载。
Android Studio下载NDK后已经内置了LLDB,无需单独下载, 安装 Cmake+NDK 即可直接调试JNI程序。
2、配置项目NDK版本
- 新建一个Android项目,如:NdkDemo,包名:com.xunua.pdfdemo。
- File->Project Structyu->Android NDK location中进行NDK版本/路径的选择即可。
一般使用NDK21版本就可。
- 在gradle.properties中新增代码以启用NDK。
ini
android.useDeprecatedNdk=true
之后项目环境就配置好了,可以开始编写java和c的代码了。
3、Java代码与C代码的编写过程
- 首先新建一个java类JNIUtils.java(用于so库的初始化加载及native函数的声明),代码如下:
arduino
public class JNIUtils {
// 加载native-jni
static {
System.loadLibrary("native-jni");
}
//java调C中的方法都需要用native声明且方法名必须和c的方法名一样
public native String stringFromJNI();
}
- 重新Make Project一下工程,完成后会在工程目录 ... /NdkDemo/app/build/intermediates/classes/debug/com/niwoxuexi/ndkdemo 看到自己编译后的classes文件JNIUtils.class
- 用javah工具生成头文件
-
- 首先新建一个java类JNIUtils.java。
- 打开Terminal命令行工具(Alt+F12)。
- 在命令行中先进入到工程的main目录下。
- 输入命令:javah -d jni -classpath 自己编译后的class文件的绝对路径
例如:
javah -d jni -classpath /Users/zhuxiaocheng/android/workspace/NdkDemo/app/build/intermediates/classes/debug com.niwoxuexi.ndkkemo.JNIUtils
需要注意的点: ① debug后的空格 ②windows 系统路径中的文件的分割线是 '' 而不是mac系统的 '/' - 执行javah命令之后就会在main目录下生成jni文件夹,同时生成.h文件 如下图所示。
- 接下来,我们在jni目录下新建一个 native-lib.c 的c文件,内容如下:
arduino
#include "com_niwoxuexi_ndkdemo_JNIUtils.h"
/**
* 上边的引用标签一定是.h的文件名家后缀,方法名一定要和.h文件中的方法名称一样
*/
JNIEXPORT jstring JNICALL Java_com_xunua_pdfdemo_JNIUtils_stringFromJNI
(JNIEnv *env, jobject ojb){
return (*env) -> NewStringUTF(env,"Hello, I'm from jni");
}
- 之后在app的build.gradle配置文件中添加如下代码:
arduino
//ndk编译生成.so文件
ndk {
moduleName "native-lib" //生成的so名字
abiFilters "armeabi", "armeabi-v7a", "x86" //输出指定三种abi体系结构下的so库。
}
- 最后进行我们JNI函数的验证,只需要在MainActivity中调用一下C的代码就可以了
scala
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = (TextView) findViewById(R.id.text);
textView.setText(new JNIUtils().stringFromJNI());
}
}
- 直接运行项目,结果如下所示:
4、将.so库的引入其他项目并使用
前面的几个步骤已经帮助我们完成了NDK代码的编写,及使用ndk-build对ndk代码编译后;编写Java类、JNI函数声明、NDK的加载,实现了对NDK代码的调用。那么如果想要将我们项目中生成的.so库提供给其他项目使用的话,该怎么做呢?
- 首先从项目中找到.so文件,位置如下:
- 将生成的.so库放到新项目的libs目录下。
- 在app module下的build.gradle中添加下面代码:
less
//放在libs目录中
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
如图所示:
- so库的初始化及native函数的声明。
参考步骤3-1 的新建一个java类JNIUtils.java 来实现对NDK的初始化及Native函数的声明和调用。
这样便完成了对.so库的调用。
Cmake编译
CMake 则是一个跨平台的编译工具,它并不会直接编译出对象,而是根据自定义的语言规则(CMakeLists.txt)生成对应 makefile 或 project 文件,然后再调用底层的编译, 在Android Studio 2.2 之后支持Cmake编译。
- add_library指令
语法:add_library(libname [SHARED | STATIC | MODULE] [EXCLUDE_FROM_ALL] [source])
将一组源文件source编译出一个库文件,并保存为libname.so (lib 前缀是生成文件时CMake自动添加上去的)。
其中有三种库文件类型,不写的话默认为STATIC:
-
- SHARED:表示动态库,可以在(java)代码中使用System.loadLibrary(name);动态调用。
- STATIC:表示静态库,集成到代码中会在编译时调用。
- MODULE:只有在使用 dyId 的系统有效,如果不支持dyid,则被当做SHARED对待。
- EXCLUDE_FROM_ALL:表示这个库不被默认构建,除非其他组件依赖或手工构建。
scss
#将compress.c 编译成 libcompress.so 的共享库
add_library(compress SHARED compress.c)
- find_library指令 **
****语法:find_library(name1 path1 path2 ...)
name1表示将find到的库取的别名。
**path1 path2 ...变量表示找到的库全路径,包含库文件名。如:
scss
find_library(libx x11 /usr/lib)
find_library(log-lib log)#路径为空时,应该是查找系统环境变量路径。
- target_link_libraries指令
语法:target_link_libraries(target library <debug | optimized> library2...)
这个指令可以用来为target 添加需要链接的共享库library,同样也可以用于为自己编写的共享库添加共享库链接。如:
bash
#指定 compress 工程需要用到 libjpeg 库和 log 库
target_link_libraries(compress libjpeg ${log-lib})
CMake的编译过程
- 首先下载NDk,这个步骤与NDK-build编译一致。
- 在module的build.gradle中添加CMake清单声明:
arduino
android {
//......
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
}
- 在main目录下创建cpp文件夹
- 在cpp文件夹下创建CMakeLists.txt 文件、自己编写c代码的.cpp文件。
- 在CMakeLists.txt中配置生成的so库的名称、要引入的三方库、及需要编译入so库的自己编写的.cpp代码的文件名。
- 编写JAVA类,定义需要JNI去实现的本地函数getString()
arduino
public class NativeLibManager {
static {
System.loadLibrary("native-lib");
}
public native String getString();
}
- 去CPP文件夹下的native-lib.cpp中实现getString()函数:
c
extern "C" //表示以C的语法来编译
JNIEXPORT jstring JNICALL //函数签名信息,表示返回值为jstring->string类型。
Java_com_xunua_MyFecDemo_NativeLibManager_getString(JNIEnv *env, jobject thiz) {
string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
AS4.0之后的版本都是支持自动创建对应的JNI函数的:
- 在安卓项目中,调用NativeLibManager的getString(),能拿到对应的内容就表示执行成功。
Abi架构
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;
根据以上的兼容总结,我们可以得到一些规律:
- armeabi的so文件基本上是万金油 ,它能运行在除了mips和mips64的设备上,但在非armeabi设备上运行性能还是有损耗。
- 64位的CPU架构总能向下兼容其对应的32位指令集,如:x86_64兼容X86,arm64_v8a兼容armeabi_v7a,mips64兼容mips。
JNI的静态注册/动态注册、静态库/动态库、ndk-build编译/cmake编译的关系?
在Android开发中,特别是使用NDK(Native Development Kit)进行本地代码(C/C++)的开发,涉及到JNI的静态注册、动态注册、静态库、动态库、ndk-build编译以及cmake编译。让我们逐步解释它们之间的关系:
- JNI(Java Native Interface): ****是Java提供的一种机制,允许Java代码调用本地(C/C++)代码。JNI提供了Java和本地代码之间的桥梁 。
- 静态注册和动态注册:
-
- 静态注册: 在Java源代码中使用native 关键字声明本地方法,并通过javah工具生成包含函数签名的头文件,然后在C/C++代码中实现这些本地方法。这些方法在Java类加载时被静态注册到JNI中。
- 动态注册: 在C/C++代码中使用JNI提供的函数动态注册本地方法,而不是在Java代码中静态声明。这样,你可以在运行时选择注册哪些本地方法。
- 静态库和动态库:
-
- 静态库: 编译时链接到目标程序,形成一个可执行文件。 .a (Unix/Linux)或 .lib(Windows)是静态库的常见扩展名。
- 动态库: 在运行时动态链接到目标程序。 .so (Unix/Linux)或 .dll(Windows)是动态库的常见扩展名。
- ndk-build编译:
-
- ndk-build 是Android NDK提供的一个构建工具,用于编译、链接和构建本地代码。通过编写Android.mk文件,你可以配置项目的构建过程,包括静态/动态库的编译和链接,以及JNI本地方法的静态注册。
- cmake编译:
-
- cmake 是一种跨平台的构建工具,可以用于配置、编译和构建项目。在Android开发中,可以使用CMake 来代替ndk-build ,并且它提供更灵活和现代的构建配置。你可以编写CMakeLists.txt文件来描述项目的结构和依赖关系。
关系总结:
- 无论是使用ndk-build 还是CMake,它们都是用于构建NDK项目的工具,可以配置本地代码的编译、链接和构建。
- JNI的静态注册和动态注册是关于本地方法在JNI中如何注册的两种不同方式。
- 静态库和动态库是两种不同的库的形式,它们在编译和链接时的行为不同。在Android NDK中,通常使用动态库( .so文件)。
- 在JNI中,静态注册和静态库之间没有直接的关系。无论是静态注册还是动态注册,都可以生成动态库( .so 文件)或者静态库( .a 或 .lib文件)。
在实际开发中,你可以选择使用ndk-build 或者CMake来进行项目构建,同时选择静态注册或动态注册方式,具体取决于你的项目需求和个人偏好。