-
JNI基本规则
-
在JNI规则中,用jfieldID和jmethodID来表示Java类的成员变量和成员函数:
jfieldID GetFieldID(jclass clazz, const char *name, const char *sig); jmethodID GetMethodID(jclass clazz, const char *name, const char *sig);
其中,jclass代表Java类,name表示成员函数或成员变量的名字,sig为这个函数和变量的 签名信息
-
获取java类实例、调用java方法:
env->FindClass("com/mph/jni_test/Tesla"); //完整类名 env->Call<Type>Method(jobject obj,jmethodID id, ..); //调用java层方法 env->CallStatic<Type>Method(jclass clazz,jmethodID id, ..); //调用java层静态方法
-
获取java类变量、设置java类变量赋值:
env->Get<Type>Field(jobject obj, jfieldID id) //获取变量值 env->Set<Type>Field(jobject obj, jfieldID id, jint value) //给一个int类型变量赋值
-
根据jni unicode字符串转成java字符串:
env->NewString(const jchar *unicodeChars, jsize len) env->NewStringUTF(const jchar *unicodeChars, jsize len)
-
根据java字符串转成jni unicode字符串:
env->GetStringChars() env->GetStringUTFChars()
用完必须调用ReleaseStringChars函数或ReleaseStringUTFChars函数来对应地释放资源,否则会导致JVM内存泄露,这一点和jstring的内部实现有关。
-
类型签名:
-
引用类型-> L包名; 比如Ljava/lang/String; //引用类型以L开头,最后都有一个分号
-
基本类型:
- int: I
- boolean: Z
- byte: B
- long: J
- short: S
- double: D
- char: C
-
数组:
-
int数组: [I
-
boolean数组: [Z
-
Object数组: [Ljava/lang/Object;
-
记不住没关系,javap可以打印签名(javap必须至少在包目录的上层目录中执行,比如com.mph.test.Demo.class的话,则必须在com的上层目录中执行):
//其中xxx为编译后的class文件,s表示输出内部数据类型的签名信息,p表示打印所有函数和成员的签名信息,默认只会打印public成员和函数的签名信息。 javap -s -p Xxx.class
-
-
保存java中的实例:
在jni中传进来的java实例如果被回收了,那之后在jni中使用的话就会出问题,因此,jni中提供了3种保存方式。
- Local Reference 函数结束就会释放; //env->DeleteLocalRef(obj)
- Global Reference 如果不主动释放则永远不会被回收 //env->DeleteGlobalRef(obj)
- Weak Global Reference 弱全局引用,一种特殊的Global Reference,在运行过程中可能会被垃圾回收。所以在使用它之前,需要调用JNIEnv的IsSameObject判断它是否被回收了。
-
-
生成库文件
这里只是gcc的非交叉编译,gcc编译器是根据当下平台决定的,因此生成的库文件只适用于当前平台,比如这里我在Mac上通过gcc打出的so库,不能在android app上被正常调用,会报错,因为android系统是arm架构的linux系统。如果要生成对应abi版本的so库需要借助android studio或者自定义的cmake脚本去完成。
-
c&c++类需要编译成so动态库之后才能被加载,生成so库的步骤:
- 生成.o文件: gcc -c xxx.c(xxx.c++)
- 生成so库: gcc -shared -fPIC xxx.o -o xxx.so
- 静态库的生成命令: ar -crv xxx.a xxx.o
-
gcc如果提示找不到jni.h:
gcc -I<JDK_HOME>/include xxx.cpp
-
gcc如果提示找不到jni_md.h:
gcc -I<JDK_HOME>/include/linux xxx.cpp
-
jni.h和jni_md.h如果不在上面这两个目录中的话可以使用find命令查找:
find <目录> -name xxx.h
比如我的jni_md.h不知道在哪,调用
find /Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home -name jni_md.h
-
如果遇到这个问题:
ld: symbol(s) not found for architecture x86_64 clang: error: linker command failed with exit code 1 (use -v to see invocation)
则编译时加上参数:-lstdc++
-
关于PIC:
静态库在链接时代码段和数据段直接合并到一个文件中的,而动态库则是在库文件中设置需要修改的段地址,然后在程序运行被加载后会根据程序即时的运行地址来修改代码段和数据段地址,
因为同时运行的每个程序的实际运行地址不可能相同,因此若是多个程序都用到了这个动态库的话则每个程序都需要代码段和数据段的拷贝,无法达到共享;其次,因为要动态修改代码段的
运行地址,所以代码段必须是可写的,这就导致安全风险。由此产生了PIC(position ignore code(位置无关代码)),PIC实现原理:
-
GOT:在动态库的数据段增加GOT(Global Offset Table),该表的每一项是符号到地址的绝对映射。由于代码段到数据段的偏移是固定的,因此可以在编译时确定代码段中
的某个符号到GOT特定项之间的偏移。这样,代码段中的符号偏移就可以在编译时确定了,在加载时也无需修改代码段的内容,只需要填写位于数据段的GOT的所有项的符号的绝对
地址就完成了。因为数据段本来就是进程间不共享,每个进程独立的一份,因此GOT的设计完全解决了以上两个问题,从而达到两个目的:1,代码段可以在多进程间共享;2,代码段是只读的。 -
PLT:PLT是 Program Linkage Table 的缩写,即程序链接表,PLT的出现是为了延时定位的目的。一个动态库中的函数往往要远多于全局变量,并且被调用的函数往往少于定义的函数。
GOT中包含了该动态库中的所有的全局变量的映射,并且在连接器加载时解析所有的全局变量的地址。如果用同样的方式去处理函数调用符号,则开销会非常大。因此在代码段设计了一个PLT表,
每一项其实是个代码段,用于执行如下逻辑:首次访问时,解析参数和向GOT填写函数地址,后续访问直接访问GOT中的函数地址。如此达到了延时定位的目的。
因此,一个PIC的动态库中,对全局变量使用GOT来映射,对函数调用使用PLT+GOT来映射,从而达到共享库代码段复用,代码段安全访问的目的。而这些就是 PIC 的意义。
-
-
关于MAC上出现"no xxx in java.library.path"的错误时
- "System.out.println(System.getProperty("java.library.path"))"打印后的路径上把so库放进其中一个目录下,结果还是不行;
- 原因是因为Mac下的库后缀为.dylib,库文件生成步骤不变,只需要变一下后缀即可通过。
出现这个问题的前提是我们运行JUnitTest方法加载的库,而不是通过apk运行的android程序加载的(因为android系统使用的是linux系统,因此.so文件是正确的)。- Mac系统 : libXxx.dylib
- Linux系统 : libXxx.so
- Windows系统 : Xxx.dll
-
网友评论