Android零基础进阶 | JNI
JNI背景
Java的一个显著的特点就是跨平台,但是正因如此,Java和本地机器的交互就很少。但是在某些情况下,比如提升性能或实现特定需求,需要我们不得不使用调用C/C++来实现,因此JNI应运而生。
JNI和NDK的关系
Jni全称:Java Native Interface,Java原生接口,是为Java和C/C++等语言相互访问提供的接口。
NDK全称:Native Develop Kit ,原生开发工具集,为了实现JNI所需要使用的工具。
一般都是编写C/C++ Code,通过NDK生成.so文件,然后再通过JAVA调用其中的接口来实现相应的功能。
如果是IOS的小伙伴,个人感觉这种关系有点类似于Swift调用OC?
JNI的实现流程
Jni编写大致过程
- 首先声明一个native方法
- 通过javah生成相应的头文件(有的会先javac生成class文件,试了直接javah好像也没有问题= 。=)
- 实现头文件中声明函数的方法
- 编译生成so文件
- 在java中进行调用so文件
声明Native方法
public class Native{
public static native void Hello();
}
通过javah生成相应的Native头文件
进入main目录下。执行javah -encoding utf-8 包名命令,会看到生成了一个.h的头文件
实现相应的方法
创建一个C/Cpp文件,然后将头文件赋值到里面进行少许修改,例如:
public static native int send_message(byte[] buffer, int size);
//.h 自动生成
JNIEXPORT jint JNICALL Java_com_vpn_NativeClass_send_1message
(JNIEnv *, jclass, jbyteArray, jint);
//.c
JNIEXPORT jint JNICALL Java_com_vpn_NativeClass_send_1message
(JNIEnv * env, jclass, jbyteArray byteData, jint size) {
return 0;
}
关于JNIEnv
JNIEnv 是一个与线程相关的结构体,是c和java相互调用的桥梁,由于线程相关,所以线程B中不能使用线程A中的JNIEnv函数。
JNIEnv 与 JavaVM : 注意区分这两个概念;
– JavaVM : JavaVM 是 Java虚拟机在 JNI 层的代表, JNI 全局仅仅有一个该虚拟机映射,所有线程共用一个;
– JNIEnv : JavaVM 在线程中的代表, 每一个线程都有一个, JNI 中可能有非常多个 JNIEnv;
JNIEnv 作用 :
– 调用 Java 函数 : JNIEnv 代表 Java 执行环境, 能够使用 JNIEnv 调用 Java 中的代码;
– 操作 Java 对象 : Java 对象传入 JNI 层就是 Jobject 对象, 须要使用 JNIEnv 来操作这个 Java 对象;
数据类型
基础数据类型
类型签名
与JNIEnv相关的常用函数
//获取jclass对象,参数:this的意思,就是native方法所在的类
1.GetObjectClass(jobject)
//获取普通属性id,第一个参数:类对象, 第二个参数:属性名,第三个参数:属性签名
2.GetFieldID(jclass clazz, const char* name, const char* sig)
//设置int属性的值, 第一个参数:this的意思, 第二个参数:获取属性id, 第三个参数:要设置的值
3.SetIntField(jobject obj, jfieldID fieldID, jint value)
当然这里就只列举SetIntField函数了,同理还有很多,比如:SetCharField,SetFloatField,SetObjectField …。有Set函数肯定也会有Get函数,与之对应的就是GetIntField(jobject obj, jfieldID fieldID),这个函数是获取指定属性的值,参数含义同SetIntField函数
//获取静态属性Id, 第一个参数:类对象, 第二个参数: 属性名, 第三个参数: 属性签名
4.GetStaticFieldID(jclass clazz, const char* name, const char* sig)
//设置静态属性的值, 第一个参数: 类对象, 第二个参数: 属性id, 第三个参数: 要设置的值
5.SetStaticIntField(jclass clazz, jfieldID fieldID, jint value)
//获取函数id, 第一参数:类对象, 第二个参数:函数名, 第三个参数: 函数签名
6.GetMethodID(jclass clazz, const char* name, const char* sig)
//调用java中的无返回值函数, 第一个参数: this的意思, 第二个参数: 函数id, 第三个参数:需要传入的实参
7.CallVoidMethod(jobject obj, jmethodID methodID, …)
//获取静态函数id, 第一个参数: 类对象, 第二个参数: 函数名, 第三个参数: 函数签名
8.GetStaticMethodID(jclass clazz, const char* name, const char* sig)
//调用java中无返回值的静态函数, 第一个参数: 类对象, 第二个参数: 函数id, 第三个参数: 需要传入的实参
9.CallStaticVoidMethod(jclass clazz, jmethodID methodID, …)
//生成一个jstring类型的方法转换,该方法会返回一个jstring类型
10.NewStringUTF(const char* bytes)
//调用java中的对象类型(String类型被认为对象类型),第一参数:this的意思, 第二个参数:函数Id, 第三个参数:需要传入的实参
11.CallObjectMethod(jobject, jmethodID, …);
//获取类中的对象属性,第一个参数:this的意思 , 第二个参数:属性id
12.GetObjectField(jobject obj, jfieldID fieldID)
//根据子类的类对象,获取父类的类对象, 第一参数:子类类对象
13.GetSuperclass(jclass clazz)
//调用java中父类的方法,第一个参数:子类的对象, 第二个参数:父类的类对象, 第三个参数:父类的函数id, 第四个参数:需要传入的实参
14.CallNonvirtualVoidMethod(jobject obj, jclass clazz, jmethodID methodID, …)
NDK生成so的流程
编译生成so的方式有:ndk-build和cmake,本处只介绍NDK的方式。
从上图可知,如果只是使用SDK即只可以使用JAVA,如果要调用底层则需要NDK通过JNI来实现。
so生成具体流程如下:
1、在defaultConfig中添加NDK配置代码:
externalNativeBuild {
ndk {
abiFilters "x86", "armeabi-v7a" //最终要生成的.so所要支持的架构。
}
externalNativeBuild {
ndkBuild {
path "src/main/jni/Android.mk"
}
}
}
各CPU和ABI之间的关系:
CPU\ABI | armeabi | Armeabi-v7a | Arm64-v8a | X86 | X86_64 | Mips | Mips_64 |
---|---|---|---|---|---|---|---|
Armeabi | 支持 | ||||||
Armeabi-v7a | 支持 | 支持 | |||||
Arm64-v8a | 支持 | 支持 | 支持 | ||||
X86 | 支持 | ||||||
X86_64 | 支持 | 支持 | |||||
Mips | 支持 | ||||||
Mips_64 | 支持 | 支持 |
64位设备(arm64-v8a, x86_64, mips64)能够运行32位的函数库,但是是以32位模式运行,在64位平台上运行32位版本的ART和Android组件,将丢失专为64位优化过的性能(ART,webview,media等等)。
向后兼容(向下兼容):即armeabi-v7a CPU支持armeabi-v7a 和armeabi ABI,兼容老版本。
Android如何加载So库?
每一个CPU架构对应一个ABI目录,会自动到相应目录下进行查找。比如Armeabi-v7a的设备会先找armeabi-v7a目录,如果目录存在但是so不存在则会直接报错。如果目录存在且so也存在,则会适配成功。如果没有armeabi-v7a目录,则会向下查找armeabi目录,过程同上。
LOCAL_PATH := $(call my-dir) //设置本地路径,my-dir即包含Android.mk的目录,当前目录
include $(CLEAR_VARS) //负责清理很多LOCAL_xxx,再进行新的设置前需要先清除
LOCAL_MODULE := myTest //生成库的名字libmyTest.so
LOCAL_SRC_FILES := native-lib.cpp //包含需要编译的源文件
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog
include $(BUILD_SHARED_LIBRARY) //生成动态库
3、clean->build后在build\intermediates\ndkBuild下查看
Java调用.so步骤:
1、将so放到jniLibs目录下或者放在libs目录下然后在build.gradle中添加:
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
2、在java中加载库后进行调用相应的接口。
static {
System.loadLibrary("myTest");
}