JNI(Java Native Interface):Java调用C/C++的规范。
一、JNI数据类型
基本数据类型:
JAVA | JNI |
---|---|
boolean | jboolean |
byte | jbyte |
char | jchar |
short | jshort |
int | jint |
long | jlong |
float | jfloat |
double | jdouble |
void | void |
引用类型:
JAVA | JNI |
---|---|
Object | jobject |
Class | jclass |
String | jstring |
Object[] | jobjectArray |
boolean[] | jbooleanArray |
char[] | jbyteArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdouble |
Throwable | jthrowable |
二、JNI签名
数据签名:
JAVA | JNI |
---|---|
byte | B |
char | C |
short | S |
int | I |
long | L |
float | F |
double | D |
void | V |
boolean | Z |
object | L开头,然后以/分隔包的完整类型,后面再加; 比如String的签名就是 "Ljava/lang/String;" |
数组 | 基本数据类型: [ + 其类型的域描述符 ,引用类型: [ + 其类型的域描述符 + ; |
多维数组 | N个[ 其余语法和数组一致 |
举例:
int[ ] [I
int[][] [[I
float[ ] [F
String[ ] [Ljava/lang/String;
Object[ ] [Ljava/lang/Object;
方法签名:
(参数的域描述符的叠加)返回
举例:
public int add(int index, String value,int[] arr)
签名:(ILjava/util/String;[I)I
括号内为参数描述符,依次为I、Ljava/util/String;、[I ,括号外为返回值I
通过方法签名和方法名来唯一确认一个JNI函数。
注:
如果不好确认对应方法的签名,可以通过javap命令去查看:
javap -s -p -v xxx.class 找到对应方法去查看具体签名。
三、native函数
extern "C" JNIEXPORT jstring JNICALL
Java_com_stan_jnidemo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "print string";
return env->NewStringUTF(hello.c_str());
}
- extern “C” 使用C语言的命名和调用约定,而不是C++的命名机制。对于JNI来说,使用extern "C" 是必须的。
- JNIEXPORT 宏定义,在Linux平台即将方法返回值之前加入 attribute ((visibility (“default”))) 标识,使so对外可见,即保证在本动态库中声明的方法 , 能够在其他项目中可以被调用。
- JNICALL 宏定义 Linux没有进行定义 , 直接置空。
- jstring 返回参数
- JNIEnv 以java线程为单位的执行环境,通过它来使用JNI API。
- jclass/jobject jclass: 静态方法,jobject: 非静态方法
四、JNI使用模板及案例举例
native函数中使用JNI主要就是玩4类东西:类、对象、属性、方法以及C/C++相关数据操作,非常类似于java的反射。
4.1 类操作:
1)获取jclass通用方法:通过具体java类来获取
jclass claz1 = env->FindClass(“com/stan/base/network/NetworkFactory”);
2)在非静态方法中获取当前函数所在的类:通过native方法参数jobject来转换
jclass claz2 = env->GetObjectClass(jobject);
4.2 对象操作:
1)获取jobject通用型方法:通过jclass创建jobject
jobject obj = env->NewObject(jclass,jmethodID);//这里jmethodID对应的是类的构造方法
2)在非静态方法中获取当前函数所在的对象:直接用native方法参数jobject
4.3 属性操作:
1)获取属性id
jfieldID jfieldId = env->GetFieldID(jclazz, "key", "Ljava/lang/String;”);//获取数据id。
2)get属性值
jint value = env->GetIntField(jobject obj, jfieldID fieldID);//非静态int数据获取。
3)set属性值
env->SetIntField(jobject obj, jfieldID fieldID,jint value);
这里获取属性id和get/set属性值都区分静态非静态。
4.4 方法操作:
1)获取方法id
jmethodID methodId = env->GetMethodID(network_cls, "<init>", "()V”);
2)调用方法
jint result = env->CallIntMethod(jclass,jmethodID);
这里获取方法id和调用方法区别静态非静态。
4.5 数据操作:
这部分是JNI数据类型的创建和JNI数据类型和C/C++数据类型相互转换,这里以字符串举例
1)创建引用类型
jstring jstr = env->NewStringUTF(str.c_str());
2)JNI数据类型和C/C++数据类型相互转换
jboolean *iscopy;
//jstring 转char *
const char *c_str = env->GetStringUTFChars(str, iscopy);//str为方法传入的参数
if (iscopy == JNI_TRUE) {//重新开辟内存空间保存
printf("is copy:true");
} else if (iscopy == JNI_FALSE) {//与str内存空间一致
printf("is copy:false");
}
//释放字符串,如果是重新开辟内存空间的则直接释放,否则通知JVM可以释放,由JVM自行释放。
env->ReleaseStringUTFChars(str, c_str);
这里简单归纳了一些高频操作。JNIEnv中的方法非常多,这里肯定不会一一列举,玩api就是熟能生巧。
案例举例:
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
public String tips;
public void showToast() {
Toast.makeText(this, tips, Toast.LENGTH_SHORT).show();
}
public static native int[] sort(int[] arr);
public native void show();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
int[] arr = {5, 2, 1, 4, 3};
int[] sortArr = sort(arr);
for (int i = 0; i < sortArr.length; i++) {
Log.d("jnitest", sortArr[i] + "");
}
show();
}
}
#native-lib.cpp
extern "C"
JNIEXPORT jintArray JNICALL
Java_com_stan_jnidemo_MainActivity_sort(JNIEnv *env, jclass clazz, jintArray arr) {
jboolean *isCopy;
jint *intArr = env->GetIntArrayElements(arr, isCopy);
int len = env->GetArrayLength(arr);
for (int i = 0; i < len; i++) {
for (int j = 0; j < len - i; j++) {
if (intArr[j] > intArr[j + 1]) {
int tmp = intArr[j + 1];
intArr[j + 1] = intArr[j];
intArr[j] = tmp;
}
}
}
env->ReleaseIntArrayElements(arr, intArr, 0);
return arr;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_stan_jnidemo_MainActivity_show(JNIEnv *env, jobject jobject) {
jclass jclaz = env->GetObjectClass(jobject);
jfieldID fieldId = env->GetFieldID(jclaz, "tips", "Ljava/lang/String;");
jstring jstr = env->NewStringUTF("suc");
env->SetObjectField(jobject, fieldId, jstr);
jmethodID jmethodId = env->GetMethodID(jclaz, "showToast", "()V");
env->CallVoidMethod(jobject, jmethodId);
}
五、引用类型
1)局部引用
定义:jobject NewLocalRef(JNIEnv *env, jobject ref);//ref:全局或者局部引用 ,return:局部引用。
释放方式:1.jvm自动释放,2.DeleteLocalRef(JNIEnv *env, jobject localRef);。JNI局部引用表,512个局部引用,太依赖jvm自动释放会导致溢出。
2)全局引用
定义:jobject NewGlobalRef(JNIEnv *env, jobject obj); //obj:任意类型的引用,return:全局引用,如果内存不足返回NULL。
释放方式:无法垃圾回收,释放它需要显示调用void DeleteGlobalRef(JNIEnv *env, jobject globalRef);
3)弱全局引用
定义:jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);
释放方式:1.当内存不足时,可以被垃圾回收;2void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);
env->IsSameObject(weakGlobalRef, NULL);//判断该对象是否被回收了
六、注册方式
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
public native String stringFromJNI();
}
静态注册
extern "C" JNIEXPORT jstring JNICALL
Java_com_stan_jni_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
动态注册
native端:
- 编写C/C++代码, 实现JNI_Onload()方法;
- 将Java 方法和 C/C++方法通过签名信息一一对应起来;
- 通过JavaVM获取JNIEnv, JNIEnv主要用于获取Java类和调用一些JNI提供的方法;
- 使用类名和对应起来的方法作为参数, 调用JNI提供的函数RegisterNatives()注册方法;
注:
javaVM:与进程对应;
JNIEnv:与线程对应;
//对应实现的native方法
jstring native_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from dynamic C++";
return env->NewStringUTF(hello.c_str());
}
//需要注册的函数列表,放在JNINativeMethod类型的数组中,以后如果需要增加函数,只需在这里添加就行了
static JNINativeMethod gMethods[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void *) native_stringFromJNI}
};
//此函数通过调用RegisterNatives方法来注册我们的函数
static int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *getMethods,
int methodsNum) {
jclass clazz;
//找到声明native方法的类
clazz = env->FindClass(className);
if (clazz == NULL) {
return JNI_FALSE;
}
//注册函数 参数:java类 所要注册的函数数组 注册函数的个数
if (env->RegisterNatives(clazz, getMethods, methodsNum) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
static int registerNatives(JNIEnv *env) {
//指定类的路径,通过FindClass 方法来找到对应的类
const char *className = "com/stan/jni/MainActivity";
return registerNativeMethods(env, className, gMethods,sizeof(gMethods) / sizeof(gMethods[0]));
}
//System.loadLibrary回调函数
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
//获取JNIEnv
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
assert(env != NULL);
//注册函数 registerNatives ->registerNativeMethods
if (!registerNatives(env)) {
return -1;
}
//返回jni 的版本
return JNI_VERSION_1_6;
}
静态注册与动态注册的对比:
- 静态注册:java的native方法与c/c++方法一一对应地书写。当需要修改包名、类名时需要逐个修改;
- 动态注册:动态关联java的native方法与c/c++方法,第一次书写比较繁琐,之后修改类名包名、增加删除方法比较灵活。
七、System.loadLibrary源码简析
这里简单分析下System.loadLibrary(libName)如何加载so,代码基于Android 8.0。
System.loadLibrary(libName) 通过Runtime来加载:
libcore/ojluni/src/main/java/java/lang/Runtime.java
synchronized void loadLibrary0(ClassLoader loader, String libname) {
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
String libraryName = libname;
if (loader != null) {
String filename = loader.findLibrary(libraryName);
if (filename == null) {
// It's not necessarily true that the ClassLoader used
// System.mapLibraryName, but the default setup does, and it's
// misleading to say we didn't find "libMyLibrary.so" when we
// actually searched for "liblibMyLibrary.so.so".
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
String filename = System.mapLibraryName(libraryName);
List<String> candidates = new ArrayList<String>();
String lastError = null;
for (String directory : getLibPaths()) {
String candidate = directory + filename;
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
String error = doLoad(candidate, loader);
if (error == null) {
return; // We successfully loaded the library. Job done.
}
lastError = error;
}
}
if (lastError != null) {
throw new UnsatisfiedLinkError(lastError);
}
throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}
整个方法主要功能是首先通过BaseDexClassLoader.findLibrary去找,找不到则通过getLibPaths()去找,其中之一能找到则通过doLoad去加载so。那么整体看来,Runtime.loadLibrary0 主要分两步走:先找so,在加载so。
1.找so
1)通过ClassLoader.findLibrary来获取so绝对路径
路径包括:
/data/app/com.stan.cmakedemo-PgoTyGH7OVC_1VGtmnSlGg==/lib/arm64, 应用安装拷贝的目录寻找so
/data/app/com.stan.cmakedemo-PgoTyGH7OVC_1VGtmnSlGg==/base.apk!/lib/arm64-v8a, 对apk内部寻找so
/system/lib64, 系统目录
/system/product/lib64 系统设备目录
不同架构和Android版本的手机应该会有差异,这里是小米9,x64架构,Android 10系统,仅供参考。
2)通过getLibPaths()来获取
即String javaLibraryPath = System.getProperty("java.library.path");
路径包括:
/system/lib64, 系统目录
/system/product/lib64 系统设备目录
找so总结:
如果ClassLoader不为null,则通过ClassLoader去找,先找从应用内部找,然后再找系统目录,如果ClassLoader为null,则直接去系统目录找。
2.加载so
Runtime.doLoad
OpenNativeLibrary最终通过dlopen方式打开so文件,返回文件操作符handle。
应用侧so加载重试:System.load(Build.CPU_ABI ) > System.loadLibrary(libName) > System.load(absPath);
八、引入三方so库
1.so在工程中存放位置选择:
- app/libs
- app/src/main/jniLibs
2.CMakeList.txt配置 ( third-party.so)
#1设置so库路径
#CMAKE_SOURCE_DIR :CMakeList.txt文件所在的绝对路径
set(my_lib_path ${CMAKE_SOURCE_DIR}/libs)
#2将第三方库作为动态库引用
add_library(
third-party
SHARED
IMPORTED )
#3指明第三方库的绝对路径
#ANDROID_ABI :当前需要编译的版本平台
set_target_properties(
third-party
PROPERTIES IMPORTED_LOCATION
${my_lib_path}/${ANDROID_ABI}/ third-party.so )
#2+3的另一种写法
add_library( # Sets the name of the library.
third-party
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
# 也可以直接指定路径
${my_lib_path}/${ANDROID_ABI}/ third-party.so)
#4 链接对应的so库
target_link_libraries( # Specifies the target library.
third-party
${log-lib} )
3 gradle配置
android {
defaultConfig {
externalNativeBuild {
cmake {
abiFilters 'armeabi-v7a', 'arm64-v8a’ //选择有so支持的平台
cppFlags ""
}
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt” //CMakeLists.txt路径 也有放cpp目录的:src/main/cpp/CMakeLists.txt
}
}
sourceSets {
main {
jniLibs.srcDir('libs’)//指定so文件夹路径
jni.srcDirs = []
}
}
}